Skip to content

Commit cefd0c4

Browse files
authored
Merge pull request #309 from carlaKC/des-tokio
Use time-paused tokio for zero sleeps
2 parents 7a93f77 + 6d9a610 commit cefd0c4

9 files changed

Lines changed: 276 additions & 184 deletions

File tree

sim-cli/Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ triggered = "0.1.2"
1818
serde = "1.0.183"
1919
serde_json = "1.0.104"
2020
simple_logger = "4.2.0"
21-
simln-lib = { path = "../simln-lib" }
21+
# The virtual-time feature is required for the --virtual-time flag, which runs simulations on a paused runtime.
22+
simln-lib = { path = "../simln-lib", features = ["virtual-time"] }
2223
tokio = { version = "1.26.0", features = ["full"] }
2324
bitcoin = { version = "0.30.1" }
2425
ctrlc = "3.4.0"

sim-cli/src/main.rs

Lines changed: 49 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,27 @@
11
use std::sync::Arc;
2+
use std::time::SystemTime;
23

34
use clap::Parser;
45
use log::LevelFilter;
56
use sim_cli::parsing::{create_simulation, create_simulation_with_network, parse_sim_params, Cli};
7+
use simln_lib::latency_interceptor::LatencyIntercepor;
8+
use simln_lib::sim_node::Interceptor;
69
use simln_lib::{
7-
clock::SimulationClock,
8-
latency_interceptor::LatencyIntercepor,
9-
sim_node::{CustomRecords, Interceptor},
10-
SimulationCfg,
10+
clock::SimulationClock, runtime::block_on_virtual_time, sim_node::CustomRecords,
11+
ActivityDefinition, Simulation, SimulationCfg,
1112
};
1213
use simple_logger::SimpleLogger;
1314
use tokio_util::task::TaskTracker;
1415

