Skip to main content

simvar_harness/
lib.rs

1//! Deterministic simulation test harness for concurrent systems.
2//!
3//! This crate provides a framework for running deterministic simulations of distributed
4//! systems and concurrent applications. It allows you to test complex scenarios involving
5//! multiple actors (hosts and clients) with controlled timing, randomness, and networking.
6//!
7//! # Features
8//!
9//! * **Deterministic execution** - Same seed produces identical simulation results
10//! * **Host and client actors** - Model persistent services (hosts) and ephemeral clients
11//! * **Simulation lifecycle hooks** - Customize behavior at key points via [`SimBootstrap`]
12//! * **Built-in TUI** - Optional terminal UI for monitoring simulation progress
13//! * **Parallel execution** - Run multiple simulation runs concurrently
14//! * **Cancellation support** - Graceful shutdown with Ctrl-C handling
15//!
16//! # Example
17//!
18//! ```rust,no_run
19//! # use simvar_harness::{run_simulation, SimBootstrap, Sim, SimConfig};
20//! # use simvar_harness::host::HostResult;
21//! # use simvar_harness::client::ClientResult;
22//! struct MyBootstrap;
23//!
24//! impl SimBootstrap for MyBootstrap {
25//!     fn build_sim(&self, config: SimConfig) -> SimConfig {
26//!         config
27//!     }
28//!
29//!     fn on_start(&self, sim: &mut impl Sim) {
30//!         // Spawn a host actor
31//!         sim.host("server", || async {
32//!             // Server logic here
33//!             Ok(())
34//!         });
35//!
36//!         // Spawn a client actor
37//!         sim.client("client", async {
38//!             // Client logic here
39//!             Ok(())
40//!         });
41//!     }
42//! }
43//!
44//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
45//! let results = run_simulation(MyBootstrap)?;
46//! # Ok(())
47//! # }
48//! ```
49//!
50//! # Environment Variables
51//!
52//! * `SIMULATOR_RUNS` - Number of simulation runs to execute (default: 1)
53//! * `SIMULATOR_MAX_PARALLEL` - Maximum parallel runs (default: number of CPUs)
54//! * `SIMULATOR_SEED` - Fixed random seed for deterministic runs
55//! * `SIMULATOR_EPOCH_OFFSET` - Fixed epoch offset override (when `time` feature is enabled)
56//! * `SIMULATOR_EPOCH_MIN` / `SIMULATOR_EPOCH_MAX` - Bounded random epoch offset (inclusive)
57//! * `SIMULATOR_EPOCH_RANGE_PROFILE` - Epoch profile (`low`, `wide`, `full`)
58//! * `NO_TUI` - Disable terminal UI when set
59
60#![cfg_attr(feature = "fail-on-warnings", deny(warnings))]
61#![warn(clippy::all, clippy::pedantic, clippy::nursery, clippy::cargo)]
62#![allow(clippy::multiple_crate_versions)]
63
64use std::{
65    cell::RefCell,
66    collections::BTreeMap,
67    panic::AssertUnwindSafe,
68    sync::{
69        Arc, LazyLock, Mutex,
70        atomic::{AtomicBool, AtomicU64},
71    },
72    time::{Duration, SystemTime},
73};
74
75use client::{Client, ClientResult};
76use color_backtrace::{BacktracePrinter, termcolor::Buffer};
77use config::run_info;
78use formatting::TimeFormat as _;
79use host::{Host, HostResult};
80use simvar_utils::{
81    cancel_global_simulation, cancel_simulation, is_global_simulator_cancelled,
82    is_simulator_cancelled, reset_simulator_cancellation_token, worker_thread_id,
83};
84use switchy::{
85    random::{rand::rand::seq::SliceRandom as _, rng},
86    time::simulator::{current_step, next_step, reset_step},
87    unsync::thread_id,
88};
89
90pub use config::{SimConfig, SimProperties, SimResult, SimRunProperties};
91pub use simvar_utils as utils;
92
93pub use switchy;
94
95/// Client actor types and utilities.
96///
97/// Provides the [`Client`] type for modeling ephemeral actors in simulations.
98///
99/// [`Client`]: client::Client
100pub mod client;
101
102mod config;
103
104/// Time formatting utilities.
105///
106/// Provides the [`TimeFormat`] trait for converting time durations
107/// in milliseconds into human-readable formatted strings.
108///
109/// [`TimeFormat`]: formatting::TimeFormat
110pub mod formatting;
111
112/// Host actor types and utilities.
113///
114/// Provides the [`Host`] type for modeling persistent actors that can be restarted.
115///
116/// [`Host`]: host::Host
117pub mod host;
118
119mod logging;
120/// Interaction planning utilities.
121///
122/// Provides the [`InteractionPlan`] trait for managing sequences of planned interactions.
123///
124/// [`InteractionPlan`]: plan::InteractionPlan
125pub mod plan;
126
127#[cfg(feature = "tui")]
128mod tui;
129
130const USE_TUI: bool = cfg!(feature = "tui") && std::option_env!("NO_TUI").is_none();
131
132thread_local! {
133    static PANIC: RefCell<Option<String>> = const { RefCell::new(None) };
134}
135
136static RUNS: LazyLock<u64> = LazyLock::new(|| {
137    std::env::var("SIMULATOR_RUNS")
138        .ok()
139        .map_or(1, |x| x.parse::<u64>().unwrap())
140});
141
142static END_SIM: LazyLock<AtomicBool> = LazyLock::new(|| AtomicBool::new(false));
143
144#[cfg(feature = "tui")]
145static DISPLAY_STATE: LazyLock<tui::DisplayState> = LazyLock::new(tui::DisplayState::new);
146
147/// Errors that can occur during simulation execution.
148#[derive(Debug, thiserror::Error)]
149pub enum Error {
150    /// I/O operation failed.
151    #[error(transparent)]
152    IO(#[from] std::io::Error),
153    /// Simulation step returned an error.
154    #[error(transparent)]
155    Step(Box<dyn std::error::Error + Send>),
156    /// Task join operation failed.
157    #[error(transparent)]
158    Join(#[from] switchy::unsync::task::JoinError),
159}
160
161fn ctrl_c() {
162    log::debug!("ctrl_c called");
163    #[cfg(feature = "tui")]
164    if USE_TUI {
165        DISPLAY_STATE.exit();
166    }
167    end_sim();
168}
169
170/// Signals all running simulations to stop.
171///
172/// This sets a global flag and cancels the global simulation, causing all
173/// simulation runs to terminate gracefully.
174pub fn end_sim() {
175    END_SIM.store(true, std::sync::atomic::Ordering::SeqCst);
176
177    if !is_global_simulator_cancelled() {
178        cancel_global_simulation();
179    }
180}
181
182fn try_get_backtrace() -> Option<String> {
183    let bt = std::backtrace::Backtrace::force_capture();
184    let bt = btparse::deserialize(&bt).ok()?;
185
186    let mut buffer = Buffer::ansi();
187    BacktracePrinter::default()
188        .print_trace(&bt, &mut buffer)
189        .ok()?;
190
191    Some(String::from_utf8_lossy(buffer.as_slice()).to_string())
192}
193
194/// Executes one or more simulation runs using the provided bootstrap implementation.
195///
196/// This is the main entry point for running simulations. It sets up the environment,
197/// handles parallel execution if configured, and returns the results of all runs.
198/// The number of runs and parallelism level can be controlled via the `SIMULATOR_RUNS`
199/// and `SIMULATOR_MAX_PARALLEL` environment variables.
200///
201/// # Panics
202///
203/// * If `SIMULATOR_RUNS` or `SIMULATOR_MAX_PARALLEL` is set but cannot be parsed
204///   as a `u64`.
205/// * If the Ctrl-C handler cannot be installed.
206/// * If converting the available parallelism value to `u64` fails unexpectedly.
207/// * If TUI shutdown thread joining fails when the `tui` feature is enabled.
208/// * If elapsed wall-clock time cannot be measured because system time goes
209///   backwards during a run.
210///
211/// # Errors
212///
213/// * The contents of this function are wrapped in a `catch_unwind` call, so if
214///   any panic happens, it will be wrapped into an error on the outer `Result`
215/// * If the `Sim` `step` returns an error, we return that in an Ok(Err(e))
216/// * If simulation worker threads report orchestration errors
217/// * If logger initialization fails when `pretty_env_logger` is enabled
218///
219/// # Examples
220///
221/// ```rust,no_run
222/// use simvar_harness::{Sim, SimBootstrap, SimConfig, run_simulation};
223///
224/// struct Bootstrap;
225///
226/// impl SimBootstrap for Bootstrap {
227///     fn build_sim(&self, config: SimConfig) -> SimConfig {
228///         config
229///     }
230///
231///     fn on_start(&self, sim: &mut impl Sim) {
232///         sim.client("client", async { Ok(()) });
233///     }
234/// }
235///
236/// let _results = run_simulation(Bootstrap)?;
237/// # Ok::<(), Box<dyn std::error::Error>>(())
238/// ```
239#[allow(clippy::let_and_return)]
240pub fn run_simulation<B: SimBootstrap>(
241    bootstrap: B,
242) -> Result<Vec<SimResult>, Box<dyn std::error::Error>> {
243    static MAX_PARALLEL: LazyLock<u64> = LazyLock::new(|| {
244        std::env::var("SIMULATOR_MAX_PARALLEL").ok().map_or_else(
245            || {
246                u64::try_from(std::thread::available_parallelism().map_or(1usize, Into::into))
247                    .unwrap()
248            },
249            |x| x.parse::<u64>().unwrap(),
250        )
251    });
252
253    // claim thread_id 1 for main thread
254    let _ = thread_id();
255
256    ctrlc::set_handler(ctrl_c).expect("Error setting Ctrl-C handler");
257
258    #[cfg(feature = "pretty_env_logger")]
259    logging::init_pretty_env_logger()?;
260
261    #[cfg(feature = "tui")]
262    let tui_handle = if USE_TUI {
263        Some(tui::spawn(DISPLAY_STATE.clone()))
264    } else {
265        None
266    };
267
268    std::panic::set_hook(Box::new({
269        move |x| {
270            let thread_id = thread_id();
271            let mut panic_str = x.to_string();
272            if let Some(bt) = try_get_backtrace() {
273                panic_str = format!("{panic_str}\n{bt}");
274            }
275            log::debug!("caught panic on thread_id={thread_id}: {panic_str}");
276            PANIC.with_borrow_mut(|x| *x = Some(panic_str));
277            end_sim();
278        }
279    }));
280
281    let runs = *RUNS;
282    let max_parallel = *MAX_PARALLEL;
283
284    log::debug!("Running simulation with max_parallel={max_parallel}");
285
286    let sim_orchestrator = SimOrchestrator::new(
287        bootstrap,
288        runs,
289        max_parallel,
290        #[cfg(feature = "tui")]
291        DISPLAY_STATE.clone(),
292    );
293
294    let resp = sim_orchestrator.start();
295
296    #[cfg(feature = "tui")]
297    if let Some(tui_handle) = tui_handle {
298        tui_handle.join().unwrap()?;
299    }
300
301    #[cfg(feature = "tui")]
302    if USE_TUI && let Ok(results) = &resp {
303        eprintln!(
304            "{}",
305            results
306                .iter()
307                .filter(|x| !x.is_success())
308                .map(SimResult::to_string)
309                .collect::<Vec<_>>()
310                .join("\n"),
311        );
312    }
313
314    resp
315}
316
317struct SimOrchestrator<B: SimBootstrap> {
318    bootstrap: B,
319    runs: u64,
320    max_parallel: u64,
321    #[cfg(feature = "tui")]
322    display_state: tui::DisplayState,
323}
324
325impl<B: SimBootstrap> SimOrchestrator<B> {
326    const fn new(
327        bootstrap: B,
328        runs: u64,
329        max_parallel: u64,
330        #[cfg(feature = "tui")] display_state: tui::DisplayState,
331    ) -> Self {
332        Self {
333            bootstrap,
334            runs,
335            max_parallel,
336            #[cfg(feature = "tui")]
337            display_state,
338        }
339    }
340
341    fn start(self) -> Result<Vec<SimResult>, Box<dyn std::error::Error>> {
342        let parallel = std::cmp::min(self.runs, self.max_parallel);
343        let run_index = Arc::new(AtomicU64::new(0));
344
345        let bootstrap = Arc::new(self.bootstrap);
346        let results = Arc::new(Mutex::new(BTreeMap::new()));
347
348        if self.max_parallel == 0 {
349            for run_number in 1..=self.runs {
350                let simulation = Simulation::new(
351                    &*bootstrap,
352                    #[cfg(feature = "tui")]
353                    self.display_state.clone(),
354                );
355
356                let result = simulation.run(run_number, None);
357
358                results.lock().unwrap().insert(0, result);
359
360                if END_SIM.load(std::sync::atomic::Ordering::SeqCst) {
361                    break;
362                }
363            }
364        } else {
365            let mut threads = vec![];
366
367            for i in 0..parallel {
368                log::debug!("starting thread {i}");
369
370                let run_index = run_index.clone();
371                let bootstrap = bootstrap.clone();
372                let runs = self.runs;
373                let results = results.clone();
374                #[cfg(feature = "tui")]
375                let display_state = self.display_state.clone();
376
377                let handle = std::thread::spawn(move || {
378                    let _ = thread_id();
379                    let thread_id = worker_thread_id();
380                    let simulation = Simulation::new(
381                        &*bootstrap,
382                        #[cfg(feature = "tui")]
383                        display_state.clone(),
384                    );
385
386                    loop {
387                        if END_SIM.load(std::sync::atomic::Ordering::SeqCst) {
388                            log::debug!("simulation has ended. thread {i} ({thread_id}) finished");
389                            break;
390                        }
391
392                        let run_index = run_index.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
393                        if run_index >= runs {
394                            log::debug!(
395                                "finished all runs ({runs}). thread {i} ({thread_id}) finished"
396                            );
397                            break;
398                        }
399
400                        log::debug!(
401                            "starting simulation run_index={run_index} on thread {i} ({thread_id})"
402                        );
403
404                        let result = simulation.run(run_index + 1, Some(thread_id));
405
406                        results.lock().unwrap().insert(thread_id, result);
407
408                        log::debug!(
409                            "simulation finished run_index={run_index} on thread {i} ({thread_id})"
410                        );
411                    }
412
413                    Ok::<_, String>(())
414                });
415
416                threads.push(handle);
417            }
418
419            let mut errors = vec![];
420
421            for (i, thread) in threads.into_iter().enumerate() {
422                log::debug!("joining thread {i}...");
423
424                match thread.join() {
425                    Ok(x) => {
426                        if let Err(e) = x {
427                            errors.push(e);
428                        }
429                        log::debug!("thread {i} joined");
430                    }
431                    Err(e) => {
432                        log::error!("failed to join thread {i}: {e:?}");
433                    }
434                }
435            }
436
437            if !errors.is_empty() {
438                return Err(errors.join("\n").into());
439            }
440        }
441
442        Ok(Arc::try_unwrap(results)
443            .unwrap()
444            .into_inner()
445            .unwrap()
446            .into_values()
447            .collect())
448    }
449}
450
451struct Simulation<'a, B: SimBootstrap> {
452    #[cfg(feature = "tui")]
453    display_state: tui::DisplayState,
454    bootstrap: &'a B,
455}
456
457impl<'a, B: SimBootstrap> Simulation<'a, B> {
458    const fn new(
459        bootstrap: &'a B,
460        #[cfg(feature = "tui")] display_state: tui::DisplayState,
461    ) -> Self {
462        Self {
463            #[cfg(feature = "tui")]
464            display_state,
465            bootstrap,
466        }
467    }
468
469    #[allow(clippy::too_many_lines)]
470    fn run(&self, run_number: u64, thread_id: Option<u64>) -> SimResult {
471        if run_number > 1 {
472            switchy::random::simulator::reset_seed();
473        }
474
475        switchy::random::simulator::reset_rng();
476        switchy::tcp::simulator::reset();
477        #[cfg(feature = "fs")]
478        switchy::fs::simulator::reset_fs();
479        #[cfg(feature = "time")]
480        switchy::time::simulator::reset_epoch_offset();
481        #[cfg(feature = "time")]
482        switchy::time::simulator::reset_step_multiplier();
483        reset_simulator_cancellation_token();
484        reset_step();
485
486        self.bootstrap.init();
487
488        let config = self.bootstrap.build_sim(SimConfig::from_rng());
489        let duration = config.duration;
490        let duration_steps = duration.as_millis();
491
492        let mut managed_sim = ManagedSim::new(config);
493
494        let props = SimProperties {
495            run_number,
496            thread_id,
497            config,
498            extra: self.bootstrap.props(),
499        };
500
501        logging::log_message(format!(
502            "\n\
503            =========================== START ============================\n\
504            Server simulator starting\n{}\n\
505            ==============================================================\n",
506            run_info(&props)
507        ));
508
509        let start = switchy::time::now();
510
511        #[cfg(feature = "tui")]
512        self.display_state
513            .update_sim_state(thread_id.unwrap_or(1), run_number, config, 0.0, false);
514
515        self.bootstrap.on_start(&mut managed_sim);
516
517        let resp = std::panic::catch_unwind(AssertUnwindSafe(|| {
518            let print_step = |sim: &ManagedSim, step| {
519                if duration < Duration::MAX {
520                    #[allow(clippy::cast_precision_loss)]
521                    let progress = (step as f64 / duration_steps as f64).clamp(0.0, 1.0);
522
523                    #[cfg(feature = "tui")]
524                    self.display_state.update_sim_state(
525                        thread_id.unwrap_or(1),
526                        run_number,
527                        config,
528                        progress,
529                        false,
530                    );
531
532                    log::info!(
533                        "step {step} ({}) ({:.1}%)",
534                        sim.elapsed().as_millis().into_formatted(),
535                        progress * 100.0,
536                    );
537                } else {
538                    log::info!(
539                        "step {step} ({})",
540                        sim.elapsed().as_millis().into_formatted()
541                    );
542                }
543            };
544
545            managed_sim.start();
546
547            loop {
548                if !is_simulator_cancelled() {
549                    let step = next_step();
550
551                    if duration < Duration::MAX && u128::from(step) >= duration_steps {
552                        log::debug!("sim ran for {duration_steps} steps. stopping");
553                        print_step(&managed_sim, step);
554                        cancel_simulation();
555                        break;
556                    }
557
558                    if step.is_multiple_of(1000) {
559                        print_step(&managed_sim, step);
560                    }
561
562                    self.bootstrap.on_step(&mut managed_sim);
563
564                    #[cfg(feature = "tui")]
565                    self.display_state
566                        .update_sim_step(thread_id.unwrap_or(1), step);
567                }
568
569                if managed_sim.step()? {
570                    log::debug!("sim completed");
571                    break;
572                }
573            }
574
575            Ok::<_, Error>(())
576        }));
577
578        self.bootstrap.on_end(&mut managed_sim);
579
580        let end = switchy::time::now();
581        let real_time_millis = end.duration_since(start).unwrap().as_millis();
582        let sim_time_millis = managed_sim.elapsed().as_millis();
583        let steps = current_step() - 1;
584
585        #[cfg(feature = "tui")]
586        self.display_state.run_completed();
587
588        log::debug!("after simulation run");
589
590        let run = SimRunProperties {
591            steps,
592            real_time_millis,
593            sim_time_millis,
594        };
595
596        managed_sim.shutdown();
597
598        let panic = PANIC.with_borrow(Clone::clone);
599
600        let result = if let Err(e) = resp {
601            SimResult::Fail {
602                props,
603                run,
604                error: if panic.is_none() {
605                    Some(format!("{e:?}"))
606                } else {
607                    None
608                },
609                panic,
610            }
611        } else if let Ok(Err(e)) = resp {
612            SimResult::Fail {
613                props,
614                run,
615                error: Some(e.to_string()),
616                panic,
617            }
618        } else if let Some(panic) = panic {
619            SimResult::Fail {
620                props,
621                run,
622                error: None,
623                panic: Some(panic),
624            }
625        } else {
626            SimResult::Success { props, run }
627        };
628
629        if !result.is_success() {
630            end_sim();
631        }
632
633        #[cfg(feature = "tui")]
634        self.display_state
635            .update_sim_step(thread_id.unwrap_or(1), steps);
636        #[cfg(feature = "tui")]
637        self.display_state.update_sim_state(
638            thread_id.unwrap_or(1),
639            run_number,
640            config,
641            #[allow(clippy::cast_precision_loss)]
642            if duration < Duration::MAX {
643                (current_step() as f64 / duration_steps as f64).clamp(0.0, 1.0)
644            } else {
645                0.0
646            },
647            !result.is_success(),
648        );
649
650        logging::log_message(result.to_string());
651
652        result
653    }
654}
655
656/// Trait for bootstrapping and configuring simulations.
657///
658/// Implement this trait to customize simulation behavior at various lifecycle
659/// points. All methods have default implementations that do nothing.
660pub trait SimBootstrap: Send + Sync + 'static {
661    /// Returns custom properties to include in simulation output.
662    #[must_use]
663    fn props(&self) -> Vec<(String, String)> {
664        vec![]
665    }
666
667    /// Modifies the simulation configuration before the simulation starts.
668    #[must_use]
669    fn build_sim(&self, config: SimConfig) -> SimConfig {
670        config
671    }
672
673    /// Called once before any simulation runs begin.
674    fn init(&self) {}
675
676    /// Called when a simulation run starts.
677    fn on_start(&self, #[allow(unused)] sim: &mut impl Sim) {}
678
679    /// Called on each simulation step.
680    fn on_step(&self, #[allow(unused)] sim: &mut impl Sim) {}
681
682    /// Called when a simulation run ends.
683    fn on_end(&self, #[allow(unused)] sim: &mut impl Sim) {}
684}
685
686/// Interface for managing simulation actors (hosts and clients).
687pub trait Sim {
688    /// Simulates a host restart by name.
689    fn bounce(&mut self, host: impl Into<String>);
690
691    /// Spawns a host actor with the given name and action.
692    ///
693    /// The action is a factory function that returns a future representing
694    /// the host's behavior.
695    fn host<F: Fn() -> Fut + 'static, Fut: Future<Output = HostResult> + 'static>(
696        &mut self,
697        name: impl Into<String>,
698        action: F,
699    );
700
701    /// Spawns a client actor with the given name and action.
702    ///
703    /// The action is a future representing the client's behavior.
704    fn client(
705        &mut self,
706        name: impl Into<String>,
707        action: impl Future<Output = ClientResult> + 'static,
708    );
709}
710
711struct ManagedSim {
712    config: SimConfig,
713    hosts: Vec<Host>,
714    clients: Vec<Client>,
715    start: Option<SystemTime>,
716}
717
718impl ManagedSim {
719    const fn new(config: SimConfig) -> Self {
720        Self {
721            config,
722            hosts: vec![],
723            clients: vec![],
724            start: None,
725        }
726    }
727
728    pub fn elapsed(&self) -> Duration {
729        let Some(start) = self.start else {
730            return Duration::ZERO;
731        };
732        switchy::time::now().duration_since(start).unwrap()
733    }
734
735    pub fn start(&mut self) {
736        self.start = Some(switchy::time::now());
737
738        for host in self.hosts.iter_mut().filter(|x| !x.has_started()) {
739            host.start();
740        }
741        for client in &mut self.clients {
742            client.start();
743        }
744    }
745
746    pub fn step(&mut self) -> Result<bool, Error> {
747        log::trace!("step {}", current_step());
748        // if current_step() == 300 {
749        //     panic!();
750        // }
751
752        let mut actors = self
753            .hosts
754            .iter()
755            .map(|x| Box::new(x) as Box<dyn Actor>)
756            .chain(self.clients.iter().map(|x| Box::new(x) as Box<dyn Actor>))
757            .collect::<Vec<_>>();
758
759        if self.config.enable_random_order {
760            actors.shuffle(&mut rng());
761        }
762
763        for actor in actors {
764            actor.tick();
765        }
766
767        let mut remaining_hosts = vec![];
768
769        for mut host in self.hosts.drain(..) {
770            if host.is_running() {
771                remaining_hosts.push(host);
772                continue;
773            }
774            if let Some(handle) = host.handle {
775                host.runtime
776                    .block_on(handle)?
777                    .transpose()
778                    .map_err(Error::Step)?;
779            }
780        }
781
782        self.hosts = remaining_hosts;
783
784        let mut remaining_clients = vec![];
785
786        for mut client in self.clients.drain(..) {
787            if client.is_running() {
788                remaining_clients.push(client);
789                continue;
790            }
791            if let Some(handle) = client.handle {
792                client
793                    .runtime
794                    .block_on(handle)?
795                    .transpose()
796                    .map_err(Error::Step)?;
797            }
798        }
799
800        self.clients = remaining_clients;
801
802        if is_simulator_cancelled() {
803            log::debug!("cancelled!");
804            let client_count = self.clients.len();
805            for (i, client) in self.clients.drain(..).enumerate() {
806                log::debug!("cancelling client {}/{client_count}!", i + 1);
807                if let Some(handle) = client.handle {
808                    client
809                        .runtime
810                        .block_on(handle)?
811                        .transpose()
812                        .map_err(Error::Step)?;
813                }
814            }
815
816            let host_count = self.hosts.len();
817            for (i, host) in self.hosts.drain(..).enumerate() {
818                log::debug!("cancelling host {}/{host_count}!", i + 1);
819                if let Some(handle) = host.handle {
820                    host.runtime
821                        .block_on(handle)?
822                        .transpose()
823                        .map_err(Error::Step)?;
824                }
825            }
826        }
827
828        if current_step().is_multiple_of(1000) || END_SIM.load(std::sync::atomic::Ordering::SeqCst)
829        {
830            log::debug!("hosts={} clients={}", self.hosts.len(), self.clients.len());
831        }
832
833        Ok(self.hosts.is_empty() && self.clients.is_empty())
834    }
835
836    #[allow(clippy::unused_self)]
837    fn shutdown(self) {
838        cancel_simulation();
839    }
840}
841
842impl Sim for ManagedSim {
843    fn bounce(&mut self, host: impl Into<String>) {
844        let host = host.into();
845        log::debug!("bouncing host={host}");
846    }
847
848    fn host<F: Fn() -> Fut + 'static, Fut: Future<Output = HostResult> + 'static>(
849        &mut self,
850        name: impl Into<String>,
851        action: F,
852    ) {
853        let name = name.into();
854        log::debug!("starting host with name={name}");
855        self.hosts.push(Host::new(name, action));
856    }
857
858    fn client(
859        &mut self,
860        name: impl Into<String>,
861        action: impl Future<Output = ClientResult> + 'static,
862    ) {
863        let name = name.into();
864        log::debug!("starting client with name={name}");
865        self.clients.push(Client::new(name, action));
866    }
867}
868
869pub(crate) trait Actor {
870    fn tick(&self);
871}