1#![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
95pub mod client;
101
102mod config;
103
104pub mod formatting;
111
112pub mod host;
118
119mod logging;
120pub 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#[derive(Debug, thiserror::Error)]
149pub enum Error {
150 #[error(transparent)]
152 IO(#[from] std::io::Error),
153 #[error(transparent)]
155 Step(Box<dyn std::error::Error + Send>),
156 #[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
170pub 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#[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 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
656pub trait SimBootstrap: Send + Sync + 'static {
661 #[must_use]
663 fn props(&self) -> Vec<(String, String)> {
664 vec![]
665 }
666
667 #[must_use]
669 fn build_sim(&self, config: SimConfig) -> SimConfig {
670 config
671 }
672
673 fn init(&self) {}
675
676 fn on_start(&self, #[allow(unused)] sim: &mut impl Sim) {}
678
679 fn on_step(&self, #[allow(unused)] sim: &mut impl Sim) {}
681
682 fn on_end(&self, #[allow(unused)] sim: &mut impl Sim) {}
684}
685
686pub trait Sim {
688 fn bounce(&mut self, host: impl Into<String>);
690
691 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 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 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}