475 lines
13 KiB
Rust
Raw Normal View History

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::{
Application, DynError, NodeClients, NodeControlHandle, ReadinessError, StartNodeOptions,
StartedNode, wait_for_http_ports,
2026-01-19 08:34:17 +01:00
};
use thiserror::Error;
2026-02-02 07:19:22 +01:00
use crate::{
env::{
LocalDeployerEnv, Node, build_initial_node_configs, build_node_from_template,
initial_persist_dir, initial_snapshot_dir, node_peer_port, readiness_endpoint_path,
spawn_node_from_config,
},
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
#[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
},
#[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> {
let configs = build_initial_node_configs::<E>(descriptors)?;
2026-02-02 07:19:22 +01:00
let mut spawned = Vec::with_capacity(configs.len());
for (index, config_entry) in configs.into_iter().enumerate() {
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,
persist_dir.as_deref(),
2026-03-20 08:11:06 +01:00
snapshot_dir.as_deref(),
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-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 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 {
node.start_kill();
}
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-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);
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());
state.register_node(&name, port, client, node);
}
}
#[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
if ports.is_empty() {
2026-02-02 07:19:22 +01:00
return Ok(());
}
2026-01-19 08:34:17 +01:00
wait_for_http_ports(&ports, readiness_endpoint_path::<E>()).await
2026-01-19 08:34:17 +01:00
}
pub async fn wait_node_ready(&self, name: &str) -> Result<(), NodeManagerError> {
let port = {
let state = self.lock_state();
let index =
*state
.indices_by_name
.get(name)
.ok_or_else(|| NodeManagerError::NodeName {
name: name.to_string(),
})?;
state
.nodes
.get(index)
.map(|node| node.endpoints().api.port())
.ok_or_else(|| NodeManagerError::NodeName {
name: name.to_string(),
})?
};
wait_for_http_ports(&[port], readiness_endpoint_path::<E>())
.await
.map_err(|source| NodeManagerError::Readiness { source })
}
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
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,
options.persist_dir.as_deref(),
2026-03-20 08:11:06 +01:00
options.snapshot_dir.as_deref(),
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> {
let (index, mut node) = self.take_node(name)?;
2026-02-05 08:23:14 +02:00
2026-02-02 07:19:22 +01:00
if let Err(source) = node.restart().await {
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-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,
persist_dir: Option<&std::path::Path>,
2026-03-20 08:11:06 +01:00
snapshot_dir: Option<&std::path::Path>,
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-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-26 08:26:15 +01:00
state.register_node(node_name, network_port, client.clone(), 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-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();
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}")
}
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())
}
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
}