2026-01-19 08:34:17 +01:00
|
|
|
use std::{
|
2026-02-02 07:19:22 +01:00
|
|
|
collections::HashMap,
|
|
|
|
|
sync::{Mutex, MutexGuard},
|
2026-01-19 08:34:17 +01:00
|
|
|
};
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
use testing_framework_core::scenario::{
|
2026-04-23 06:54:21 +02:00
|
|
|
Application, DynError, NodeClients, NodeControlHandle, NodeRuntimeOptions, ReadinessError,
|
|
|
|
|
StartNodeOptions, StartedNode, wait_for_http_ports, wait_for_http_ports_with_timeout,
|
2026-01-19 08:34:17 +01:00
|
|
|
};
|
|
|
|
|
use thiserror::Error;
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
use crate::{
|
2026-04-10 16:43:13 +02:00
|
|
|
env::{
|
2026-04-02 08:18:52 +02:00
|
|
|
LocalDeployerEnv, Node, build_initial_node_configs, build_launch_spec_with_args,
|
|
|
|
|
build_node_from_template, initial_persist_dir, initial_snapshot_dir, node_peer_port,
|
|
|
|
|
readiness_endpoint_path, spawn_node_from_config,
|
2026-04-10 16:43:13 +02:00
|
|
|
},
|
2026-02-02 07:19:22 +01:00
|
|
|
process::ProcessSpawnError,
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-19 08:34:17 +01:00
|
|
|
mod state;
|
|
|
|
|
|
2026-02-05 08:23:14 +02:00
|
|
|
use state::LocalNodeManagerState;
|
2026-02-02 07:19:22 +01:00
|
|
|
|
|
|
|
|
#[derive(Clone)]
|
2026-03-20 08:11:06 +01:00
|
|
|
struct NodeStartSnapshot<Config> {
|
2026-02-02 07:19:22 +01:00
|
|
|
peer_ports: Vec<u16>,
|
|
|
|
|
peer_ports_by_name: HashMap<String, u16>,
|
|
|
|
|
node_name: String,
|
|
|
|
|
index: usize,
|
2026-03-20 08:11:06 +01:00
|
|
|
template_config: Option<Config>,
|
2026-02-02 07:19:22 +01:00
|
|
|
}
|
2026-01-19 08:34:17 +01:00
|
|
|
|
2026-04-23 06:54:21 +02:00
|
|
|
#[derive(Clone, Copy)]
|
|
|
|
|
struct NodeReadinessTarget {
|
|
|
|
|
port: u16,
|
|
|
|
|
runtime: NodeRuntimeOptions,
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 08:34:17 +01:00
|
|
|
#[derive(Debug, Error)]
|
2026-02-02 07:19:22 +01:00
|
|
|
pub enum NodeManagerError {
|
2026-01-19 08:34:17 +01:00
|
|
|
#[error("failed to generate node config: {source}")]
|
|
|
|
|
Config {
|
|
|
|
|
#[source]
|
2026-02-02 07:19:22 +01:00
|
|
|
source: DynError,
|
2026-01-19 08:34:17 +01:00
|
|
|
},
|
|
|
|
|
#[error("failed to spawn node: {source}")]
|
|
|
|
|
Spawn {
|
|
|
|
|
#[source]
|
2026-02-02 07:19:22 +01:00
|
|
|
source: DynError,
|
2026-01-19 08:34:17 +01:00
|
|
|
},
|
|
|
|
|
#[error("{message}")]
|
|
|
|
|
InvalidArgument { message: String },
|
|
|
|
|
#[error("{message}")]
|
|
|
|
|
PortAllocation { message: String },
|
2026-02-05 08:23:14 +02:00
|
|
|
#[error("node config patch failed: {message}")]
|
|
|
|
|
ConfigPatch { message: String },
|
|
|
|
|
#[error("node name '{name}' is unknown")]
|
|
|
|
|
NodeName { name: String },
|
|
|
|
|
#[error("failed to restart node: {source}")]
|
|
|
|
|
Restart {
|
|
|
|
|
#[source]
|
2026-02-02 07:19:22 +01:00
|
|
|
source: DynError,
|
2026-02-05 08:23:14 +02:00
|
|
|
},
|
2026-02-17 10:54:36 +01:00
|
|
|
#[error("failed readiness check: {source}")]
|
|
|
|
|
Readiness {
|
|
|
|
|
#[source]
|
|
|
|
|
source: ReadinessError,
|
|
|
|
|
},
|
2026-01-19 08:34:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
pub struct NodeManager<E: LocalDeployerEnv> {
|
|
|
|
|
descriptors: E::Deployment,
|
|
|
|
|
node_clients: NodeClients<E>,
|
|
|
|
|
keep_tempdir: bool,
|
|
|
|
|
seed: NodeManagerSeed,
|
|
|
|
|
state: Mutex<LocalNodeManagerState<E>>,
|
2026-01-19 08:34:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Default)]
|
2026-02-02 07:19:22 +01:00
|
|
|
pub struct NodeManagerSeed {
|
2026-01-26 08:26:15 +01:00
|
|
|
pub node_count: usize,
|
2026-01-19 08:34:17 +01:00
|
|
|
pub peer_ports: Vec<u16>,
|
|
|
|
|
pub peer_ports_by_name: HashMap<String, u16>,
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
impl<E: LocalDeployerEnv> NodeManager<E> {
|
2026-02-05 08:23:14 +02:00
|
|
|
pub async fn spawn_initial_nodes(
|
2026-02-02 07:19:22 +01:00
|
|
|
descriptors: &E::Deployment,
|
|
|
|
|
keep_tempdir: bool,
|
|
|
|
|
) -> Result<Vec<Node<E>>, ProcessSpawnError> {
|
2026-04-10 16:43:13 +02:00
|
|
|
let configs = build_initial_node_configs::<E>(descriptors)?;
|
2026-02-02 07:19:22 +01:00
|
|
|
let mut spawned = Vec::with_capacity(configs.len());
|
2026-02-17 10:28:50 +01:00
|
|
|
|
|
|
|
|
for (index, config_entry) in configs.into_iter().enumerate() {
|
2026-04-10 16:43:13 +02:00
|
|
|
let persist_dir = initial_persist_dir::<E>(descriptors, &config_entry.name, index);
|
|
|
|
|
let snapshot_dir = initial_snapshot_dir::<E>(descriptors, &config_entry.name, index);
|
2026-02-02 07:19:22 +01:00
|
|
|
spawned.push(
|
|
|
|
|
spawn_node_from_config::<E>(
|
|
|
|
|
config_entry.name,
|
|
|
|
|
config_entry.config,
|
|
|
|
|
keep_tempdir,
|
2026-02-17 10:28:50 +01:00
|
|
|
persist_dir.as_deref(),
|
2026-03-20 08:11:06 +01:00
|
|
|
snapshot_dir.as_deref(),
|
2026-04-02 08:18:52 +02:00
|
|
|
&[],
|
2026-02-02 07:19:22 +01:00
|
|
|
)
|
|
|
|
|
.await?,
|
|
|
|
|
);
|
2026-02-05 08:23:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
Ok(spawned)
|
2026-02-05 08:23:14 +02:00
|
|
|
}
|
2026-02-02 07:19:22 +01:00
|
|
|
pub fn new(descriptors: E::Deployment, node_clients: NodeClients<E>) -> Self {
|
|
|
|
|
Self::new_with_seed(descriptors, node_clients, false, NodeManagerSeed::default())
|
2026-01-19 08:34:17 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn new_with_seed(
|
2026-02-02 07:19:22 +01:00
|
|
|
descriptors: E::Deployment,
|
|
|
|
|
node_clients: NodeClients<E>,
|
|
|
|
|
keep_tempdir: bool,
|
|
|
|
|
seed: NodeManagerSeed,
|
2026-01-19 08:34:17 +01:00
|
|
|
) -> Self {
|
2026-02-05 08:23:14 +02:00
|
|
|
let state = LocalNodeManagerState {
|
2026-01-26 08:26:15 +01:00
|
|
|
node_count: seed.node_count,
|
2026-01-19 08:34:17 +01:00
|
|
|
peer_ports: seed.peer_ports.clone(),
|
|
|
|
|
peer_ports_by_name: seed.peer_ports_by_name.clone(),
|
|
|
|
|
clients_by_name: HashMap::new(),
|
2026-02-05 08:23:14 +02:00
|
|
|
indices_by_name: HashMap::new(),
|
2026-04-23 06:54:21 +02:00
|
|
|
runtime_by_name: HashMap::new(),
|
2026-01-26 08:26:15 +01:00
|
|
|
nodes: Vec::new(),
|
2026-03-20 08:11:06 +01:00
|
|
|
template_config: None,
|
2026-01-19 08:34:17 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Self {
|
|
|
|
|
descriptors,
|
|
|
|
|
node_clients,
|
2026-02-02 07:19:22 +01:00
|
|
|
keep_tempdir,
|
2026-01-19 08:34:17 +01:00
|
|
|
seed,
|
|
|
|
|
state: Mutex::new(state),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
2026-02-02 07:19:22 +01:00
|
|
|
pub fn node_client(&self, name: &str) -> Option<E::NodeClient> {
|
|
|
|
|
let state = self.lock_state();
|
2026-01-19 10:13:13 +01:00
|
|
|
|
2026-01-19 08:34:17 +01:00
|
|
|
state.clients_by_name.get(name).cloned()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 08:23:14 +02:00
|
|
|
#[must_use]
|
|
|
|
|
pub fn node_pid(&self, name: &str) -> Option<u32> {
|
2026-02-02 07:19:22 +01:00
|
|
|
let mut state = self.lock_state();
|
2026-02-05 08:23:14 +02:00
|
|
|
|
|
|
|
|
let index = *state.indices_by_name.get(name)?;
|
|
|
|
|
let node = state.nodes.get_mut(index)?;
|
|
|
|
|
if node.is_running() {
|
|
|
|
|
Some(node.pid())
|
|
|
|
|
} else {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-19 08:34:17 +01:00
|
|
|
pub fn stop_all(&self) {
|
2026-02-02 07:19:22 +01:00
|
|
|
let mut state = self.lock_state();
|
2026-03-20 08:11:06 +01:00
|
|
|
for node in &mut state.nodes {
|
2026-05-01 05:07:12 +02:00
|
|
|
node.stop_blocking();
|
2026-03-20 08:11:06 +01:00
|
|
|
}
|
2026-01-19 10:13:13 +01:00
|
|
|
|
2026-01-26 08:26:15 +01:00
|
|
|
state.nodes.clear();
|
2026-01-19 08:34:17 +01:00
|
|
|
state.peer_ports.clone_from(&self.seed.peer_ports);
|
|
|
|
|
state
|
|
|
|
|
.peer_ports_by_name
|
|
|
|
|
.clone_from(&self.seed.peer_ports_by_name);
|
|
|
|
|
state.clients_by_name.clear();
|
2026-02-05 08:23:14 +02:00
|
|
|
state.indices_by_name.clear();
|
2026-04-23 06:54:21 +02:00
|
|
|
state.runtime_by_name.clear();
|
2026-01-26 08:26:15 +01:00
|
|
|
state.node_count = self.seed.node_count;
|
2026-03-20 08:11:06 +01:00
|
|
|
state.template_config = None;
|
2026-01-19 08:34:17 +01:00
|
|
|
self.node_clients.clear();
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
pub fn initialize_with_nodes(&self, nodes: Vec<Node<E>>) {
|
2026-02-05 08:23:14 +02:00
|
|
|
self.node_clients.clear();
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
let mut state = self.lock_state();
|
|
|
|
|
clear_registered_nodes(&mut state);
|
2026-02-05 08:23:14 +02:00
|
|
|
|
|
|
|
|
for (idx, node) in nodes.into_iter().enumerate() {
|
2026-02-02 07:19:22 +01:00
|
|
|
let name = default_node_label(idx);
|
2026-04-10 16:43:13 +02:00
|
|
|
let port = node_peer_port::<E>(&node);
|
2026-02-02 07:19:22 +01:00
|
|
|
let client = node.client();
|
2026-02-05 08:23:14 +02:00
|
|
|
|
|
|
|
|
self.node_clients.add_node(client.clone());
|
2026-04-23 06:54:21 +02:00
|
|
|
state.register_node(&name, port, client, NodeRuntimeOptions::default(), node);
|
2026-02-05 08:23:14 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[must_use]
|
2026-02-02 07:19:22 +01:00
|
|
|
pub fn node_clients(&self) -> NodeClients<E> {
|
2026-02-05 08:23:14 +02:00
|
|
|
self.node_clients.clone()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
pub async fn wait_network_ready(&self) -> Result<(), ReadinessError> {
|
|
|
|
|
let ports: Vec<_> = {
|
|
|
|
|
let state = self.lock_state();
|
|
|
|
|
state
|
|
|
|
|
.nodes
|
|
|
|
|
.iter()
|
|
|
|
|
.map(|node| node.endpoints().api.port())
|
|
|
|
|
.collect()
|
|
|
|
|
};
|
2026-01-19 08:34:17 +01:00
|
|
|
|
2026-02-17 06:07:23 +01:00
|
|
|
if ports.is_empty() {
|
2026-02-02 07:19:22 +01:00
|
|
|
return Ok(());
|
|
|
|
|
}
|
2026-01-19 08:34:17 +01:00
|
|
|
|
2026-04-10 16:43:13 +02:00
|
|
|
wait_for_http_ports(&ports, readiness_endpoint_path::<E>()).await
|
2026-01-19 08:34:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-17 10:54:36 +01:00
|
|
|
pub async fn wait_node_ready(&self, name: &str) -> Result<(), NodeManagerError> {
|
2026-04-23 06:54:21 +02:00
|
|
|
let target = self.readiness_target(name)?;
|
2026-02-17 10:54:36 +01:00
|
|
|
|
2026-04-23 06:54:21 +02:00
|
|
|
wait_for_http_ports_with_timeout(
|
|
|
|
|
&[target.port],
|
|
|
|
|
readiness_endpoint_path::<E>(),
|
|
|
|
|
target.runtime.start_timeout,
|
|
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|source| NodeManagerError::Readiness { source })
|
2026-02-17 10:54:36 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
pub async fn start_node_with(
|
2026-01-19 08:34:17 +01:00
|
|
|
&self,
|
|
|
|
|
name: &str,
|
2026-02-02 07:19:22 +01:00
|
|
|
options: StartNodeOptions<E>,
|
|
|
|
|
) -> Result<StartedNode<E>, NodeManagerError> {
|
|
|
|
|
let snapshot = self.start_snapshot(name)?;
|
2026-01-19 08:34:17 +01:00
|
|
|
|
2026-04-10 16:43:13 +02:00
|
|
|
let mut built = build_node_from_template::<E>(
|
2026-01-19 08:34:17 +01:00
|
|
|
&self.descriptors,
|
2026-02-02 07:19:22 +01:00
|
|
|
snapshot.index,
|
|
|
|
|
&snapshot.peer_ports_by_name,
|
2026-01-19 08:34:17 +01:00
|
|
|
&options,
|
2026-02-02 07:19:22 +01:00
|
|
|
&snapshot.peer_ports,
|
2026-03-20 08:11:06 +01:00
|
|
|
snapshot.template_config.as_ref(),
|
2026-02-02 07:19:22 +01:00
|
|
|
)
|
|
|
|
|
.map_err(|source| NodeManagerError::Config { source })?;
|
|
|
|
|
|
|
|
|
|
if let Some(config_patch) = &options.config_patch {
|
|
|
|
|
built.config =
|
|
|
|
|
config_patch(built.config).map_err(|source| NodeManagerError::ConfigPatch {
|
|
|
|
|
message: source.to_string(),
|
|
|
|
|
})?;
|
|
|
|
|
}
|
2026-02-05 08:23:14 +02:00
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
let client = self
|
|
|
|
|
.spawn_and_register_node(
|
|
|
|
|
&snapshot.node_name,
|
|
|
|
|
built.network_port,
|
|
|
|
|
built.config,
|
2026-04-23 06:54:21 +02:00
|
|
|
options.runtime,
|
2026-02-02 07:19:22 +01:00
|
|
|
options.persist_dir.as_deref(),
|
2026-03-20 08:11:06 +01:00
|
|
|
options.snapshot_dir.as_deref(),
|
2026-04-23 06:54:21 +02:00
|
|
|
&options.args,
|
2026-02-02 07:19:22 +01:00
|
|
|
)
|
2026-01-26 08:26:15 +01:00
|
|
|
.await?;
|
2026-01-19 08:34:17 +01:00
|
|
|
|
|
|
|
|
Ok(StartedNode {
|
2026-02-02 07:19:22 +01:00
|
|
|
name: snapshot.node_name,
|
|
|
|
|
client,
|
2026-01-19 08:34:17 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
pub async fn restart_node(&self, name: &str) -> Result<(), NodeManagerError> {
|
2026-04-23 06:54:21 +02:00
|
|
|
self.restart_node_with(name, StartNodeOptions::default())
|
|
|
|
|
.await
|
2026-04-02 08:18:52 +02:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 06:54:21 +02:00
|
|
|
pub async fn restart_node_with(
|
2026-04-02 08:18:52 +02:00
|
|
|
&self,
|
|
|
|
|
name: &str,
|
2026-04-23 06:54:21 +02:00
|
|
|
options: StartNodeOptions<E>,
|
2026-04-02 08:18:52 +02:00
|
|
|
) -> Result<(), NodeManagerError> {
|
2026-02-02 07:19:22 +01:00
|
|
|
let (index, mut node) = self.take_node(name)?;
|
2026-02-05 08:23:14 +02:00
|
|
|
|
2026-04-23 06:54:21 +02:00
|
|
|
validate_restart_options(&options)?;
|
|
|
|
|
|
2026-04-02 08:18:52 +02:00
|
|
|
let launch = build_launch_spec_with_args::<E>(
|
|
|
|
|
node.config(),
|
|
|
|
|
node.working_dir(),
|
|
|
|
|
name,
|
2026-04-23 06:54:21 +02:00
|
|
|
&options.args,
|
2026-04-02 08:18:52 +02:00
|
|
|
)
|
|
|
|
|
.map_err(|source| NodeManagerError::Config { source })?;
|
|
|
|
|
|
|
|
|
|
if let Err(source) = node.restart_with_launch(launch).await {
|
2026-02-02 07:19:22 +01:00
|
|
|
self.put_node_back(index, node);
|
2026-02-05 08:23:14 +02:00
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
return Err(NodeManagerError::Restart {
|
|
|
|
|
source: source.into(),
|
|
|
|
|
});
|
2026-02-05 08:23:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
self.put_node_back(index, node);
|
2026-04-23 06:54:21 +02:00
|
|
|
self.store_runtime_options(name, options.runtime);
|
2026-02-02 07:19:22 +01:00
|
|
|
|
2026-02-05 08:23:14 +02:00
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
pub async fn stop_node(&self, name: &str) -> Result<(), NodeManagerError> {
|
|
|
|
|
let (index, mut node) = self.take_node(name)?;
|
2026-02-05 08:23:14 +02:00
|
|
|
|
|
|
|
|
node.stop().await;
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
self.put_node_back(index, node);
|
2026-02-05 08:23:14 +02:00
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
2026-01-26 08:26:15 +01:00
|
|
|
async fn spawn_and_register_node(
|
2026-01-19 08:34:17 +01:00
|
|
|
&self,
|
|
|
|
|
node_name: &str,
|
|
|
|
|
network_port: u16,
|
2026-02-02 07:19:22 +01:00
|
|
|
config: <E as Application>::NodeConfig,
|
2026-04-23 06:54:21 +02:00
|
|
|
runtime: NodeRuntimeOptions,
|
2026-02-02 07:19:22 +01:00
|
|
|
persist_dir: Option<&std::path::Path>,
|
2026-03-20 08:11:06 +01:00
|
|
|
snapshot_dir: Option<&std::path::Path>,
|
2026-04-02 08:18:52 +02:00
|
|
|
extra_args: &[String],
|
2026-02-02 07:19:22 +01:00
|
|
|
) -> Result<E::NodeClient, NodeManagerError> {
|
|
|
|
|
let node = spawn_node_from_config::<E>(
|
|
|
|
|
node_name.to_string(),
|
|
|
|
|
config,
|
|
|
|
|
self.keep_tempdir,
|
|
|
|
|
persist_dir,
|
2026-03-20 08:11:06 +01:00
|
|
|
snapshot_dir,
|
2026-04-02 08:18:52 +02:00
|
|
|
extra_args,
|
2026-02-02 07:19:22 +01:00
|
|
|
)
|
|
|
|
|
.await
|
|
|
|
|
.map_err(|source| NodeManagerError::Spawn {
|
|
|
|
|
source: source.into(),
|
|
|
|
|
})?;
|
|
|
|
|
let client = node.client();
|
2026-01-19 08:34:17 +01:00
|
|
|
|
2026-01-26 08:26:15 +01:00
|
|
|
self.node_clients.add_node(client.clone());
|
2026-01-19 08:34:17 +01:00
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
let mut state = self.lock_state();
|
2026-03-20 08:11:06 +01:00
|
|
|
if state.template_config.is_none() && snapshot_dir.is_some() {
|
|
|
|
|
state.template_config = Some(node.config().clone());
|
|
|
|
|
}
|
2026-01-19 10:13:13 +01:00
|
|
|
|
2026-04-23 06:54:21 +02:00
|
|
|
state.register_node(node_name, network_port, client.clone(), runtime, node);
|
2026-01-19 08:34:17 +01:00
|
|
|
|
|
|
|
|
Ok(client)
|
|
|
|
|
}
|
2026-02-02 07:19:22 +01:00
|
|
|
|
|
|
|
|
fn take_node(&self, name: &str) -> Result<(usize, Node<E>), NodeManagerError> {
|
|
|
|
|
let mut state = self.lock_state();
|
|
|
|
|
remove_node_from_state(&mut state, name)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn put_node_back(&self, index: usize, node: Node<E>) {
|
|
|
|
|
let mut state = self.lock_state();
|
|
|
|
|
reinsert_node_at(&mut state, index, node);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 06:54:21 +02:00
|
|
|
fn store_runtime_options(&self, name: &str, runtime: NodeRuntimeOptions) {
|
|
|
|
|
let mut state = self.lock_state();
|
|
|
|
|
state.runtime_by_name.insert(name.to_string(), runtime);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn readiness_target(&self, name: &str) -> Result<NodeReadinessTarget, NodeManagerError> {
|
|
|
|
|
let state = self.lock_state();
|
|
|
|
|
let index = node_index(&state, name)?;
|
|
|
|
|
let port = node_api_port(&state, index, name)?;
|
|
|
|
|
let runtime = node_runtime_options(&state, name);
|
|
|
|
|
|
|
|
|
|
Ok(NodeReadinessTarget { port, runtime })
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-20 08:11:06 +01:00
|
|
|
fn start_snapshot(
|
|
|
|
|
&self,
|
|
|
|
|
requested_name: &str,
|
|
|
|
|
) -> Result<NodeStartSnapshot<E::NodeConfig>, NodeManagerError> {
|
2026-02-02 07:19:22 +01:00
|
|
|
let state = self.lock_state();
|
|
|
|
|
let index = state.node_count;
|
|
|
|
|
let node_name = validate_new_node_name::<E>(state.node_count, &state, requested_name)?;
|
|
|
|
|
|
|
|
|
|
Ok(NodeStartSnapshot {
|
|
|
|
|
peer_ports: state.peer_ports.clone(),
|
|
|
|
|
peer_ports_by_name: state.peer_ports_by_name.clone(),
|
|
|
|
|
node_name,
|
|
|
|
|
index,
|
2026-03-20 08:11:06 +01:00
|
|
|
template_config: state.template_config.clone(),
|
2026-02-02 07:19:22 +01:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn lock_state(&self) -> MutexGuard<'_, LocalNodeManagerState<E>> {
|
|
|
|
|
self.state
|
|
|
|
|
.lock()
|
|
|
|
|
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
|
|
|
|
}
|
2026-01-19 08:34:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
fn clear_registered_nodes<E: LocalDeployerEnv>(state: &mut LocalNodeManagerState<E>) {
|
|
|
|
|
state.nodes.clear();
|
|
|
|
|
state.peer_ports.clear();
|
|
|
|
|
state.peer_ports_by_name.clear();
|
|
|
|
|
state.clients_by_name.clear();
|
|
|
|
|
state.indices_by_name.clear();
|
2026-04-23 06:54:21 +02:00
|
|
|
state.runtime_by_name.clear();
|
2026-02-02 07:19:22 +01:00
|
|
|
state.node_count = 0;
|
2026-03-20 08:11:06 +01:00
|
|
|
state.template_config = None;
|
2026-02-02 07:19:22 +01:00
|
|
|
}
|
2026-02-05 08:23:14 +02:00
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
fn validate_new_node_name<E: LocalDeployerEnv>(
|
|
|
|
|
node_count: usize,
|
|
|
|
|
state: &LocalNodeManagerState<E>,
|
|
|
|
|
requested_name: &str,
|
|
|
|
|
) -> Result<String, NodeManagerError> {
|
|
|
|
|
let label = normalize_node_name(node_count, requested_name);
|
|
|
|
|
|
|
|
|
|
if state.peer_ports_by_name.contains_key(&label) {
|
|
|
|
|
return Err(NodeManagerError::InvalidArgument {
|
|
|
|
|
message: format!("node name '{label}' already exists"),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(label)
|
2026-02-05 08:23:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
fn normalize_node_name(index: usize, requested_name: &str) -> String {
|
|
|
|
|
if requested_name.trim().is_empty() {
|
|
|
|
|
return default_node_label(index);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if requested_name.starts_with("node-") {
|
|
|
|
|
return requested_name.to_string();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
format!("node-{requested_name}")
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 06:54:21 +02:00
|
|
|
fn validate_restart_options<E: LocalDeployerEnv>(
|
|
|
|
|
options: &StartNodeOptions<E>,
|
|
|
|
|
) -> Result<(), NodeManagerError> {
|
|
|
|
|
if options.peers.is_some() {
|
|
|
|
|
return Err(unsupported_restart_override("peer selection"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if options.config_override.is_some() {
|
|
|
|
|
return Err(unsupported_restart_override("config override"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if options.config_patch.is_some() {
|
|
|
|
|
return Err(unsupported_restart_override("config patch"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if options.persist_dir.is_some() {
|
|
|
|
|
return Err(unsupported_restart_override("persist dir"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if options.snapshot_dir.is_some() {
|
|
|
|
|
return Err(unsupported_restart_override("snapshot dir"));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn unsupported_restart_override(field: &str) -> NodeManagerError {
|
|
|
|
|
NodeManagerError::InvalidArgument {
|
|
|
|
|
message: format!("restart_node_with does not support {field} overrides"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn node_index<E: LocalDeployerEnv>(
|
|
|
|
|
state: &LocalNodeManagerState<E>,
|
|
|
|
|
name: &str,
|
|
|
|
|
) -> Result<usize, NodeManagerError> {
|
|
|
|
|
state
|
|
|
|
|
.indices_by_name
|
|
|
|
|
.get(name)
|
|
|
|
|
.copied()
|
|
|
|
|
.ok_or_else(|| NodeManagerError::NodeName {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn node_api_port<E: LocalDeployerEnv>(
|
|
|
|
|
state: &LocalNodeManagerState<E>,
|
|
|
|
|
index: usize,
|
|
|
|
|
name: &str,
|
|
|
|
|
) -> Result<u16, NodeManagerError> {
|
|
|
|
|
state
|
|
|
|
|
.nodes
|
|
|
|
|
.get(index)
|
|
|
|
|
.map(|node| node.endpoints().api.port())
|
|
|
|
|
.ok_or_else(|| NodeManagerError::NodeName {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn node_runtime_options<E: LocalDeployerEnv>(
|
|
|
|
|
state: &LocalNodeManagerState<E>,
|
|
|
|
|
name: &str,
|
|
|
|
|
) -> NodeRuntimeOptions {
|
|
|
|
|
state.runtime_by_name.get(name).copied().unwrap_or_default()
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
fn default_node_label(index: usize) -> String {
|
|
|
|
|
format!("node-{index}")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn remove_node_from_state<E: LocalDeployerEnv>(
|
|
|
|
|
state: &mut LocalNodeManagerState<E>,
|
|
|
|
|
name: &str,
|
|
|
|
|
) -> Result<(usize, Node<E>), NodeManagerError> {
|
|
|
|
|
let Some(index) = state.indices_by_name.get(name).copied() else {
|
|
|
|
|
return Err(NodeManagerError::NodeName {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
});
|
2026-02-05 08:23:14 +02:00
|
|
|
};
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
if index >= state.nodes.len() {
|
|
|
|
|
return Err(NodeManagerError::NodeName {
|
|
|
|
|
name: name.to_string(),
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Ok((index, state.nodes.remove(index)))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn reinsert_node_at<E: LocalDeployerEnv>(
|
|
|
|
|
state: &mut LocalNodeManagerState<E>,
|
|
|
|
|
index: usize,
|
|
|
|
|
node: Node<E>,
|
|
|
|
|
) {
|
|
|
|
|
if index <= state.nodes.len() {
|
|
|
|
|
state.nodes.insert(index, node);
|
|
|
|
|
} else {
|
|
|
|
|
state.nodes.push(node);
|
|
|
|
|
}
|
2026-02-05 08:23:14 +02:00
|
|
|
}
|
|
|
|
|
|
2026-01-19 08:34:17 +01:00
|
|
|
#[async_trait::async_trait]
|
2026-02-02 07:19:22 +01:00
|
|
|
impl<E: LocalDeployerEnv> NodeControlHandle<E> for NodeManager<E> {
|
2026-02-05 08:23:14 +02:00
|
|
|
async fn restart_node(&self, name: &str) -> Result<(), DynError> {
|
|
|
|
|
self.restart_node(name).await.map_err(|err| err.into())
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 06:54:21 +02:00
|
|
|
async fn restart_node_with(
|
|
|
|
|
&self,
|
|
|
|
|
name: &str,
|
|
|
|
|
options: StartNodeOptions<E>,
|
|
|
|
|
) -> Result<(), DynError> {
|
|
|
|
|
self.restart_node_with(name, options)
|
2026-04-02 08:18:52 +02:00
|
|
|
.await
|
|
|
|
|
.map_err(|err| err.into())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-05 08:23:14 +02:00
|
|
|
async fn stop_node(&self, name: &str) -> Result<(), DynError> {
|
|
|
|
|
self.stop_node(name).await.map_err(|err| err.into())
|
2026-01-19 08:34:17 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
async fn start_node(&self, name: &str) -> Result<StartedNode<E>, DynError> {
|
|
|
|
|
self.start_node_with(name, StartNodeOptions::<E>::default())
|
2026-01-19 08:34:17 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|err| err.into())
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-26 08:26:15 +01:00
|
|
|
async fn start_node_with(
|
2026-01-19 08:34:17 +01:00
|
|
|
&self,
|
|
|
|
|
name: &str,
|
2026-02-02 07:19:22 +01:00
|
|
|
options: StartNodeOptions<E>,
|
|
|
|
|
) -> Result<StartedNode<E>, DynError> {
|
2026-01-26 08:26:15 +01:00
|
|
|
self.start_node_with(name, options)
|
2026-01-19 08:34:17 +01:00
|
|
|
.await
|
|
|
|
|
.map_err(|err| err.into())
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 07:19:22 +01:00
|
|
|
fn node_client(&self, name: &str) -> Option<E::NodeClient> {
|
2026-01-19 08:34:17 +01:00
|
|
|
self.node_client(name)
|
|
|
|
|
}
|
2026-02-05 08:23:14 +02:00
|
|
|
|
|
|
|
|
fn node_pid(&self, name: &str) -> Option<u32> {
|
|
|
|
|
self.node_pid(name)
|
|
|
|
|
}
|
2026-01-19 08:34:17 +01:00
|
|
|
}
|