15-
#[tokio::main]
16-
async fn main() -> anyhow::Result<()> {
16+
fn main() -> anyhow::Result<()> {
1717
// Enable tracing if building in developer mode.
1818
#[cfg(feature = "dev")]
1919
{
2020
console_subscriber::init();
2121
}
2222

2323
let cli = Cli::parse();
24-
let sim_params = parse_sim_params(&cli).await?;
24+
let sim_params = parse_sim_params(&cli)?;
2525

2626
SimpleLogger::new()
2727
.with_level(LevelFilter::Warn)
@@ -32,35 +32,52 @@ async fn main() -> anyhow::Result<()> {
3232

3333
cli.validate(&sim_params)?;
3434

35-
let tasks = TaskTracker::new();
36-
37-
let (sim, validated_activities) = if sim_params.sim_network.is_empty() {
38-
create_simulation(&cli, &sim_params, tasks.clone()).await?
39-
} else {
40-
let latency = cli.latency_ms.unwrap_or(0);
41-
let interceptors = if latency > 0 {
42-
vec![Arc::new(LatencyIntercepor::new_poisson(
43-
latency as f32,
44-
cli.fix_seed,
45-
)?) as Arc<dyn Interceptor>]
35+
let cfg = SimulationCfg::try_from(&cli)?;
36+
let latency = cli.latency_ms.unwrap_or(0);
37+
let build_and_run = move |clock: Arc<SimulationClock>| async move {
38+
let (sim, activities) = if sim_params.sim_network.is_empty() {
39+
create_simulation(cfg, &sim_params, clock, TaskTracker::new()).await?
4640
} else {
47-
vec![]
41+
let (sim, activities, _) = create_simulation_with_network(
42+
cfg,
43+
&sim_params,
44+
clock,
45+
TaskTracker::new(),
46+
// Create an interceptor to add latency to payments, otherwise none.
47+
if latency > 0 {
48+
vec![Arc::new(LatencyIntercepor::new_poisson(
49+
latency as f32,
50+
cli.fix_seed,
51+
)?) as Arc<dyn Interceptor>]
52+
} else {
53+
vec![]
54+
},
55+
CustomRecords::default(),
56+
)
57+
.await?;
58+
(sim, activities)
4859
};
49-
let sim_cfg: SimulationCfg = SimulationCfg::try_from(&cli)?;
50-
let clock = Arc::new(SimulationClock::new(cli.speedup_clock.unwrap_or(1))?);
51-
let (sim, validated_activities, _) = create_simulation_with_network(
52-
sim_cfg,
53-
&sim_params,
54-
clock,
55-
tasks.clone(),
56-
interceptors,
57-
CustomRecords::default(),
58-
)
59-
.await?;
60-
(sim, validated_activities)
60+
61+
run_simulation(sim, activities).await
6162
};
62-
let sim2 = sim.clone();
6363

64+
// For virtual time, our helper will build the clock we need and pass it to build_and_run. If
65+
// running with regular time, we can just pass a clock right in.
66+
if cli.virtual_time {
67+
block_on_virtual_time(SystemTime::now(), build_and_run)?
68+
} else {
69+
tokio::runtime::Runtime::new()?.block_on(build_and_run(Arc::new(SimulationClock::new(
70+
SystemTime::now(),
71+
))))
72+
}
73+
}
74+
75+
/// Drives a fully-configured simulation to completion, wiring up a ctrl-c handler that triggers a clean shutdown.
76+
async fn run_simulation(
77+
sim: Simulation<SimulationClock>,
78+
validated_activities: Vec<ActivityDefinition>,
79+
) -> anyhow::Result<()> {
80+
let sim2 = sim.clone();
6481
ctrlc::set_handler(move || {
6582
log::info!("Shutting down simulation.");
6683
sim2.shutdown();

sim-cli/src/parsing.rs

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -84,14 +84,15 @@ pub struct Cli {
8484
/// Seed to run random activity generator deterministically
8585
#[clap(long, short)]
8686
pub fix_seed: Option<u64>,
87-
/// A multiplier to wall time to speed up the simulation's clock. Only available when when running on a network of
88-
/// simulated nodes.
89-
#[clap(long)]
90-
pub speedup_clock: Option<u16>,
9187
/// Latency to optionally introduce for payments in a simulated network expressed in
9288
/// milliseconds.
9389
#[clap(long)]
9490
pub latency_ms: Option<u32>,
91+
/// Run the simulated network on virtual time: time advances instantly to the next event instead of sleeping on the
92+
/// wall clock, so the run finishes as fast as the CPU allows and is reproducible for a given seed. Only available on
93+
/// a simulated network, and requires --total-time to bound the run.
94+
#[clap(long, default_value_t = false)]
95+
pub virtual_time: bool,
9596
}
9697

9798
impl Cli {
@@ -111,18 +112,25 @@ impl Cli {
111112
nodes or sim_graph to run with simulated nodes"
112113
));
113114
}
114-
if !sim_params.nodes.is_empty() && self.speedup_clock.is_some() {
115-
return Err(anyhow!(
116-
"Clock speedup is only allowed when running on a simulated network"
117-
));
118-
}
119-
120115
if !sim_params.nodes.is_empty() && self.latency_ms.is_some() {
121116
return Err(anyhow!(
122117
"Latency for payments is only allowed when running on a simulated network"
123118
));
124119
}
125120

121+
if self.virtual_time {
122+
if !sim_params.nodes.is_empty() {
123+
return Err(anyhow!(
124+
"Virtual time is only allowed when running on a simulated network; real nodes run on wall time"
125+
));
126+
}
127+
if self.total_time.is_none() {
128+
return Err(anyhow!(
129+
"Virtual time requires --total-time, otherwise it advances forever"
130+
));
131+
}
132+
}
133+
126134
if !sim_params.exclude.is_empty() {
127135
if sim_params.sim_network.is_empty() {
128136
return Err(anyhow!(
@@ -339,14 +347,15 @@ pub async fn create_simulation_with_network(
339347
))
340348
}
341349

342-
/// Parses the cli options provided and creates a simulation to be run, connecting to lightning nodes and validating
343-
/// any activity described in the simulation file.
350+
/// Creates a simulation to be run against a set of real lightning nodes, connecting to the nodes described in
351+
/// `sim_params` and validating any activity described in the simulation file. The simulation is driven by `clock`,
352+
/// which must be constructed on the runtime that will run the simulation.
344353
pub async fn create_simulation(
345-
cli: &Cli,
354+
cfg: SimulationCfg,
346355
sim_params: &SimParams,
356+
clock: Arc<SimulationClock>,
347357
tasks: TaskTracker,
348358
) -> Result<(Simulation<SimulationClock>, Vec<ActivityDefinition>), anyhow::Error> {
349-
let cfg: SimulationCfg = SimulationCfg::try_from(cli)?;
350359
let SimParams {
351360
nodes,
352361
sim_network: _sim_network,
@@ -365,9 +374,7 @@ pub async fn create_simulation(
365374
cfg,
366375
clients,
367376
tasks,
368-
// When running on a real network, the underlying node may use wall time so we always use a clock with no
369-
// speedup.
370-
Arc::new(SimulationClock::new(1)?),
377+
clock,
371378
shutdown_trigger,
372379
shutdown_listener,
373380
),
@@ -531,7 +538,7 @@ async fn validate_activities(
531538
Ok(validated_activities)
532539
}
533540

534-
async fn read_sim_path(data_dir: PathBuf, sim_file: PathBuf) -> anyhow::Result<PathBuf> {
541+
fn read_sim_path(data_dir: PathBuf, sim_file: PathBuf) -> anyhow::Result<PathBuf> {
535542
if sim_file.exists() {
536543
Ok(sim_file)
537544
} else if sim_file.is_relative() {
@@ -540,15 +547,15 @@ async fn read_sim_path(data_dir: PathBuf, sim_file: PathBuf) -> anyhow::Result<P
540547
Ok(sim_path)
541548
} else {
542549
log::info!("Simulation file '{}' does not exist.", sim_path.display());
543-
select_sim_file(data_dir).await
550+
select_sim_file(data_dir)
544551
}
545552
} else {
546553
log::info!("Simulation file '{}' does not exist.", sim_file.display());
547-
select_sim_file(data_dir).await
554+
select_sim_file(data_dir)
548555
}
549556
}
550557

551-
pub async fn select_sim_file(data_dir: PathBuf) -> anyhow::Result<PathBuf> {
558+
pub fn select_sim_file(data_dir: PathBuf) -> anyhow::Result<PathBuf> {
552559
let sim_files = std::fs::read_dir(data_dir.clone())?
553560
.filter_map(|f| {
554561
f.ok().and_then(|f| {
@@ -585,8 +592,8 @@ fn mkdir(dir: PathBuf) -> anyhow::Result<PathBuf> {
585592
Ok(dir)
586593
}
587594

588-
pub async fn parse_sim_params(cli: &Cli) -> anyhow::Result<SimParams> {
589-
let sim_path = read_sim_path(cli.data_dir.clone(), cli.sim_file.clone()).await?;
595+
pub fn parse_sim_params(cli: &Cli) -> anyhow::Result<SimParams> {
596+
let sim_path = read_sim_path(cli.data_dir.clone(), cli.sim_file.clone())?;
590597
let sim_params = serde_json::from_str(&std::fs::read_to_string(sim_path)?).map_err(|e| {
591598
anyhow!(
592599
"Could not deserialize node connection data or activity description from simulation file (line {}, col {}, err: {}).",

simln-lib/Cargo.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ thiserror = "1.0.45"
2424
log = "0.4.20"
2525
triggered = "0.1.2"
2626
mpsc = "0.2.0"
27-
tokio = "1.31.0"
27+
tokio = { version = "1.31.0", features = ["rt", "sync", "time", "macros", "fs", "io-util"] }
2828
rand = "0.8.5"
2929
hex = "0.4.3"
3030
csv = "1.2.2"
@@ -35,8 +35,12 @@ reqwest = { version = "0.12", features = ["json", "multipart"] }
3535
tokio-util = { version = "0.7.13", features = ["rt"] }
3636
ldk-server-client = { git = "https://github.com/lightningdevkit/ldk-server", rev = "8163f4fe139368613959bf4f10b19ee6a5b9b4ab" }
3737

38+
[features]
39+
virtual-time = ["tokio/test-util"]
40+
3841
[dev-dependencies]
3942
ntest = "0.9.0"
4043
mockall = "0.13.1"
4144
futures = "0.3.31"
4245
tempfile = "3"
46+
tokio = { version = "1.31.0", features = ["macros", "rt-multi-thread"] }

0 commit comments

Comments
 (0)