1pub mod chain_upgrade;
2pub mod jamchain;
3pub mod node;
4pub mod parachain;
5pub mod relaychain;
6
7use std::{
8 cell::RefCell,
9 collections::HashMap,
10 path::PathBuf,
11 rc::Rc,
12 sync::Arc,
13 time::{Duration, SystemTime, UNIX_EPOCH},
14};
15
16use configuration::{
17 para_states::{Initial, Running},
18 shared::{helpers::generate_unique_node_name_from_names, node::EnvVar},
19 types::{Arg, Command, Image, ParaId, Port, ValidationContext},
20 ParachainConfig, ParachainConfigBuilder, RegistrationStrategy,
21};
22use provider::{types::TransferedFile, DynNamespace, ProviderError};
23use serde::{Deserialize, Serialize};
24use support::{
25 constants::{RELAY_NOT_NONE, THIS_IS_A_BUG},
26 fs::FileSystem,
27};
28use tokio::sync::RwLock;
29use tracing::{error, warn};
30
31use self::{
32 jamchain::Jamchain,
33 node::{JamNetworkNode, NetworkNode, NodeKind, SpawnedNode},
34 parachain::Parachain,
35 relaychain::Relaychain,
36};
37use crate::{
38 generators::{self, chain_spec::ChainSpec},
39 network_spec::{self, NetworkSpec},
40 observability::{self, ObservabilityInfo, ObservabilityState},
41 shared::{
42 constants::{NODE_MONITORING_FAILURE_THRESHOLD_SECONDS, NODE_MONITORING_INTERVAL_SECONDS},
43 macros,
44 types::{ChainDefaultContext, RegisterParachainOptions},
45 },
46 spawner::{self, SpawnNodeCtx},
47 utils::write_zombie_json,
48 ScopedFilesystem, ZombieRole,
49};
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
54pub enum NodeContext {
55 Rc,
56 Para {
57 para_id: ParaId,
58 is_cumulus_based: bool,
59 },
60 Jam,
61}
62
63#[derive(Serialize)]
64pub struct Network<T: FileSystem> {
65 #[serde(skip)]
66 ns: DynNamespace,
67 #[serde(skip)]
68 filesystem: T,
69 #[serde(skip_serializing_if = "Option::is_none")]
70 relay: Option<Relaychain>,
71 initial_spec: NetworkSpec,
72 parachains: HashMap<u32, Vec<Parachain>>,
73 #[serde(skip_serializing_if = "Option::is_none")]
74 jamchain: Option<Jamchain>,
75 #[serde(skip)]
81 nodes_by_name: HashMap<String, Arc<dyn SpawnedNode>>,
82 #[serde(skip)]
83 nodes_to_watch: Arc<RwLock<Vec<Arc<dyn SpawnedNode>>>>,
84 #[serde(skip_serializing_if = "Option::is_none")]
85 start_time_ts: Option<String>,
86 #[serde(skip)]
87 observability: ObservabilityState,
88}
89
90impl<T: FileSystem> std::fmt::Debug for Network<T> {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 f.debug_struct("Network")
93 .field("ns", &"ns_skipped")
94 .field("relay", &self.relay)
95 .field("initial_spec", &self.initial_spec)
96 .field("parachains", &self.parachains)
97 .field("jamchain", &self.jamchain)
98 .field("nodes_by_name", &self.nodes_by_name.keys())
99 .field("observability", &self.observability)
100 .finish()
101 }
102}
103
104macros::create_add_options!(AddNodeOptions {
105 chain_spec: Option<PathBuf>,
106 override_eth_key: Option<String>
107});
108
109macros::create_add_options!(AddCollatorOptions {
110 chain_spec: Option<PathBuf>,
111 chain_spec_relay: Option<PathBuf>,
112 override_eth_key: Option<String>
113});
114
115impl<T: FileSystem> Network<T> {
116 pub(crate) fn new_with_relay(
117 relay: Relaychain,
118 ns: DynNamespace,
119 fs: T,
120 initial_spec: NetworkSpec,
121 ) -> Self {
122 Self::new(Some(relay), ns, fs, initial_spec)
123 }
124
125 pub(crate) fn new_without_relay(ns: DynNamespace, fs: T, initial_spec: NetworkSpec) -> Self {
127 Self::new(None, ns, fs, initial_spec)
128 }
129
130 fn new(relay: Option<Relaychain>, ns: DynNamespace, fs: T, initial_spec: NetworkSpec) -> Self {
131 Self {
132 ns,
133 filesystem: fs,
134 relay,
135 initial_spec,
136 parachains: Default::default(),
137 jamchain: Default::default(),
138 nodes_by_name: Default::default(),
139 nodes_to_watch: Default::default(),
140 start_time_ts: Default::default(),
141 observability: ObservabilityState::default(),
142 }
143 }
144
145 pub fn ns_name(&self) -> String {
147 self.ns.name().to_string()
148 }
149
150 pub fn base_dir(&self) -> Option<&str> {
151 self.ns.base_dir().to_str()
152 }
153
154 pub fn relaychain(&self) -> &Relaychain {
155 self.relay
156 .as_ref()
157 .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}"))
158 }
159
160 pub fn try_relaychain(&self) -> Option<&Relaychain> {
162 self.relay.as_ref()
163 }
164
165 fn relaychain_mut(&mut self) -> &mut Relaychain {
166 self.relay
167 .as_mut()
168 .expect(&format!("{RELAY_NOT_NONE}, {THIS_IS_A_BUG}"))
169 }
170
171 pub async fn destroy(mut self) -> Result<(), ProviderError> {
173 if let Err(e) = self.stop_observability().await {
174 warn!("⚠️ Failed to cleanup observability stack: {e}");
175 }
176 self.ns.destroy().await
177 }
178
179 pub fn observability(&self) -> Option<&ObservabilityInfo> {
180 self.observability.as_runnnig()
181 }
182
183 pub fn observability_state(&self) -> &ObservabilityState {
184 &self.observability
185 }
186
187 pub async fn add_node(
213 &mut self,
214 name: impl Into<String>,
215 options: AddNodeOptions,
216 ) -> Result<(), anyhow::Error> {
217 let name = generate_unique_node_name_from_names(
218 name,
219 &mut self.nodes_by_name.keys().cloned().collect(),
220 );
221
222 let relaychain = self.relaychain();
223
224 let chain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec {
225 chain_spec_custom_path.clone()
226 } else {
227 PathBuf::from(format!(
228 "{}/{}.json",
229 self.ns.base_dir().to_string_lossy(),
230 relaychain.chain
231 ))
232 };
233
234 let chain_context = ChainDefaultContext {
235 default_command: self.initial_spec.relaychain().default_command.as_ref(),
236 default_image: self.initial_spec.relaychain().default_image.as_ref(),
237 default_resources: self.initial_spec.relaychain().default_resources.as_ref(),
238 default_db_snapshot: self.initial_spec.relaychain().default_db_snapshot.as_ref(),
239 default_args: self.initial_spec.relaychain().default_args.iter().collect(),
240 };
241
242 let mut node_spec = network_spec::node::NodeSpec::from_ad_hoc(
243 &name,
244 options.into(),
245 &chain_context,
246 false,
247 false,
248 )?;
249
250 node_spec.available_args_output = Some(
251 self.initial_spec
252 .node_available_args_output(&node_spec, self.ns.clone())
253 .await?,
254 );
255
256 let base_dir = self.ns.base_dir().to_string_lossy();
257 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
258
259 let resolved_db_snapshots = generators::resolve_db_snapshots(
260 std::iter::once(&node_spec),
261 &self.ns,
262 &self.filesystem,
263 )
264 .await?;
265
266 let ctx = SpawnNodeCtx {
267 chain_id: &relaychain.chain_id,
268 parachain_id: None,
269 chain: &relaychain.chain,
270 role: ZombieRole::Node,
271 ns: &self.ns,
272 scoped_fs: &scoped_fs,
273 parachain: None,
274 bootnodes_addr: &vec![],
275 wait_ready: true,
276 nodes_by_name: serde_json::to_value(&self.nodes_by_name)?,
277 global_settings: &self.initial_spec.global_settings,
278 resolved_db_snapshots: &resolved_db_snapshots,
279 };
280
281 let global_files_to_inject = vec![TransferedFile::new(
282 chain_spec_path,
283 PathBuf::from(format!("/cfg/{}.json", relaychain.chain)),
284 )];
285
286 let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
287
288 node.wait_until_is_up(self.initial_spec.global_settings.network_spawn_timeout())
300 .await?;
301
302 self.add_running_node(node.clone(), None).await;
304
305 self.write_zombie_json().await?;
307
308 generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
309
310 Ok(())
311 }
312
313 pub async fn add_collator(
340 &mut self,
341 name: impl Into<String>,
342 options: AddCollatorOptions,
343 para_id: u32,
344 ) -> Result<(), anyhow::Error> {
345 let name = generate_unique_node_name_from_names(
346 name,
347 &mut self.nodes_by_name.keys().cloned().collect(),
348 );
349 let spec = self
350 .initial_spec
351 .parachains
352 .iter()
353 .find(|para| para.id == para_id)
354 .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
355 let role = if spec.is_cumulus_based {
356 ZombieRole::CumulusCollator
357 } else {
358 ZombieRole::Collator
359 };
360 let chain_context = ChainDefaultContext {
361 default_command: spec.default_command.as_ref(),
362 default_image: spec.default_image.as_ref(),
363 default_resources: spec.default_resources.as_ref(),
364 default_db_snapshot: spec.default_db_snapshot.as_ref(),
365 default_args: spec.default_args.iter().collect(),
366 };
367
368 let relay_chain = self.relaychain().chain.clone();
369 let relay_chain_id = self.relaychain().chain_id.clone();
370
371 let parachain = self
372 .parachains
373 .get_mut(¶_id)
374 .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?
375 .get_mut(0)
376 .ok_or(anyhow::anyhow!(format!("parachain: {para_id} not found!")))?;
377
378 let base_dir = self.ns.base_dir().to_string_lossy();
379 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
380
381 let relaychain_spec_path = if let Some(chain_spec_custom_path) = &options.chain_spec_relay {
382 chain_spec_custom_path.clone()
383 } else {
384 PathBuf::from(format!(
385 "{}/{}.json",
386 self.ns.base_dir().to_string_lossy(),
387 relay_chain
388 ))
389 };
390
391 let mut global_files_to_inject = vec![TransferedFile::new(
392 relaychain_spec_path,
393 PathBuf::from(format!("/cfg/{}.json", relay_chain)),
394 )];
395
396 let para_chain_spec_local_path = if let Some(para_chain_spec_custom) = &options.chain_spec {
397 Some(para_chain_spec_custom.clone())
398 } else if let Some(para_spec_path) = ¶chain.chain_spec_path {
399 Some(PathBuf::from(format!(
400 "{}/{}",
401 self.ns.base_dir().to_string_lossy(),
402 para_spec_path.to_string_lossy()
403 )))
404 } else {
405 None
406 };
407
408 if let Some(para_spec_path) = para_chain_spec_local_path {
409 global_files_to_inject.push(TransferedFile::new(
410 para_spec_path,
411 PathBuf::from(format!("/cfg/{para_id}.json")),
412 ));
413 }
414
415 let mut node_spec = network_spec::node::NodeSpec::from_ad_hoc(
416 name,
417 options.into(),
418 &chain_context,
419 true,
420 spec.is_evm_based,
421 )?;
422
423 node_spec.available_args_output = Some(
424 self.initial_spec
425 .node_available_args_output(&node_spec, self.ns.clone())
426 .await?,
427 );
428
429 let resolved_db_snapshots = generators::resolve_db_snapshots(
430 std::iter::once(&node_spec),
431 &self.ns,
432 &self.filesystem,
433 )
434 .await?;
435
436 let ctx = SpawnNodeCtx {
438 chain_id: &relay_chain_id,
439 parachain_id: parachain.chain_id.as_deref(),
440 chain: &relay_chain,
441 role,
442 ns: &self.ns,
443 scoped_fs: &scoped_fs,
444 parachain: Some(spec),
445 bootnodes_addr: &vec![],
446 wait_ready: true,
447 nodes_by_name: serde_json::to_value(&self.nodes_by_name)?,
448 global_settings: &self.initial_spec.global_settings,
449 resolved_db_snapshots: &resolved_db_snapshots,
450 };
451
452 let node = spawner::spawn_node(&node_spec, global_files_to_inject, &ctx).await?;
453
454 node.wait_until_is_up(self.initial_spec.global_settings.network_spawn_timeout())
456 .await?;
457
458 self.add_running_node(node, Some(para_id)).await;
459
460 self.write_zombie_json().await?;
462
463 generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
464
465 Ok(())
466 }
467
468 pub fn para_config_builder(&self) -> ParachainConfigBuilder<Initial, Running> {
473 let used_ports = self
474 .nodes_iter()
475 .map(|node| node.spec())
476 .flat_map(|spec| {
477 [
478 spec.ws_port.0,
479 spec.rpc_port.0,
480 spec.prometheus_port.0,
481 spec.p2p_port.0,
482 ]
483 })
484 .collect();
485
486 let used_nodes_names = self.nodes_by_name.keys().cloned().collect();
487
488 let used_para_ids = self
490 .parachains
491 .iter()
492 .map(|(id, paras)| (*id, paras.len().saturating_sub(1) as u8))
493 .collect();
494
495 let context = ValidationContext {
496 used_ports,
497 used_nodes_names,
498 used_para_ids,
499 };
500 let context = Rc::new(RefCell::new(context));
501
502 ParachainConfigBuilder::new_with_running(context)
503 }
504
505 pub async fn add_parachain(
539 &mut self,
540 para_config: &ParachainConfig,
541 custom_relaychain_spec: Option<PathBuf>,
542 custom_parchain_fs_prefix: Option<String>,
543 ) -> Result<(), anyhow::Error> {
544 let base_dir = self.ns.base_dir().to_string_lossy().to_string();
545 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
546
547 let mut global_files_to_inject = vec![];
548
549 let relay_chain_id = if let Some(custom_path) = custom_relaychain_spec {
551 global_files_to_inject.push(TransferedFile::new(
553 custom_path.clone(),
554 PathBuf::from(format!("/cfg/{}.json", self.relaychain().chain)),
555 ));
556 let content = std::fs::read_to_string(custom_path)?;
557 ChainSpec::chain_id_from_spec(&content)?
558 } else {
559 global_files_to_inject.push(TransferedFile::new(
560 PathBuf::from(format!(
561 "{}/{}",
562 scoped_fs.base_dir,
563 self.relaychain().chain_spec_path.to_string_lossy()
564 )),
565 PathBuf::from(format!("/cfg/{}.json", self.relaychain().chain)),
566 ));
567 self.relaychain().chain_id.clone()
568 };
569
570 let mut para_spec = network_spec::parachain::ParachainSpec::from_config(
571 para_config,
572 relay_chain_id.as_str().try_into()?,
573 )?;
574
575 let chain_spec_raw_path = para_spec
576 .build_chain_spec(
577 &relay_chain_id,
578 &self.ns,
579 &scoped_fs,
580 para_spec.post_process_script.clone().as_deref(),
581 )
582 .await?;
583
584 let para_path_prefix = if let Some(custom_prefix) = custom_parchain_fs_prefix {
586 custom_prefix
587 } else {
588 para_spec.id.to_string()
589 };
590
591 scoped_fs.create_dir(¶_path_prefix).await?;
592 para_spec
594 .genesis_state
595 .build(
596 chain_spec_raw_path.as_ref(),
597 format!("{}/genesis-state", para_path_prefix),
598 &self.ns,
599 &scoped_fs,
600 None,
601 )
602 .await?;
603 para_spec
604 .genesis_wasm
605 .build(
606 chain_spec_raw_path.as_ref(),
607 format!("{}/para_spec-wasm", para_path_prefix),
608 &self.ns,
609 &scoped_fs,
610 None,
611 )
612 .await?;
613
614 let parachain =
615 Parachain::from_spec(¶_spec, &global_files_to_inject, &scoped_fs).await?;
616 let parachain_id = parachain.chain_id.clone();
617
618 let resolved_db_snapshots = generators::resolve_db_snapshots(
619 para_spec.collators.iter(),
620 &self.ns,
621 &self.filesystem,
622 )
623 .await?;
624
625 let ctx_para = SpawnNodeCtx {
627 parachain: Some(¶_spec),
628 parachain_id: parachain_id.as_deref(),
629 role: if para_spec.is_cumulus_based {
630 ZombieRole::CumulusCollator
631 } else {
632 ZombieRole::Collator
633 },
634 bootnodes_addr: ¶_config
635 .bootnodes_addresses()
636 .iter()
637 .map(|&a| a.to_string())
638 .collect(),
639 chain_id: &self.relaychain().chain_id,
640 chain: &self.relaychain().chain,
641 ns: &self.ns,
642 scoped_fs: &scoped_fs,
643 wait_ready: false,
644 nodes_by_name: serde_json::to_value(&self.nodes_by_name)?,
645 global_settings: &self.initial_spec.global_settings,
646 resolved_db_snapshots: &resolved_db_snapshots,
647 };
648
649 let first_node_url = self
651 .relaychain()
652 .nodes
653 .first()
654 .ok_or(anyhow::anyhow!(
655 "At least one node of the relaychain should be running"
656 ))?
657 .ws_uri();
658
659 if para_config.registration_strategy() == Some(&RegistrationStrategy::UsingExtrinsic) {
660 let register_para_options = RegisterParachainOptions {
661 id: parachain.para_id,
662 wasm_path: para_spec
664 .genesis_wasm
665 .artifact_path()
666 .ok_or(anyhow::anyhow!(
667 "artifact path for wasm must be set at this point",
668 ))?
669 .to_path_buf(),
670 state_path: para_spec
671 .genesis_state
672 .artifact_path()
673 .ok_or(anyhow::anyhow!(
674 "artifact path for state must be set at this point",
675 ))?
676 .to_path_buf(),
677 node_ws_url: first_node_url.to_string(),
678 onboard_as_para: para_spec.onboard_as_parachain,
679 seed: None, finalization: false,
681 };
682
683 Parachain::register(register_para_options, &scoped_fs).await?;
684 }
685
686 let spawning_tasks = para_spec
688 .collators
689 .iter()
690 .map(|node| spawner::spawn_node(node, parachain.files_to_inject.clone(), &ctx_para));
691
692 let running_nodes = futures::future::try_join_all(spawning_tasks).await?;
693
694 let waiting_tasks = running_nodes.iter().map(|node| {
696 node.wait_until_is_up(self.initial_spec.global_settings.network_spawn_timeout())
697 });
698
699 let _ = futures::future::try_join_all(waiting_tasks).await?;
700
701 let running_para_id = parachain.para_id;
702 self.add_para(parachain);
703 for node in running_nodes {
704 self.add_running_node(node, Some(running_para_id)).await;
705 }
706
707 self.write_zombie_json().await?;
709
710 generators::cleanup_db_snapshot_cache(&resolved_db_snapshots).await;
711
712 Ok(())
713 }
714
715 pub async fn register_parachain(&mut self, para_id: u32) -> Result<(), anyhow::Error> {
754 let para = self
755 .initial_spec
756 .parachains
757 .iter()
758 .find(|p| p.id == para_id)
759 .ok_or(anyhow::anyhow!(
760 "no parachain with id = {para_id} available",
761 ))?;
762 let para_genesis_config = para.get_genesis_config()?;
763 let first_node_url = self
764 .relaychain()
765 .nodes
766 .first()
767 .ok_or(anyhow::anyhow!(
768 "At least one node of the relaychain should be running"
769 ))?
770 .ws_uri();
771 let register_para_options: RegisterParachainOptions = RegisterParachainOptions {
772 id: para_id,
773 wasm_path: para_genesis_config.wasm_path.clone(),
775 state_path: para_genesis_config.state_path.clone(),
776 node_ws_url: first_node_url.to_string(),
777 onboard_as_para: para_genesis_config.as_parachain,
778 seed: None, finalization: false,
780 };
781 let base_dir = self.ns.base_dir().to_string_lossy().to_string();
782 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
783 Parachain::register(register_para_options, &scoped_fs).await?;
784
785 Ok(())
786 }
787
788 fn get_node_as<'a, N: SpawnedNode>(&'a self, name: &str) -> Result<&'a N, anyhow::Error> {
793 let node = self.nodes_by_name.get(name).ok_or_else(|| {
794 anyhow::anyhow!(
795 "can't find node with name: {name:?}, should be one of {}",
796 self.node_names().join(", ")
797 )
798 })?;
799
800 let node_any: &dyn std::any::Any = node.as_ref();
802 node_any.downcast_ref::<N>().ok_or_else(|| {
803 anyhow::anyhow!(
804 "node {name:?} is a '{}' node, it can't be used as a {}",
805 node.kind(),
806 std::any::type_name::<N>()
807 )
808 })
809 }
810
811 pub fn get_node(&self, name: impl Into<String>) -> Result<&NetworkNode, anyhow::Error> {
813 self.get_node_as::<NetworkNode>(&name.into())
814 }
815
816 pub fn get_jam_node(&self, name: impl Into<String>) -> Result<&JamNetworkNode, anyhow::Error> {
818 self.get_node_as::<JamNetworkNode>(&name.into())
819 }
820
821 pub fn get_any_node(&self, name: impl Into<String>) -> Result<&dyn SpawnedNode, anyhow::Error> {
826 let name = name.into();
827 self.nodes_by_name
828 .get(&name)
829 .map(|node| node.as_ref())
830 .ok_or_else(|| {
831 anyhow::anyhow!(
832 "can't find node with name: {name:?}, should be one of {}",
833 self.node_names().join(", ")
834 )
835 })
836 }
837
838 pub fn node_names(&self) -> Vec<String> {
839 self.nodes_by_name.keys().cloned().collect::<Vec<_>>()
840 }
841
842 pub fn nodes(&self) -> Vec<&NetworkNode> {
844 self.nodes_iter().collect()
845 }
846
847 pub fn all_nodes(&self) -> Vec<&dyn SpawnedNode> {
849 self.nodes_by_name.values().map(|n| n.as_ref()).collect()
850 }
851
852 pub fn nodes_of_kind(&self, kind: NodeKind) -> Vec<&dyn SpawnedNode> {
854 self.nodes_by_name
855 .values()
856 .filter(|n| n.kind() == kind)
857 .map(|n| n.as_ref())
858 .collect()
859 }
860
861 pub async fn detach(&self) {
862 self.ns.detach().await
863 }
864
865 pub(crate) async fn add_running_node(&mut self, node: NetworkNode, para_id: Option<u32>) {
867 let node = Arc::new(node);
868 if let Some(para_id) = para_id {
869 if let Some(para) = self.parachains.get_mut(¶_id).and_then(|p| p.get_mut(0)) {
870 para.collators.push(node.clone());
871 } else {
872 unreachable!()
874 }
875 } else {
876 self.relaychain_mut().nodes.push(node.clone());
877 }
878
879 self.register_running_node(node).await;
880 }
881
882 pub(crate) async fn add_running_jam_node(&mut self, node: JamNetworkNode) {
884 let node = Arc::new(node);
885 if let Some(jamchain) = self.jamchain.as_mut() {
886 jamchain.nodes.push(node.clone());
887 } else {
888 unreachable!()
890 }
891
892 self.register_running_node(node).await;
893 }
894
895 async fn register_running_node(&mut self, node: Arc<impl SpawnedNode>) {
898 node.core().set_is_running(true);
899 node.core().set_last_start_ts(
900 SystemTime::now()
901 .duration_since(UNIX_EPOCH)
902 .expect("Timestamp should be valid")
903 .as_secs(),
904 );
905
906 let node: Arc<dyn SpawnedNode> = node;
907 self.nodes_by_name
908 .insert(node.name().to_string(), node.clone());
909 self.nodes_to_watch.write().await.push(node);
910 }
911
912 pub(crate) fn add_para(&mut self, para: Parachain) {
913 self.parachains.entry(para.para_id).or_default().push(para);
914 }
915
916 pub(crate) async fn write_zombie_json(&self) -> Result<(), anyhow::Error> {
917 let base_dir = self.ns.base_dir().to_string_lossy();
918 let scoped_fs = ScopedFilesystem::new(&self.filesystem, &base_dir);
919 let ns_name = self.ns.name();
920
921 write_zombie_json(serde_json::to_value(self)?, scoped_fs, ns_name).await?;
922 Ok(())
923 }
924
925 pub fn name(&self) -> &str {
926 self.ns.name()
927 }
928
929 pub fn parachain(&self, para_id: u32) -> Option<&Parachain> {
939 self.parachains.get(¶_id)?.first()
940 }
941
942 pub fn parachain_by_unique_id(&self, unique_id: impl AsRef<str>) -> Option<&Parachain> {
950 self.parachains
951 .values()
952 .flat_map(|p| p.iter())
953 .find(|p| p.unique_id == unique_id.as_ref())
954 }
955
956 pub fn parachains(&self) -> Vec<&Parachain> {
957 self.parachains.values().flatten().collect()
958 }
959
960 pub(crate) fn nodes_iter(&self) -> impl Iterator<Item = &NetworkNode> {
961 self.relay
962 .iter()
963 .flat_map(|relay| relay.nodes.iter())
964 .chain(
965 self.parachains
966 .values()
967 .flat_map(|p| p.iter())
968 .flat_map(|p| &p.collators),
969 )
970 .map(|n| n.as_ref())
971 }
972
973 pub async fn wait_until_is_up(&self, timeout_secs: u64) -> Result<(), anyhow::Error> {
983 let handles = self
984 .nodes_by_name
985 .values()
986 .map(|node| node.wait_until_is_up(timeout_secs));
987
988 futures::future::try_join_all(handles).await?;
989
990 Ok(())
991 }
992
993 pub async fn pause(&self) -> Result<(), anyhow::Error> {
999 futures::future::try_join_all(self.nodes_iter().map(|n| n.pause())).await?;
1000 Ok(())
1001 }
1002
1003 pub async fn resume(&self) -> Result<(), anyhow::Error> {
1009 futures::future::try_join_all(self.nodes_iter().map(|n| n.resume())).await?;
1010 Ok(())
1011 }
1012
1013 pub async fn start_observability(
1042 &mut self,
1043 config: &configuration::ObservabilityConfig,
1044 ) -> Result<&ObservabilityInfo, anyhow::Error> {
1045 if self.observability().is_some() {
1046 self.stop_observability().await?;
1047 }
1048
1049 let nodes = self.nodes();
1050 let info = observability::spawn_observability_stack(
1051 config,
1052 &nodes,
1053 self.ns.name(),
1054 self.ns.base_dir(),
1055 &self.filesystem,
1056 )
1057 .await?;
1058
1059 self.observability = ObservabilityState::Running(info);
1060 self.observability()
1061 .ok_or_else(|| anyhow::anyhow!("observability state was just set but is not running"))
1062 }
1063
1064 pub async fn stop_observability(&mut self) -> Result<(), anyhow::Error> {
1069 if let ObservabilityState::Running(info) =
1070 std::mem::replace(&mut self.observability, ObservabilityState::Stopped)
1071 {
1072 observability::cleanup_observability_stack(&info).await?;
1073 }
1074 Ok(())
1075 }
1076
1077 pub(crate) fn spawn_watching_task(&self) {
1078 let nodes_to_watch = Arc::clone(&self.nodes_to_watch);
1079 let ns = Arc::clone(&self.ns);
1080 let node_bootstrap_timeout = self.initial_spec.global_settings.node_spawn_timeout();
1081
1082 tokio::spawn(async move {
1083 loop {
1084 tokio::time::sleep(Duration::from_secs(NODE_MONITORING_INTERVAL_SECONDS)).await;
1085
1086 let guard = nodes_to_watch.read().await;
1087 let nodes = guard.iter().filter(|n| n.is_running()).collect::<Vec<_>>();
1088
1089 let all_running = {
1090 let all_running =
1091 futures::future::try_join_all(nodes.iter().map(|n| {
1092 n.wait_until_is_up(NODE_MONITORING_FAILURE_THRESHOLD_SECONDS)
1093 }))
1094 .await;
1095
1096 if nodes.iter().any(|n| !n.is_running()) {
1098 continue;
1099 } else {
1100 all_running
1101 }
1102 };
1103
1104 if let Err(e) = all_running {
1105 if let Some(node) = nodes.iter().find(|n| n.name() == e.to_string()) {
1107 let now = SystemTime::now()
1108 .duration_since(UNIX_EPOCH)
1109 .expect("get current ts should work.")
1110 .as_secs();
1111 if node_bootstrap_timeout as u64 > (now - node.last_start_ts()) {
1112 warn!("[{}] still in bootstrap window from last starting ({}), continue waiting...", node.name(), node.last_start_ts());
1113 continue;
1114 }
1115 }
1116
1117 warn!("\n\t🧟 One of the nodes crashed: {e}. tearing the network down...");
1118
1119 if let Err(e) = ns.destroy().await {
1120 error!("an error occurred during network teardown: {}", e);
1121 }
1122
1123 std::process::exit(1);
1124 }
1125 }
1126 });
1127 }
1128
1129 pub(crate) fn set_parachains(&mut self, parachains: HashMap<u32, Vec<Parachain>>) {
1130 self.parachains = parachains;
1131 }
1132
1133 pub(crate) fn insert_node(&mut self, node: Arc<dyn SpawnedNode>) {
1134 self.nodes_by_name.insert(node.name().to_string(), node);
1135 }
1136
1137 pub(crate) fn set_jamchain(&mut self, jamchain: Jamchain) {
1138 self.jamchain = Some(jamchain);
1139 }
1140
1141 pub fn jamchain(&self) -> Option<&Jamchain> {
1143 self.jamchain.as_ref()
1144 }
1145
1146 pub(crate) fn set_start_time_ts(&mut self, start_time: SystemTime) {
1147 if let Ok(start_time_ts) = start_time.duration_since(SystemTime::UNIX_EPOCH) {
1148 self.start_time_ts = Some(start_time_ts.as_millis().to_string());
1149 } else {
1150 warn!("⚠️ Error getting start_time timestamp");
1152 }
1153 }
1154}