From 77c8a3cdb9a162b072c75df4479f5e84e9122c15 Mon Sep 17 00:00:00 2001 From: Enigbe Ochekliye Date: Thu, 1 Feb 2024 23:00:39 +0100 Subject: [PATCH 1/7] feat: load config from file, configures log_interval - loads config options from a file and overwrites with CLI args - adds configurable log interval -change configuration file type by doing the ffg: replacing conf.json with conf.ini, updating SimulationConfig::load(...) to read the new .ini file, implementing cli_overwrite!(...) macro to reduce LOC when overwriting config values with CLI arguments --- Cargo.lock | 328 +++++++++++++++++++++++++++++++++++++++++- conf.ini | 9 ++ sim-cli/Cargo.toml | 1 + sim-cli/src/config.rs | 241 +++++++++++++++++++++++++++++++ 4 files changed, 576 insertions(+), 3 deletions(-) create mode 100644 conf.ini create mode 100644 sim-cli/src/config.rs diff --git a/Cargo.lock b/Cargo.lock index 21aa1a41..723b6d26 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -240,6 +240,9 @@ name = "bitflags" version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "327762f6e5a765692301e5bb513e0d9fef63be86bbc14528052b1cd3e6f03e07" +dependencies = [ + "serde", +] [[package]] name = "blake2b_simd" @@ -252,6 +255,15 @@ dependencies = [ "constant_time_eq", ] +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + [[package]] name = "bumpalo" version = "3.14.0" @@ -344,6 +356,26 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "config" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7328b20597b53c2454f0b1919720c25c7339051c02b72b7e05409e00b14132be" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "lazy_static", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust", +] + [[package]] name = "console" version = "0.15.7" @@ -357,12 +389,41 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "const-random" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaf16c9c2c612020bcfd042e170f6e32de9b9d75adb5277cdbbd2e2c8c8299a" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.10", + "once_cell", + "tiny-keccak", +] + [[package]] name = "constant_time_eq" version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -379,6 +440,15 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" +[[package]] +name = "cpufeatures" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" +dependencies = [ + "libc", +] + [[package]] name = "crossbeam-queue" version = "0.3.8" @@ -398,6 +468,22 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + [[package]] name = "csv" version = "1.3.0" @@ -464,6 +550,16 @@ dependencies = [ "zeroize", ] +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + [[package]] name = "dirs" version = "1.0.5" @@ -475,6 +571,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + [[package]] name = "either" version = "1.9.0" @@ -641,6 +746,16 @@ dependencies = [ "slab", ] +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + [[package]] name = "getrandom" version = "0.1.16" @@ -694,6 +809,12 @@ version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +[[package]] +name = "hashbrown" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43a3c133739dddd0d2990f9a4bdf8eb4b21ef50e4851ca85ab661199821d510e" + [[package]] name = "hashbrown" version = "0.14.2" @@ -874,6 +995,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + [[package]] name = "lazy_static" version = "1.4.0" @@ -901,6 +1033,12 @@ dependencies = [ "bitcoin 0.29.2", ] +[[package]] +name = "linked-hash-map" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" + [[package]] name = "linux-raw-sys" version = "0.4.10" @@ -941,6 +1079,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -994,6 +1138,16 @@ dependencies = [ "libc", ] +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "ntest" version = "0.9.0" @@ -1077,6 +1231,16 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "ordered-multimap" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ed8acf08e98e744e5384c8bc63ceb0364e68a6854187221c18df61c4797690e" +dependencies = [ + "dlv-list", + "hashbrown 0.13.2", +] + [[package]] name = "parking_lot" version = "0.12.1" @@ -1100,12 +1264,63 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "pathdiff" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" + [[package]] name = "percent-encoding" version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" +[[package]] +name = "pest" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219c0dcc30b6a27553f9cc242972b67f75b60eb0db71f0b5462f38b058c41546" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22e1288dbd7786462961e69bfd4df7848c1e37e8b74303dbdab82c3a9cdd2809" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1381c29a877c6d34b8c176e734f35d7f7f5b3adaefe940cb4d1bb7af94678e2e" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.38", +] + +[[package]] +name = "pest_meta" +version = "2.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0934d6907f148c22a3acbda520c7eed243ad7487a30f51f6ce52b58b7077a8a" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "petgraph" version = "0.6.4" @@ -1187,7 +1402,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" dependencies = [ "once_cell", - "toml_edit", + "toml_edit 0.19.15", ] [[package]] @@ -1471,6 +1686,18 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.4", + "bitflags 2.4.1", + "serde", + "serde_derive", +] + [[package]] name = "rust-argon2" version = "0.8.3" @@ -1483,6 +1710,16 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "rust-ini" +version = "0.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e2a3bcec1f113553ef1c88aae6c020a369d03d55b58de9869a0908930385091" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -1696,6 +1933,26 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" +dependencies = [ + "serde", +] + +[[package]] +name = "sha2" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + [[package]] name = "shell-words" version = "1.1.0" @@ -1718,6 +1975,7 @@ dependencies = [ "anyhow", "bitcoin 0.30.1", "clap", + "config", "ctrlc", "dialoguer", "log", @@ -1921,6 +2179,15 @@ dependencies = [ "time-core", ] +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + [[package]] name = "tokio" version = "1.33.0" @@ -2007,11 +2274,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "toml" +version = "0.8.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a9aad4a3066010876e8dcf5a8a06e70a558751117a145c6ce2b82c2e2054290" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.22.4", +] + [[package]] name = "toml_datetime" -version = "0.6.3" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cda73e2f1397b1262d6dfdcef8aafae14d1de7748d66822d3bfeeb6d03e5e4b" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] [[package]] name = "toml_edit" @@ -2024,6 +2306,19 @@ dependencies = [ "winnow", ] +[[package]] +name = "toml_edit" +version = "0.22.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c9ffdf896f8daaabf9b66ba8e77ea1ed5ed0f72821b398aba62352e95062951" +dependencies = [ + "indexmap 2.0.2", + "serde", + "serde_spanned", + "toml_datetime", + "winnow", +] + [[package]] name = "tonic" version = "0.8.3" @@ -2200,12 +2495,30 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed" +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicode-ident" version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode-width" version = "0.1.11" @@ -2515,6 +2828,15 @@ dependencies = [ "memchr", ] +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zeroize" version = "1.6.0" diff --git a/conf.ini b/conf.ini new file mode 100644 index 00000000..7b86c8de --- /dev/null +++ b/conf.ini @@ -0,0 +1,9 @@ +data_dir = "." +sim_file = "sim.json" +total_time= +print_batch_size = 500 +log_level = "info" +expected_pmt_amt = 3800000 +capacity_multiplier = 2 +no_results = false +log_interval = 60 \ No newline at end of file diff --git a/sim-cli/Cargo.toml b/sim-cli/Cargo.toml index 67a6f41b..74921025 100644 --- a/sim-cli/Cargo.toml +++ b/sim-cli/Cargo.toml @@ -20,3 +20,4 @@ sim-lib = { path = "../sim-lib" } tokio = { version = "1.26.0", features = ["full"] } bitcoin = { version = "0.30.1" } ctrlc = "3.4.0" +config ={ version = "0.14.0", features = ["ini"]} diff --git a/sim-cli/src/config.rs b/sim-cli/src/config.rs new file mode 100644 index 00000000..85962e85 --- /dev/null +++ b/sim-cli/src/config.rs @@ -0,0 +1,241 @@ +use std::path::PathBuf; + +use config::{Config, File}; +use log::LevelFilter; +use serde::Deserialize; + +use crate::Cli; + +/// Overwrites the fields on a SimulationConfig if corresponding arguments are supplied via the command line +macro_rules! cli_overwrite { + ($config:expr, $cli:expr, $($field:ident),*) => { + match (&mut $config, $cli) { + (config_struct, cli_struct) => { + $( + if let Some(value) = cli_struct.$field { + log::info!( + "SimulatinConfig::{} {:?} overwritten by CLI argument {:?}", + stringify!($field), + config_struct.$field, + value + ); + config_struct.$field = value; + } + )* + } + } + + }; +} + +#[derive(Debug, Deserialize)] +pub struct SimulationConfig { + /// Path to a directory where simulation results are save + pub data_dir: PathBuf, + /// Path to simulation file + pub sim_file: PathBuf, + /// Duration for which the simulation should run + #[serde(deserialize_with = "serde_config::deserialize_total_time")] + pub total_time: Option, + /// Number of activity results to batch together before printing to csv file [min: 1] + pub print_batch_size: u32, + /// Level of verbosity of the messages displayed by the simulator. + /// Possible values: [off, error, warn, info, debug, trace] + #[serde(deserialize_with = "serde_config::deserialize_log_level")] + pub log_level: LevelFilter, + /// Expected payment amount for the random activity generator + pub expected_pmt_amt: u64, + /// Multiplier of the overall network capacity used by the random activity generator + pub capacity_multiplier: f64, + /// Do not create an output file containing the simulations results + pub no_results: bool, + /// Duration after which results are logged + pub log_interval: u64, +} + +/// Custom deserializers for SimulationConfig fields +mod serde_config { + use std::str::FromStr; + + use log::LevelFilter; + use serde::Deserialize; + + /// Custom deserialization function for `LevelFilter`. Required because `LevelFilter` does + /// not implement deserialize + pub fn deserialize_log_level<'de, D>(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let level_filter_str = String::deserialize(deserializer)?; + let level_filter = LevelFilter::from_str(&level_filter_str).map_err(|e| { + serde::de::Error::custom(format!("Failed to deserialize LevelFilter from &str: {e}")) + })?; + + Ok(level_filter) + } + + /// Custom deserialization function for total time. This method is required because + /// the `config` crate is unable to parse null values in `.ini` files to an `Option::None` + pub fn deserialize_total_time<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let total_time_str = String::deserialize(deserializer)?; + + if total_time_str.is_empty() { + return Ok(None); + } + + // Parse string value to u32 + let total_time = total_time_str + .parse::() + .map_err(|e| serde::de::Error::custom(format!("Failed to parse u32 from &str: {e}")))?; + + Ok(Some(total_time)) + } +} + +impl SimulationConfig { + /// The default simulation configuration file with default simulation values + pub const DEFAULT_SIM_CONFIG_FILE: &'static str = "conf.ini"; + + /// Loads the simulation configuration from a path and overwrites loaded values with + /// CLI arguments + pub fn load(path: &PathBuf, cli: Cli) -> anyhow::Result { + // 1. Load simulation config from specified path + let path_str = if let Some(path_str) = path.to_str() { + path_str + } else { + anyhow::bail!("Failed to convert path {path:?} to &str.") + }; + + let config = Config::builder() + .add_source(File::with_name(path_str)) + .build()?; + let mut sim_conf: SimulationConfig = config.try_deserialize()?; + + // 2. Overwrite config values with CLI arguments (if passed) + if let Some(total_time) = cli.total_time { + log::info!( + "SimulatinConfig::total_time {:?} overwritten by CLI argument {}", + sim_conf.total_time, + total_time + ); + sim_conf.total_time = Some(total_time) + }; + + cli_overwrite!( + sim_conf, + cli, + data_dir, + sim_file, + print_batch_size, + log_level, + expected_pmt_amt, + capacity_multiplier, + no_results + ); + + Ok(sim_conf) + } +} + +#[cfg(test)] +mod test { + use std::{path::PathBuf, str::FromStr}; + + use crate::Cli; + + use super::SimulationConfig; + + #[test] + fn overwrite_config_with_cli() { + let test_cli = Cli { + data_dir: Some( + PathBuf::from_str("data").expect("Failed to create test data directory"), + ), + sim_file: Some( + PathBuf::from_str("sim.json").expect("Failed to create test simulation file"), + ), + total_time: Some(60), + print_batch_size: Some(10), + log_level: Some(log::LevelFilter::Debug), + expected_pmt_amt: Some(500), + capacity_multiplier: Some(3.142), + no_results: Some(true), + }; + + let mut test_config = SimulationConfig { + data_dir: PathBuf::from_str("simulation.data") + .expect("Failed to create test config data directory"), + sim_file: PathBuf::from_str("simulation.sim.json") + .expect("Failed to create test config simulation file"), + total_time: None, + print_batch_size: 200, + log_level: log::LevelFilter::Trace, + expected_pmt_amt: 500, + capacity_multiplier: 5.5, + no_results: true, + log_interval: 10, + }; + + cli_overwrite!( + test_config, + test_cli.clone(), + data_dir, + sim_file, + print_batch_size, + log_level, + expected_pmt_amt, + capacity_multiplier, + no_results + ); + + assert_eq!(Some(test_config.data_dir), test_cli.data_dir); + assert_eq!(Some(test_config.sim_file), test_cli.sim_file); + assert_eq!( + Some(test_config.print_batch_size), + test_cli.print_batch_size + ); + assert_eq!(Some(test_config.log_level), test_cli.log_level); + assert_eq!( + Some(test_config.expected_pmt_amt), + test_cli.expected_pmt_amt + ); + assert_eq!( + Some(test_config.capacity_multiplier), + test_cli.capacity_multiplier + ); + assert_eq!(Some(test_config.no_results), test_cli.no_results); + } + + #[test] + fn replace_config_with_cli_args() { + let cli = Cli { + data_dir: Some( + PathBuf::from_str("data").expect("Failed to create test data directory"), + ), + sim_file: Some( + PathBuf::from_str("sim.json").expect("Failed to create test simulation file"), + ), + total_time: Some(60), + print_batch_size: Some(10), + log_level: Some(log::LevelFilter::Debug), + expected_pmt_amt: Some(500), + capacity_multiplier: Some(3.142), + no_results: Some(true), + }; + + let conf = SimulationConfig::load(&PathBuf::from("../conf.ini"), cli.clone()) + .expect("Failed to create simulation config"); + + assert_eq!(Some(conf.data_dir), cli.data_dir); + assert_eq!(Some(conf.sim_file), cli.sim_file); + assert_eq!(conf.total_time, cli.total_time); + assert_eq!(Some(conf.print_batch_size), cli.print_batch_size); + assert_eq!(Some(conf.log_level), cli.log_level); + assert_eq!(Some(conf.expected_pmt_amt), cli.expected_pmt_amt); + assert_eq!(Some(conf.capacity_multiplier), cli.capacity_multiplier); + assert_eq!(Some(conf.no_results), cli.no_results); + } +} From 5826a447b293104c19a23642ad93331dee2864ec Mon Sep 17 00:00:00 2001 From: Enigbe Ochekliye Date: Fri, 8 Mar 2024 03:56:10 +0100 Subject: [PATCH 2/7] feat: load config options from optional config file - Implements a Cli::from_config_file(...) method to load configuration options from either a default or user-provided (via command line argument) config file - Merges the Cli-s generated from the config file and the parsed command line arguments taking into account the following precedence (command line args > configuration options > default value) - deletes config.rs and the former implementation that loads configuration options into a Simulation- Config struct --- conf.ini | 15 +- sim-cli/src/config.rs | 241 ------------------------- sim-cli/src/main.rs | 405 ++++++++++++++++++++++++++++++++++++++++-- sim-lib/src/lib.rs | 15 ++ 4 files changed, 411 insertions(+), 265 deletions(-) delete mode 100644 sim-cli/src/config.rs diff --git a/conf.ini b/conf.ini index 7b86c8de..3699695c 100644 --- a/conf.ini +++ b/conf.ini @@ -1,9 +1,10 @@ +[simln.conf] data_dir = "." sim_file = "sim.json" -total_time= -print_batch_size = 500 -log_level = "info" -expected_pmt_amt = 3800000 -capacity_multiplier = 2 -no_results = false -log_interval = 60 \ No newline at end of file +total_time = 250 +print_batch_size = 200 +log_level = "trace" +expected_pmt_amt = 3500000 +capacity_multiplier = 4.7 +no_results = true +log_interval = 35 diff --git a/sim-cli/src/config.rs b/sim-cli/src/config.rs deleted file mode 100644 index 85962e85..00000000 --- a/sim-cli/src/config.rs +++ /dev/null @@ -1,241 +0,0 @@ -use std::path::PathBuf; - -use config::{Config, File}; -use log::LevelFilter; -use serde::Deserialize; - -use crate::Cli; - -/// Overwrites the fields on a SimulationConfig if corresponding arguments are supplied via the command line -macro_rules! cli_overwrite { - ($config:expr, $cli:expr, $($field:ident),*) => { - match (&mut $config, $cli) { - (config_struct, cli_struct) => { - $( - if let Some(value) = cli_struct.$field { - log::info!( - "SimulatinConfig::{} {:?} overwritten by CLI argument {:?}", - stringify!($field), - config_struct.$field, - value - ); - config_struct.$field = value; - } - )* - } - } - - }; -} - -#[derive(Debug, Deserialize)] -pub struct SimulationConfig { - /// Path to a directory where simulation results are save - pub data_dir: PathBuf, - /// Path to simulation file - pub sim_file: PathBuf, - /// Duration for which the simulation should run - #[serde(deserialize_with = "serde_config::deserialize_total_time")] - pub total_time: Option, - /// Number of activity results to batch together before printing to csv file [min: 1] - pub print_batch_size: u32, - /// Level of verbosity of the messages displayed by the simulator. - /// Possible values: [off, error, warn, info, debug, trace] - #[serde(deserialize_with = "serde_config::deserialize_log_level")] - pub log_level: LevelFilter, - /// Expected payment amount for the random activity generator - pub expected_pmt_amt: u64, - /// Multiplier of the overall network capacity used by the random activity generator - pub capacity_multiplier: f64, - /// Do not create an output file containing the simulations results - pub no_results: bool, - /// Duration after which results are logged - pub log_interval: u64, -} - -/// Custom deserializers for SimulationConfig fields -mod serde_config { - use std::str::FromStr; - - use log::LevelFilter; - use serde::Deserialize; - - /// Custom deserialization function for `LevelFilter`. Required because `LevelFilter` does - /// not implement deserialize - pub fn deserialize_log_level<'de, D>(deserializer: D) -> Result - where - D: serde::Deserializer<'de>, - { - let level_filter_str = String::deserialize(deserializer)?; - let level_filter = LevelFilter::from_str(&level_filter_str).map_err(|e| { - serde::de::Error::custom(format!("Failed to deserialize LevelFilter from &str: {e}")) - })?; - - Ok(level_filter) - } - - /// Custom deserialization function for total time. This method is required because - /// the `config` crate is unable to parse null values in `.ini` files to an `Option::None` - pub fn deserialize_total_time<'de, D>(deserializer: D) -> Result, D::Error> - where - D: serde::Deserializer<'de>, - { - let total_time_str = String::deserialize(deserializer)?; - - if total_time_str.is_empty() { - return Ok(None); - } - - // Parse string value to u32 - let total_time = total_time_str - .parse::() - .map_err(|e| serde::de::Error::custom(format!("Failed to parse u32 from &str: {e}")))?; - - Ok(Some(total_time)) - } -} - -impl SimulationConfig { - /// The default simulation configuration file with default simulation values - pub const DEFAULT_SIM_CONFIG_FILE: &'static str = "conf.ini"; - - /// Loads the simulation configuration from a path and overwrites loaded values with - /// CLI arguments - pub fn load(path: &PathBuf, cli: Cli) -> anyhow::Result { - // 1. Load simulation config from specified path - let path_str = if let Some(path_str) = path.to_str() { - path_str - } else { - anyhow::bail!("Failed to convert path {path:?} to &str.") - }; - - let config = Config::builder() - .add_source(File::with_name(path_str)) - .build()?; - let mut sim_conf: SimulationConfig = config.try_deserialize()?; - - // 2. Overwrite config values with CLI arguments (if passed) - if let Some(total_time) = cli.total_time { - log::info!( - "SimulatinConfig::total_time {:?} overwritten by CLI argument {}", - sim_conf.total_time, - total_time - ); - sim_conf.total_time = Some(total_time) - }; - - cli_overwrite!( - sim_conf, - cli, - data_dir, - sim_file, - print_batch_size, - log_level, - expected_pmt_amt, - capacity_multiplier, - no_results - ); - - Ok(sim_conf) - } -} - -#[cfg(test)] -mod test { - use std::{path::PathBuf, str::FromStr}; - - use crate::Cli; - - use super::SimulationConfig; - - #[test] - fn overwrite_config_with_cli() { - let test_cli = Cli { - data_dir: Some( - PathBuf::from_str("data").expect("Failed to create test data directory"), - ), - sim_file: Some( - PathBuf::from_str("sim.json").expect("Failed to create test simulation file"), - ), - total_time: Some(60), - print_batch_size: Some(10), - log_level: Some(log::LevelFilter::Debug), - expected_pmt_amt: Some(500), - capacity_multiplier: Some(3.142), - no_results: Some(true), - }; - - let mut test_config = SimulationConfig { - data_dir: PathBuf::from_str("simulation.data") - .expect("Failed to create test config data directory"), - sim_file: PathBuf::from_str("simulation.sim.json") - .expect("Failed to create test config simulation file"), - total_time: None, - print_batch_size: 200, - log_level: log::LevelFilter::Trace, - expected_pmt_amt: 500, - capacity_multiplier: 5.5, - no_results: true, - log_interval: 10, - }; - - cli_overwrite!( - test_config, - test_cli.clone(), - data_dir, - sim_file, - print_batch_size, - log_level, - expected_pmt_amt, - capacity_multiplier, - no_results - ); - - assert_eq!(Some(test_config.data_dir), test_cli.data_dir); - assert_eq!(Some(test_config.sim_file), test_cli.sim_file); - assert_eq!( - Some(test_config.print_batch_size), - test_cli.print_batch_size - ); - assert_eq!(Some(test_config.log_level), test_cli.log_level); - assert_eq!( - Some(test_config.expected_pmt_amt), - test_cli.expected_pmt_amt - ); - assert_eq!( - Some(test_config.capacity_multiplier), - test_cli.capacity_multiplier - ); - assert_eq!(Some(test_config.no_results), test_cli.no_results); - } - - #[test] - fn replace_config_with_cli_args() { - let cli = Cli { - data_dir: Some( - PathBuf::from_str("data").expect("Failed to create test data directory"), - ), - sim_file: Some( - PathBuf::from_str("sim.json").expect("Failed to create test simulation file"), - ), - total_time: Some(60), - print_batch_size: Some(10), - log_level: Some(log::LevelFilter::Debug), - expected_pmt_amt: Some(500), - capacity_multiplier: Some(3.142), - no_results: Some(true), - }; - - let conf = SimulationConfig::load(&PathBuf::from("../conf.ini"), cli.clone()) - .expect("Failed to create simulation config"); - - assert_eq!(Some(conf.data_dir), cli.data_dir); - assert_eq!(Some(conf.sim_file), cli.sim_file); - assert_eq!(conf.total_time, cli.total_time); - assert_eq!(Some(conf.print_batch_size), cli.print_batch_size); - assert_eq!(Some(conf.log_level), cli.log_level); - assert_eq!(Some(conf.expected_pmt_amt), cli.expected_pmt_amt); - assert_eq!(Some(conf.capacity_multiplier), cli.capacity_multiplier); - assert_eq!(Some(conf.no_results), cli.no_results); - } -} diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index 5d857c21..ed5286d8 100644 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -1,7 +1,9 @@ use bitcoin::secp256k1::PublicKey; -use std::collections::HashMap; +use config::{Config, File}; +use serde::Deserialize; use std::path::PathBuf; use std::sync::Arc; +use std::{collections::HashMap, str::FromStr}; use tokio::sync::Mutex; use anyhow::anyhow; @@ -10,7 +12,7 @@ use clap::Parser; use log::LevelFilter; use sim_lib::{ cln::ClnNode, lnd::LndNode, ActivityDefinition, LightningError, LightningNode, NodeConnection, - NodeId, SimParams, Simulation, WriteResults, + NodeId, SimParams, Simulation, SimulationConfig, WriteResults, }; use simple_logger::SimpleLogger; @@ -29,6 +31,21 @@ pub const ACTIVITY_MULTIPLIER: f64 = 2.0; /// Default batch size to flush result data to disk const DEFAULT_PRINT_BATCH_SIZE: u32 = 500; +/// Default configuration file +const DEFAULT_CONFIGURATION_FILE: &str = "conf.ini"; + +/// Default total time +const DEFAULT_TOTAL_TIME: Option = None; + +/// Default log level +const DEFAULT_LOG_LEVEL: &str = "info"; + +/// Default no results +const DEFAULT_NO_RESULTS: bool = false; + +/// Default log interval +const DEFAULT_LOG_INTERVAL: u32 = 60; + /// Deserializes a f64 as long as it is positive and greater than 0. fn deserialize_f64_greater_than_zero(x: String) -> Result { match x.parse::() { @@ -45,7 +62,42 @@ fn deserialize_f64_greater_than_zero(x: String) -> Result { } } -#[derive(Parser)] +/// Custom deserialization function for `LevelFilter`. Required because `LevelFilter` does +/// not implement deserialize +pub fn deserialize_log_level<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let level_filter_str = String::deserialize(deserializer)?; + + let level_filter = LevelFilter::from_str(&level_filter_str).map_err(|e| { + serde::de::Error::custom(format!("Failed to deserialize LevelFilter from &str: {e}")) + })?; + + Ok(level_filter) +} + +/// Custom deserialization function for total time. This method is required because +/// the `config` crate is unable to parse null values in `.ini` files to an `Option::None` +pub fn deserialize_total_time<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let total_time_str = String::deserialize(deserializer)?; + + if total_time_str.is_empty() { + return Ok(None); + } + + // Parse string value to u32 + let total_time = total_time_str + .parse::() + .map_err(|e| serde::de::Error::custom(format!("Failed to parse u32 from &str: {e}")))?; + + Ok(Some(total_time)) +} + +#[derive(Parser, Deserialize)] #[command(version, about)] struct Cli { /// Path to a directory containing simulation files, and where simulation results will be stored @@ -63,7 +115,8 @@ struct Cli { print_batch_size: u32, /// Level of verbosity of the messages displayed by the simulator. /// Possible values: [off, error, warn, info, debug, trace] - #[clap(long, short, verbatim_doc_comment, default_value = "info")] + #[clap(long, short, verbatim_doc_comment, default_value = DEFAULT_LOG_LEVEL)] + #[serde(deserialize_with = "deserialize_log_level")] log_level: LevelFilter, /// Expected payment amount for the random activity generator #[clap(long, short, default_value_t = EXPECTED_PAYMENT_AMOUNT, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u64::MAX))] @@ -72,22 +125,340 @@ struct Cli { #[clap(long, short, default_value_t = ACTIVITY_MULTIPLIER, value_parser = clap::builder::StringValueParser::new().try_map(deserialize_f64_greater_than_zero))] capacity_multiplier: f64, /// Do not create an output file containing the simulations results - #[clap(long, default_value_t = false)] + #[clap(long, default_value_t = DEFAULT_NO_RESULTS)] no_results: bool, + /// Sets a custom configuration (INI) file + #[clap(long, short = 'C', value_name = "CONFIG_FILE", default_value = DEFAULT_CONFIGURATION_FILE)] + config: PathBuf, + #[clap(long, short = 'L', default_value_t = DEFAULT_LOG_INTERVAL, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u32::MAX as u64))] + log_interval: u32, +} + +/// Implementation of Cli with default values +impl Default for Cli { + fn default() -> Self { + Cli { + data_dir: PathBuf::from(DEFAULT_DATA_DIR), + sim_file: PathBuf::from(DEFAULT_SIM_FILE), + total_time: DEFAULT_TOTAL_TIME, + print_batch_size: DEFAULT_PRINT_BATCH_SIZE, + log_level: LevelFilter::Info, + expected_pmt_amt: EXPECTED_PAYMENT_AMOUNT, + capacity_multiplier: ACTIVITY_MULTIPLIER, + no_results: DEFAULT_NO_RESULTS, + config: PathBuf::from(DEFAULT_CONFIGURATION_FILE), + log_interval: DEFAULT_LOG_INTERVAL, + } + } +} + +impl Cli { + /// Creates Cli from a configuration if provided, else it defaults to + /// CLI default values + fn from_config_file(file_path: PathBuf) -> anyhow::Result { + let default_cli = Cli::default(); + + let config = Config::builder() + .add_source(File::with_name( + file_path.as_os_str().to_string_lossy().as_ref(), + )) + .build()?; + + let simln_conf = config.get_table("simln.conf")?; + + let config_data_dir = + simln_conf + .get("data_dir") + .map_or(default_cli.data_dir.clone(), |val| { + let data_dir_res = val.clone().try_deserialize::(); + match data_dir_res { + Ok(data_dir) => { + log::info!("Configuration file arg: data_dir={:?}.", data_dir); + PathBuf::from(data_dir) + }, + Err(e) => { + log::error!( + "Failed to parse data_dir. Default value used. Error: {e:?}." + ); + default_cli.data_dir.clone() + }, + } + }); + + let config_sim_file = + simln_conf + .get("sim_file") + .map_or(default_cli.sim_file.clone(), |val| { + let sim_file_res = val.clone().try_deserialize::(); + match sim_file_res { + Ok(sim_file) => { + log::info!("Configuration file arg: sim_file={:?}.", sim_file); + PathBuf::from(sim_file) + }, + Err(e) => { + log::error!( + "Failed to parse sim_file. Default value used. Error: {e:?}.", + ); + default_cli.sim_file + }, + } + }); + + let config_total_time = + simln_conf + .get("total_time") + .map_or(default_cli.total_time, |val| { + let total_time_res = val.clone().try_deserialize::>(); + match total_time_res { + Ok(total_time) => { + log::info!("Configuration file arg: total_time={:?}.", total_time); + total_time + }, + Err(e) => { + log::error!( + "Failed to parse total_time. Default value used. Error: {e:?}." + ); + default_cli.total_time + }, + } + }); + + let config_print_batch_size = + simln_conf + .get("print_batch_size") + .map_or(default_cli.print_batch_size, |val| { + let print_batch_size_res = val.clone().try_deserialize::(); + match print_batch_size_res { + Ok(print_batch_size) => { + log::info!( + "Configuration file arg: print_batch_size={:?}.", + print_batch_size + ); + print_batch_size + }, + Err(e) => { + log::error!( + "Failed to parse print_batch_size. Default value used. Error: {e:?}." + ); + default_cli.print_batch_size + }, + } + }); + + let config_log_level = + simln_conf + .get("log_level") + .map_or(DEFAULT_LOG_LEVEL.to_string(), |val| { + let log_level_res = val.clone().try_deserialize::(); + match log_level_res { + Ok(log_level) => { + log::info!("Configuration file arg: log_level={:?}.", log_level); + log_level + }, + Err(e) => { + log::error!( + "Failed to parse log_level. Default value used. Error: {e:?}." + ); + DEFAULT_LOG_LEVEL.to_string() + }, + } + }); + + let config_expected_pmt_amt = + simln_conf + .get("expected_pmt_amt") + .map_or(default_cli.expected_pmt_amt, |val| { + let pmt_amt_res = val.clone().try_deserialize::(); + match pmt_amt_res { + Ok(pmt_amt) => { + log::info!("Configuration file arg: expected_pmt_amt={:?}.", pmt_amt); + pmt_amt + }, + Err(e) => { + log::error!( + "Failed to parse expected_pmt_amt. Default value used. Error: {e:?}." + ); + default_cli.expected_pmt_amt + }, + } + }); + + let config_capacity_multiplier = + simln_conf + .get("capacity_multiplier") + .map_or(default_cli.capacity_multiplier, |val| { + let capacity_multiplier_res = val.clone().try_deserialize::(); + match capacity_multiplier_res { + Ok(capacity_multiplier) => { + log::info!( + "Configuration file arg: capacity_multiplier={:?}.", + capacity_multiplier + ); + capacity_multiplier + }, + Err(e) => { + log::error!( + "Failed to parse capacity_multiplier. Default value used. Error: {e:?}." + ); + default_cli.capacity_multiplier + }, + } + }); + + let config_no_results = + simln_conf + .get("no_results") + .map_or(default_cli.no_results, |val| { + let no_results_res = val.clone().try_deserialize::(); + match no_results_res { + Ok(no_results) => { + log::info!("Configuration file arg: no_results={:?}.", no_results); + no_results + }, + Err(e) => { + log::error!( + "Failed to parse no_results. Default value used. Error: {e:?}." + ); + default_cli.no_results + }, + } + }); + + let config_log_interval = + simln_conf + .get("log_interval") + .map_or(default_cli.log_interval, |val| { + let log_interval_res = val.clone().try_deserialize::(); + match log_interval_res { + Ok(log_interval) => { + log::info!("Configuration file arg: log_interval={:?}.", log_interval); + log_interval + }, + Err(e) => { + log::error!( + "Failed to parse log_interval. Default value used. Error: {e:?}." + ); + default_cli.log_interval + }, + } + }); + + let config_cli = Cli { + data_dir: config_data_dir, + sim_file: config_sim_file, + total_time: config_total_time, + print_batch_size: config_print_batch_size, + log_level: LevelFilter::from_str(&config_log_level)?, + expected_pmt_amt: config_expected_pmt_amt, + capacity_multiplier: config_capacity_multiplier, + no_results: config_no_results, + config: default_cli.config, + log_interval: config_log_interval, + }; + + Ok(config_cli) + } + + /// Converts into simulation config + fn to_simulation_config(&self) -> SimulationConfig { + SimulationConfig { + log_level: self.log_level, + total_time: self.total_time, + expected_pmt_amt: self.expected_pmt_amt, + capacity_multiplier: self.capacity_multiplier, + no_results: self.no_results, + print_batch_size: self.print_batch_size, + data_dir: self.data_dir.to_path_buf(), + sim_file: self.sim_file.to_path_buf(), + log_interval: self.log_interval, + } + } +} + +/// Merge command line and configuration value `Cli`s +fn merge_cli() -> anyhow::Result { + let cli = Cli::parse(); + log::info!( + "Configuration file: {:?}.", + cli.config.canonicalize()?.display() + ); + + let mut cli_from_config = Cli::from_config_file(cli.config.to_path_buf())?; + + if cli.data_dir != PathBuf::from(DEFAULT_DATA_DIR) { + log::info!("Command line arg: data_dir={:?}.", cli.data_dir); + cli_from_config.data_dir = cli.data_dir + } + + if cli.sim_file != PathBuf::from(DEFAULT_SIM_FILE) { + log::info!("Command line arg: sim_file={:?}.", cli.sim_file); + cli_from_config.sim_file = cli.sim_file + } + + if cli.total_time != DEFAULT_TOTAL_TIME { + log::info!("Command line arg: total_time={:?}.", cli.total_time); + cli_from_config.total_time = cli.total_time + } + + if cli.print_batch_size != DEFAULT_PRINT_BATCH_SIZE { + log::info!( + "Command line arg: print_batch_size={:?}.", + cli.print_batch_size + ); + cli_from_config.print_batch_size = cli.print_batch_size + } + + if cli.log_level.as_str() != DEFAULT_LOG_LEVEL { + log::info!("Command line arg: log_level={:?}.", cli.log_level); + cli_from_config.log_level = cli.log_level + } + + if cli.expected_pmt_amt != EXPECTED_PAYMENT_AMOUNT { + log::info!( + "Command line arg: expected_pmt_amt={:?}.", + cli.expected_pmt_amt + ); + cli_from_config.expected_pmt_amt = cli.expected_pmt_amt + } + + if cli.capacity_multiplier != ACTIVITY_MULTIPLIER { + log::info!( + "Command line arg: capacity_multiplier={:?}.", + cli.capacity_multiplier + ); + cli_from_config.capacity_multiplier = cli.capacity_multiplier + } + + if cli.no_results != DEFAULT_NO_RESULTS { + log::info!("Command line arg: no_results={:?}.", cli.no_results); + cli_from_config.no_results = cli.no_results + } + + if cli.log_interval != DEFAULT_LOG_INTERVAL { + log::info!("Command line arg: log_interval={:?}.", cli.log_interval); + cli_from_config.log_interval = cli.log_interval; + } + + if cli.config != PathBuf::from(DEFAULT_CONFIGURATION_FILE) { + log::info!("Command line arg: config={:?}.", cli.config); + } + + anyhow::Ok(cli_from_config) } #[tokio::main] async fn main() -> anyhow::Result<()> { - let cli = Cli::parse(); + SimpleLogger::new().with_level(LevelFilter::Info).init()?; + let cli = merge_cli()?; + let opts = cli.to_simulation_config(); SimpleLogger::new() .with_level(LevelFilter::Warn) - .with_module_level("sim_lib", cli.log_level) - .with_module_level("sim_cli", cli.log_level) - .init() - .unwrap(); + .with_module_level("sim_lib", opts.log_level) + .with_module_level("sim_cli", opts.log_level) + .init()?; - let sim_path = read_sim_path(cli.data_dir.clone(), cli.sim_file).await?; + let sim_path = read_sim_path(opts.data_dir.clone(), opts.sim_file).await?; let SimParams { nodes, activity } = serde_json::from_str(&std::fs::read_to_string(sim_path)?) .map_err(|e| anyhow!("Could not deserialize node connection data or activity description from simulation file (line {}, col {}).", e.line(), e.column()))?; @@ -190,10 +561,10 @@ async fn main() -> anyhow::Result<()> { }); } - let write_results = if !cli.no_results { + let write_results = if !opts.no_results { Some(WriteResults { - results_dir: mkdir(cli.data_dir.join("results")).await?, - batch_size: cli.print_batch_size, + results_dir: mkdir(opts.data_dir.join("results")).await?, + batch_size: opts.print_batch_size, }) } else { None @@ -202,9 +573,9 @@ async fn main() -> anyhow::Result<()> { let sim = Simulation::new( clients, validated_activities, - cli.total_time, - cli.expected_pmt_amt, - cli.capacity_multiplier, + opts.total_time, + opts.expected_pmt_amt, + opts.capacity_multiplier, write_results, ); let sim2 = sim.clone(); diff --git a/sim-lib/src/lib.rs b/sim-lib/src/lib.rs index 59e50b7e..4042f3c1 100644 --- a/sim-lib/src/lib.rs +++ b/sim-lib/src/lib.rs @@ -4,6 +4,7 @@ use bitcoin::Network; use csv::WriterBuilder; use lightning::ln::features::NodeFeatures; use lightning::ln::PaymentHash; +use log::LevelFilter; use random_activity::RandomActivityError; use serde::{Deserialize, Serialize}; use std::collections::HashSet; @@ -1077,3 +1078,17 @@ async fn track_payment_result( log::trace!("Payment result tracker exiting."); } + +/// Configuration options for simulation +#[derive(Debug)] +pub struct SimulationConfig { + pub log_level: LevelFilter, + pub total_time: Option, + pub expected_pmt_amt: u64, + pub capacity_multiplier: f64, + pub no_results: bool, + pub print_batch_size: u32, + pub data_dir: PathBuf, + pub sim_file: PathBuf, + pub log_interval: u32, +} From c3d239fc4ad076fd63cf3f4c07fc9346b77d970c Mon Sep 17 00:00:00 2001 From: Enigbe Ochekliye Date: Fri, 8 Mar 2024 15:53:53 +0100 Subject: [PATCH 3/7] refactor: replace simple_logger with flexi_logger - The latter allows dynamic, programmatic updates to the log level at runtime. - The replace was necessary to capture and log command line arguments and configuration options which may or may not contain specification of the log levels after parsing --- Cargo.lock | 232 +++++++++++++++++++++++++++----------------- sim-cli/Cargo.toml | 2 +- sim-cli/src/main.rs | 19 ++-- 3 files changed, 156 insertions(+), 97 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 723b6d26..355d2253 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -38,6 +38,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anstyle" version = "1.0.4" @@ -291,6 +306,18 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf5903dcbc0a39312feb77df2ff4c76387d591b9fc7b04a238dcf8bb62639a" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "windows-targets 0.52.4", +] + [[package]] name = "clap" version = "4.4.6" @@ -345,17 +372,6 @@ dependencies = [ "tonic-build 0.8.4", ] -[[package]] -name = "colored" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" -dependencies = [ - "is-terminal", - "lazy_static", - "windows-sys 0.48.0", -] - [[package]] name = "config" version = "0.14.0" @@ -528,15 +544,6 @@ dependencies = [ "parking_lot_core", ] -[[package]] -name = "deranged" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f32d04922c60427da6f9fef14d042d9edddef64cb9d4ce0d64d0685fbeb1fd3" -dependencies = [ - "powerfmt", -] - [[package]] name = "dialoguer" version = "0.11.0" @@ -651,6 +658,22 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" +[[package]] +name = "flexi_logger" +version = "0.27.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469e584c031833564840fb0cdbce99bdfe946fd45480a188545e73a76f45461c" +dependencies = [ + "chrono", + "glob", + "is-terminal", + "lazy_static", + "log", + "nu-ansi-term", + "regex", + "thiserror", +] + [[package]] name = "fnv" version = "1.0.7" @@ -784,6 +807,12 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "h2" version = "0.3.21" @@ -940,6 +969,29 @@ dependencies = [ "tokio-io-timeout", ] +[[package]] +name = "iana-time-zone" +version = "0.1.60" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "indexmap" version = "1.9.3" @@ -1181,6 +1233,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "nu-ansi-term" +version = "0.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c073d3c1930d0751774acf49e66653acecb416c3a54c6ec095a9b11caddb5a68" +dependencies = [ + "windows-sys 0.48.0", +] + [[package]] name = "num-traits" version = "0.2.17" @@ -1201,15 +1262,6 @@ dependencies = [ "libc", ] -[[package]] -name = "num_threads" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" -dependencies = [ - "libc", -] - [[package]] name = "object" version = "0.32.1" @@ -1363,12 +1415,6 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - [[package]] name = "ppv-lite86" version = "0.2.17" @@ -1978,11 +2024,11 @@ dependencies = [ "config", "ctrlc", "dialoguer", + "flexi_logger", "log", "serde", "serde_json", "sim-lib", - "simple_logger", "tokio", ] @@ -2013,18 +2059,6 @@ dependencies = [ "triggered", ] -[[package]] -name = "simple_logger" -version = "4.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2230cd5c29b815c9b699fb610b49a5ed65588f3509d9f0108be3a885da629333" -dependencies = [ - "colored", - "log", - "time", - "windows-sys 0.42.0", -] - [[package]] name = "slab" version = "0.4.9" @@ -2148,37 +2182,6 @@ dependencies = [ "syn 2.0.38", ] -[[package]] -name = "time" -version = "0.3.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a34ab300f2dee6e562c10a046fc05e358b29f9bf92277f30c3c8d82275f6f5" -dependencies = [ - "deranged", - "itoa", - "libc", - "num_threads", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" - -[[package]] -name = "time-macros" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ad70d68dba9e1f8aceda7aa6711965dfec1cac869f311a51bd08b3a2ccbce20" -dependencies = [ - "time-core", -] - [[package]] name = "tiny-keccak" version = "2.0.2" @@ -2673,18 +2676,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.42.0" +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", + "windows-targets 0.52.4", ] [[package]] @@ -2735,6 +2732,21 @@ dependencies = [ "windows_x86_64_msvc 0.48.5", ] +[[package]] +name = "windows-targets" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] + [[package]] name = "windows_aarch64_gnullvm" version = "0.42.2" @@ -2747,6 +2759,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" + [[package]] name = "windows_aarch64_msvc" version = "0.42.2" @@ -2759,6 +2777,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" + [[package]] name = "windows_i686_gnu" version = "0.42.2" @@ -2771,6 +2795,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" +[[package]] +name = "windows_i686_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" + [[package]] name = "windows_i686_msvc" version = "0.42.2" @@ -2783,6 +2813,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" +[[package]] +name = "windows_i686_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" + [[package]] name = "windows_x86_64_gnu" version = "0.42.2" @@ -2795,6 +2831,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" + [[package]] name = "windows_x86_64_gnullvm" version = "0.42.2" @@ -2807,6 +2849,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" + [[package]] name = "windows_x86_64_msvc" version = "0.42.2" @@ -2819,6 +2867,12 @@ version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" + [[package]] name = "winnow" version = "0.5.17" diff --git a/sim-cli/Cargo.toml b/sim-cli/Cargo.toml index 74921025..7de2072b 100644 --- a/sim-cli/Cargo.toml +++ b/sim-cli/Cargo.toml @@ -15,9 +15,9 @@ dialoguer = "0.11.0" log = "0.4.20" serde = "1.0.183" serde_json = "1.0.104" -simple_logger = "4.2.0" sim-lib = { path = "../sim-lib" } tokio = { version = "1.26.0", features = ["full"] } bitcoin = { version = "0.30.1" } ctrlc = "3.4.0" config ={ version = "0.14.0", features = ["ini"]} +flexi_logger = { version = "0.27", features = ["colors"] } diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index ed5286d8..5eeaf5e8 100644 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -1,5 +1,6 @@ use bitcoin::secp256k1::PublicKey; use config::{Config, File}; +use flexi_logger::{LogSpecBuilder, Logger}; use serde::Deserialize; use std::path::PathBuf; use std::sync::Arc; @@ -14,7 +15,6 @@ use sim_lib::{ cln::ClnNode, lnd::LndNode, ActivityDefinition, LightningError, LightningNode, NodeConnection, NodeId, SimParams, Simulation, SimulationConfig, WriteResults, }; -use simple_logger::SimpleLogger; /// The default directory where the simulation files are stored and where the results will be written to. pub const DEFAULT_DATA_DIR: &str = "."; @@ -448,15 +448,20 @@ fn merge_cli() -> anyhow::Result { #[tokio::main] async fn main() -> anyhow::Result<()> { - SimpleLogger::new().with_level(LevelFilter::Info).init()?; + let logger_handle = Logger::try_with_str("info")? + .set_palette("b1;3;2;4;6".to_string()) + .start()?; + let cli = merge_cli()?; let opts = cli.to_simulation_config(); - SimpleLogger::new() - .with_level(LevelFilter::Warn) - .with_module_level("sim_lib", opts.log_level) - .with_module_level("sim_cli", opts.log_level) - .init()?; + logger_handle.set_new_spec( + LogSpecBuilder::new() + .default(LevelFilter::Warn) + .module("sim_lib", opts.log_level) + .module("sim_cli", opts.log_level) + .build(), + ); let sim_path = read_sim_path(opts.data_dir.clone(), opts.sim_file).await?; let SimParams { nodes, activity } = From 451c3a790443de7514550fd36137a666ce3661e5 Mon Sep 17 00:00:00 2001 From: Enigbe Ochekliye Date: Mon, 11 Mar 2024 10:41:17 +0100 Subject: [PATCH 4/7] refactor: move Cli-related functionality to cli.rs - additionally reduces the LOC by implementing the overwrite_field!(...) macro and from_config_field(...) generic function. - makes the requirement of a configuration file optional, defaulting to default Cli values if no config file is provided --- sim-cli/src/cli.rs | 310 ++++++++++++++++++++++++++++++ sim-cli/src/main.rs | 446 +------------------------------------------- 2 files changed, 319 insertions(+), 437 deletions(-) create mode 100644 sim-cli/src/cli.rs diff --git a/sim-cli/src/cli.rs b/sim-cli/src/cli.rs new file mode 100644 index 00000000..833e259b --- /dev/null +++ b/sim-cli/src/cli.rs @@ -0,0 +1,310 @@ +use std::{collections::HashMap, fmt::Debug, path::PathBuf, str::FromStr}; + +use clap::{builder::TypedValueParser, Parser}; +use config::{Config, File}; +use log::LevelFilter; +use serde::Deserialize; +use sim_lib::SimulationConfig; + +/// The default directory where the simulation files are stored and where the results will be written to. +pub const DEFAULT_DATA_DIR: &str = "."; + +/// The default simulation file to be used by the simulator. +pub const DEFAULT_SIM_FILE: &str = "sim.json"; + +/// The default expected payment amount for the simulation, around ~$10 at the time of writing. +pub const EXPECTED_PAYMENT_AMOUNT: u64 = 3_800_000; + +/// The number of times over each node in the network sends its total deployed capacity in a calendar month. +pub const ACTIVITY_MULTIPLIER: f64 = 2.0; + +/// Default batch size to flush result data to disk +pub const DEFAULT_PRINT_BATCH_SIZE: u32 = 500; + +/// Default configuration file +pub const DEFAULT_CONFIGURATION_FILE: &str = "conf.ini"; + +/// Default total time +pub const DEFAULT_TOTAL_TIME: Option = None; + +/// Default log level +pub const DEFAULT_LOG_LEVEL: &str = "info"; + +/// Default no results +pub const DEFAULT_NO_RESULTS: bool = false; + +/// Default log interval +pub const DEFAULT_LOG_INTERVAL: u32 = 60; + +/// Configuration header +pub const CONFIG_HEADER: &str = "simln.conf"; + +/// Workspace root directory +pub const WORKSPACE_ROOT_DIR: &str = "."; + +/// Deserializes a f64 as long as it is positive and greater than 0. +fn deserialize_f64_greater_than_zero(x: String) -> Result { + match x.parse::() { + Ok(x) => { + if x > 0.0 { + Ok(x) + } else { + Err(format!("capacity_multiplier must be higher than 0. {x} received.")) + } + }, + Err(e) => Err(e.to_string()), + } +} + +/// Custom deserialization function for `LevelFilter`. Required because `LevelFilter` does +/// not implement deserialize +pub fn deserialize_log_level<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let level_filter_str = String::deserialize(deserializer)?; + + let level_filter = LevelFilter::from_str(&level_filter_str) + .map_err(|e| serde::de::Error::custom(format!("Failed to deserialize LevelFilter from &str: {e}")))?; + + Ok(level_filter) +} + +/// Custom deserialization function for total time. This method is required because +/// the `config` crate is unable to parse null values in `.ini` files to an `Option::None` +pub fn deserialize_total_time<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let total_time_str = String::deserialize(deserializer)?; + + if total_time_str.is_empty() { + return Ok(None); + } + + // Parse string value to u32 + let total_time = total_time_str + .parse::() + .map_err(|e| serde::de::Error::custom(format!("Failed to parse u32 from &str: {e}")))?; + + Ok(Some(total_time)) +} + +#[derive(Parser, Deserialize)] +#[command(version, about)] +pub struct Cli { + /// Path to a directory containing simulation files, and where simulation results will be stored + #[clap(long, short, default_value = DEFAULT_DATA_DIR)] + data_dir: PathBuf, + /// Path to the simulation file to be used by the simulator + /// This can either be an absolute path, or relative path with respect to data_dir + #[clap(long, short, default_value = DEFAULT_SIM_FILE)] + sim_file: PathBuf, + /// Total time the simulator will be running + #[clap(long, short)] + #[serde(deserialize_with = "deserialize_total_time")] + total_time: Option, + /// Number of activity results to batch together before printing to csv file [min: 1] + #[clap( + long, + short, + default_value_t = DEFAULT_PRINT_BATCH_SIZE, + value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u32::MAX as u64) + )] + print_batch_size: u32, + /// Level of verbosity of the messages displayed by the simulator. + /// Possible values: [off, error, warn, info, debug, trace] + #[clap(long, short, verbatim_doc_comment, default_value = DEFAULT_LOG_LEVEL)] + #[serde(deserialize_with = "deserialize_log_level")] + log_level: LevelFilter, + /// Expected payment amount for the random activity generator + #[clap( + long, + short, + default_value_t = EXPECTED_PAYMENT_AMOUNT, + value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u64::MAX) + )] + expected_pmt_amt: u64, + /// Multiplier of the overall network capacity used by the random activity generator + #[clap( + long, + short, + default_value_t = ACTIVITY_MULTIPLIER, + value_parser = clap::builder::StringValueParser::new().try_map(deserialize_f64_greater_than_zero) + )] + capacity_multiplier: f64, + /// Do not create an output file containing the simulations results + #[clap(long, default_value_t = DEFAULT_NO_RESULTS)] + no_results: bool, + /// Sets a custom configuration (INI) file + #[clap(long, short = 'C', value_name = "CONFIG_FILE", default_value = DEFAULT_CONFIGURATION_FILE)] + config: PathBuf, + #[clap( + long, + short = 'L', + default_value_t = DEFAULT_LOG_INTERVAL, + value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u32::MAX as u64) + )] + log_interval: u32, +} + +/// Implementation of Cli with default values +impl Default for Cli { + fn default() -> Self { + Cli { + data_dir: PathBuf::from(DEFAULT_DATA_DIR), + sim_file: PathBuf::from(DEFAULT_SIM_FILE), + total_time: DEFAULT_TOTAL_TIME, + print_batch_size: DEFAULT_PRINT_BATCH_SIZE, + log_level: LevelFilter::Info, + expected_pmt_amt: EXPECTED_PAYMENT_AMOUNT, + capacity_multiplier: ACTIVITY_MULTIPLIER, + no_results: DEFAULT_NO_RESULTS, + config: DEFAULT_CONFIGURATION_FILE.into(), + log_interval: DEFAULT_LOG_INTERVAL, + } + } +} + +impl Cli { + /// Creates Cli from a configuration if provided, else it defaults to + /// CLI default values + fn from_config_file(file_path: PathBuf) -> anyhow::Result { + let default_cli = Cli::default(); + + if file_path == PathBuf::from(DEFAULT_CONFIGURATION_FILE) { + let config_path_res = default_config_path(WORKSPACE_ROOT_DIR.into(), file_path.clone()); + if config_path_res.is_err() { + log::info!("Default configuration file: {file_path:?} (not found, skipping)."); + return anyhow::Ok(default_cli); + } + } + + let simln_conf = Config::builder() + .add_source(File::with_name(file_path.as_os_str().to_string_lossy().as_ref())) + .build()? + .get_table(CONFIG_HEADER)?; + + let config_cli = Cli { + data_dir: from_config_field(&simln_conf, "data_dir", DEFAULT_DATA_DIR.into()), + sim_file: from_config_field(&simln_conf, "sim_file", DEFAULT_SIM_FILE.into()), + total_time: from_config_field(&simln_conf, "total_time", DEFAULT_TOTAL_TIME), + print_batch_size: from_config_field(&simln_conf, "print_batch_size", DEFAULT_PRINT_BATCH_SIZE), + log_level: LevelFilter::from_str(&from_config_field::( + &simln_conf, + "log_level", + DEFAULT_LOG_LEVEL.to_string(), + ))?, + expected_pmt_amt: from_config_field(&simln_conf, "expected_pmt_amt", EXPECTED_PAYMENT_AMOUNT), + capacity_multiplier: from_config_field(&simln_conf, "capacity_multiplier", ACTIVITY_MULTIPLIER), + no_results: from_config_field(&simln_conf, "no_results", DEFAULT_NO_RESULTS), + log_interval: from_config_field(&simln_conf, "log_interval", DEFAULT_LOG_INTERVAL), + config: default_cli.config, + }; + + anyhow::Ok(config_cli) + } + + /// Converts into simulation config + pub fn to_simulation_config(&self) -> SimulationConfig { + SimulationConfig { + log_level: self.log_level, + total_time: self.total_time, + expected_pmt_amt: self.expected_pmt_amt, + capacity_multiplier: self.capacity_multiplier, + no_results: self.no_results, + print_batch_size: self.print_batch_size, + data_dir: self.data_dir.to_path_buf(), + sim_file: self.sim_file.to_path_buf(), + log_interval: self.log_interval, + } + } + + /// Overwrites Cli with another + fn overwrite_with(&mut self, cli: &Cli) { + macro_rules! overwrite_field { + ($cli:ident, $field:ident, $default:expr) => { + if $cli.$field != $default { + log::info!("Command line arg: {}={:?}.", stringify!($field), $cli.$field); + self.$field = $cli.$field.clone(); + } + }; + } + + overwrite_field!(cli, data_dir, PathBuf::from(DEFAULT_DATA_DIR)); + overwrite_field!(cli, sim_file, PathBuf::from(DEFAULT_SIM_FILE)); + overwrite_field!(cli, total_time, DEFAULT_TOTAL_TIME); + overwrite_field!(cli, print_batch_size, DEFAULT_PRINT_BATCH_SIZE); + overwrite_field!(cli, log_level, LevelFilter::Info); + overwrite_field!(cli, expected_pmt_amt, EXPECTED_PAYMENT_AMOUNT); + overwrite_field!(cli, capacity_multiplier, ACTIVITY_MULTIPLIER); + overwrite_field!(cli, no_results, DEFAULT_NO_RESULTS); + overwrite_field!(cli, log_interval, DEFAULT_LOG_INTERVAL); + overwrite_field!(cli, config, PathBuf::from(DEFAULT_CONFIGURATION_FILE)); + } +} + +/// Helper function to parse field_name from the config map. Defaults to the +/// field_name's default if deserialization fails +fn from_config_field<'de, T>(config_map: &HashMap, field_name: &str, default_value: T) -> T +where + T: Clone + Deserialize<'de> + Debug, +{ + config_map.get(field_name).map_or(default_value.clone(), |value| { + let res = value.clone().try_deserialize(); + match res { + Ok(result) => { + log::info!("Configuration file arg: {field_name}={result:?}."); + result + }, + Err(e) => { + log::error!("Failed to parse {field_name}. Error => {e:?}."); + log::info!("Default value {:?} used.", default_value); + default_value + }, + } + }) +} + +/// Merge command line and configuration value `Cli`s +pub fn merge_cli() -> anyhow::Result { + let cli = Cli::parse(); + log::info!("Configuration file: {:?}.", cli.config.display()); + let mut cli_from_config = Cli::from_config_file(cli.config.clone())?; + cli_from_config.overwrite_with(&cli); + + anyhow::Ok(cli_from_config) +} + +fn default_config_path(root_dir: PathBuf, config_file: PathBuf) -> anyhow::Result { + let conf_path = if config_file.is_relative() { + root_dir.join(config_file) + } else { + config_file + }; + + if conf_path.exists() { + Ok(conf_path) + } else { + anyhow::bail!("Default configuration file not found.") + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use crate::cli::from_config_field; + + #[test] + fn test_from_config_field() { + let mut config_map = HashMap::new(); + config_map.insert("key".to_string(), config::Value::new(None, "value")); + + let value = from_config_field(&config_map, "key", "default_value".to_string()); + let default_value = from_config_field(&config_map, "no_key", "default_value"); + + assert_eq!(value, "value"); + assert_eq!(default_value, "default_value"); + } +} diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index 5eeaf5e8..ccf3b994 100644 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -1,455 +1,27 @@ use bitcoin::secp256k1::PublicKey; -use config::{Config, File}; use flexi_logger::{LogSpecBuilder, Logger}; -use serde::Deserialize; +use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; -use std::{collections::HashMap, str::FromStr}; use tokio::sync::Mutex; use anyhow::anyhow; -use clap::builder::TypedValueParser; -use clap::Parser; use log::LevelFilter; use sim_lib::{ - cln::ClnNode, lnd::LndNode, ActivityDefinition, LightningError, LightningNode, NodeConnection, - NodeId, SimParams, Simulation, SimulationConfig, WriteResults, + cln::ClnNode, lnd::LndNode, ActivityDefinition, LightningError, LightningNode, NodeConnection, NodeId, SimParams, + Simulation, WriteResults, }; -/// The default directory where the simulation files are stored and where the results will be written to. -pub const DEFAULT_DATA_DIR: &str = "."; +mod cli; +use cli::{merge_cli, DEFAULT_LOG_LEVEL}; -/// The default simulation file to be used by the simulator. -pub const DEFAULT_SIM_FILE: &str = "sim.json"; - -/// The default expected payment amount for the simulation, around ~$10 at the time of writing. -pub const EXPECTED_PAYMENT_AMOUNT: u64 = 3_800_000; - -/// The number of times over each node in the network sends its total deployed capacity in a calendar month. -pub const ACTIVITY_MULTIPLIER: f64 = 2.0; - -/// Default batch size to flush result data to disk -const DEFAULT_PRINT_BATCH_SIZE: u32 = 500; - -/// Default configuration file -const DEFAULT_CONFIGURATION_FILE: &str = "conf.ini"; - -/// Default total time -const DEFAULT_TOTAL_TIME: Option = None; - -/// Default log level -const DEFAULT_LOG_LEVEL: &str = "info"; - -/// Default no results -const DEFAULT_NO_RESULTS: bool = false; - -/// Default log interval -const DEFAULT_LOG_INTERVAL: u32 = 60; - -/// Deserializes a f64 as long as it is positive and greater than 0. -fn deserialize_f64_greater_than_zero(x: String) -> Result { - match x.parse::() { - Ok(x) => { - if x > 0.0 { - Ok(x) - } else { - Err(format!( - "capacity_multiplier must be higher than 0. {x} received." - )) - } - }, - Err(e) => Err(e.to_string()), - } -} - -/// Custom deserialization function for `LevelFilter`. Required because `LevelFilter` does -/// not implement deserialize -pub fn deserialize_log_level<'de, D>(deserializer: D) -> Result -where - D: serde::Deserializer<'de>, -{ - let level_filter_str = String::deserialize(deserializer)?; - - let level_filter = LevelFilter::from_str(&level_filter_str).map_err(|e| { - serde::de::Error::custom(format!("Failed to deserialize LevelFilter from &str: {e}")) - })?; - - Ok(level_filter) -} - -/// Custom deserialization function for total time. This method is required because -/// the `config` crate is unable to parse null values in `.ini` files to an `Option::None` -pub fn deserialize_total_time<'de, D>(deserializer: D) -> Result, D::Error> -where - D: serde::Deserializer<'de>, -{ - let total_time_str = String::deserialize(deserializer)?; - - if total_time_str.is_empty() { - return Ok(None); - } - - // Parse string value to u32 - let total_time = total_time_str - .parse::() - .map_err(|e| serde::de::Error::custom(format!("Failed to parse u32 from &str: {e}")))?; - - Ok(Some(total_time)) -} - -#[derive(Parser, Deserialize)] -#[command(version, about)] -struct Cli { - /// Path to a directory containing simulation files, and where simulation results will be stored - #[clap(long, short, default_value = DEFAULT_DATA_DIR)] - data_dir: PathBuf, - /// Path to the simulation file to be used by the simulator - /// This can either be an absolute path, or relative path with respect to data_dir - #[clap(long, short, default_value = DEFAULT_SIM_FILE)] - sim_file: PathBuf, - /// Total time the simulator will be running - #[clap(long, short)] - total_time: Option, - /// Number of activity results to batch together before printing to csv file [min: 1] - #[clap(long, short, default_value_t = DEFAULT_PRINT_BATCH_SIZE, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u32::MAX as u64))] - print_batch_size: u32, - /// Level of verbosity of the messages displayed by the simulator. - /// Possible values: [off, error, warn, info, debug, trace] - #[clap(long, short, verbatim_doc_comment, default_value = DEFAULT_LOG_LEVEL)] - #[serde(deserialize_with = "deserialize_log_level")] - log_level: LevelFilter, - /// Expected payment amount for the random activity generator - #[clap(long, short, default_value_t = EXPECTED_PAYMENT_AMOUNT, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u64::MAX))] - expected_pmt_amt: u64, - /// Multiplier of the overall network capacity used by the random activity generator - #[clap(long, short, default_value_t = ACTIVITY_MULTIPLIER, value_parser = clap::builder::StringValueParser::new().try_map(deserialize_f64_greater_than_zero))] - capacity_multiplier: f64, - /// Do not create an output file containing the simulations results - #[clap(long, default_value_t = DEFAULT_NO_RESULTS)] - no_results: bool, - /// Sets a custom configuration (INI) file - #[clap(long, short = 'C', value_name = "CONFIG_FILE", default_value = DEFAULT_CONFIGURATION_FILE)] - config: PathBuf, - #[clap(long, short = 'L', default_value_t = DEFAULT_LOG_INTERVAL, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u32::MAX as u64))] - log_interval: u32, -} - -/// Implementation of Cli with default values -impl Default for Cli { - fn default() -> Self { - Cli { - data_dir: PathBuf::from(DEFAULT_DATA_DIR), - sim_file: PathBuf::from(DEFAULT_SIM_FILE), - total_time: DEFAULT_TOTAL_TIME, - print_batch_size: DEFAULT_PRINT_BATCH_SIZE, - log_level: LevelFilter::Info, - expected_pmt_amt: EXPECTED_PAYMENT_AMOUNT, - capacity_multiplier: ACTIVITY_MULTIPLIER, - no_results: DEFAULT_NO_RESULTS, - config: PathBuf::from(DEFAULT_CONFIGURATION_FILE), - log_interval: DEFAULT_LOG_INTERVAL, - } - } -} - -impl Cli { - /// Creates Cli from a configuration if provided, else it defaults to - /// CLI default values - fn from_config_file(file_path: PathBuf) -> anyhow::Result { - let default_cli = Cli::default(); - - let config = Config::builder() - .add_source(File::with_name( - file_path.as_os_str().to_string_lossy().as_ref(), - )) - .build()?; - - let simln_conf = config.get_table("simln.conf")?; - - let config_data_dir = - simln_conf - .get("data_dir") - .map_or(default_cli.data_dir.clone(), |val| { - let data_dir_res = val.clone().try_deserialize::(); - match data_dir_res { - Ok(data_dir) => { - log::info!("Configuration file arg: data_dir={:?}.", data_dir); - PathBuf::from(data_dir) - }, - Err(e) => { - log::error!( - "Failed to parse data_dir. Default value used. Error: {e:?}." - ); - default_cli.data_dir.clone() - }, - } - }); - - let config_sim_file = - simln_conf - .get("sim_file") - .map_or(default_cli.sim_file.clone(), |val| { - let sim_file_res = val.clone().try_deserialize::(); - match sim_file_res { - Ok(sim_file) => { - log::info!("Configuration file arg: sim_file={:?}.", sim_file); - PathBuf::from(sim_file) - }, - Err(e) => { - log::error!( - "Failed to parse sim_file. Default value used. Error: {e:?}.", - ); - default_cli.sim_file - }, - } - }); - - let config_total_time = - simln_conf - .get("total_time") - .map_or(default_cli.total_time, |val| { - let total_time_res = val.clone().try_deserialize::>(); - match total_time_res { - Ok(total_time) => { - log::info!("Configuration file arg: total_time={:?}.", total_time); - total_time - }, - Err(e) => { - log::error!( - "Failed to parse total_time. Default value used. Error: {e:?}." - ); - default_cli.total_time - }, - } - }); - - let config_print_batch_size = - simln_conf - .get("print_batch_size") - .map_or(default_cli.print_batch_size, |val| { - let print_batch_size_res = val.clone().try_deserialize::(); - match print_batch_size_res { - Ok(print_batch_size) => { - log::info!( - "Configuration file arg: print_batch_size={:?}.", - print_batch_size - ); - print_batch_size - }, - Err(e) => { - log::error!( - "Failed to parse print_batch_size. Default value used. Error: {e:?}." - ); - default_cli.print_batch_size - }, - } - }); - - let config_log_level = - simln_conf - .get("log_level") - .map_or(DEFAULT_LOG_LEVEL.to_string(), |val| { - let log_level_res = val.clone().try_deserialize::(); - match log_level_res { - Ok(log_level) => { - log::info!("Configuration file arg: log_level={:?}.", log_level); - log_level - }, - Err(e) => { - log::error!( - "Failed to parse log_level. Default value used. Error: {e:?}." - ); - DEFAULT_LOG_LEVEL.to_string() - }, - } - }); - - let config_expected_pmt_amt = - simln_conf - .get("expected_pmt_amt") - .map_or(default_cli.expected_pmt_amt, |val| { - let pmt_amt_res = val.clone().try_deserialize::(); - match pmt_amt_res { - Ok(pmt_amt) => { - log::info!("Configuration file arg: expected_pmt_amt={:?}.", pmt_amt); - pmt_amt - }, - Err(e) => { - log::error!( - "Failed to parse expected_pmt_amt. Default value used. Error: {e:?}." - ); - default_cli.expected_pmt_amt - }, - } - }); - - let config_capacity_multiplier = - simln_conf - .get("capacity_multiplier") - .map_or(default_cli.capacity_multiplier, |val| { - let capacity_multiplier_res = val.clone().try_deserialize::(); - match capacity_multiplier_res { - Ok(capacity_multiplier) => { - log::info!( - "Configuration file arg: capacity_multiplier={:?}.", - capacity_multiplier - ); - capacity_multiplier - }, - Err(e) => { - log::error!( - "Failed to parse capacity_multiplier. Default value used. Error: {e:?}." - ); - default_cli.capacity_multiplier - }, - } - }); - - let config_no_results = - simln_conf - .get("no_results") - .map_or(default_cli.no_results, |val| { - let no_results_res = val.clone().try_deserialize::(); - match no_results_res { - Ok(no_results) => { - log::info!("Configuration file arg: no_results={:?}.", no_results); - no_results - }, - Err(e) => { - log::error!( - "Failed to parse no_results. Default value used. Error: {e:?}." - ); - default_cli.no_results - }, - } - }); - - let config_log_interval = - simln_conf - .get("log_interval") - .map_or(default_cli.log_interval, |val| { - let log_interval_res = val.clone().try_deserialize::(); - match log_interval_res { - Ok(log_interval) => { - log::info!("Configuration file arg: log_interval={:?}.", log_interval); - log_interval - }, - Err(e) => { - log::error!( - "Failed to parse log_interval. Default value used. Error: {e:?}." - ); - default_cli.log_interval - }, - } - }); - - let config_cli = Cli { - data_dir: config_data_dir, - sim_file: config_sim_file, - total_time: config_total_time, - print_batch_size: config_print_batch_size, - log_level: LevelFilter::from_str(&config_log_level)?, - expected_pmt_amt: config_expected_pmt_amt, - capacity_multiplier: config_capacity_multiplier, - no_results: config_no_results, - config: default_cli.config, - log_interval: config_log_interval, - }; - - Ok(config_cli) - } - - /// Converts into simulation config - fn to_simulation_config(&self) -> SimulationConfig { - SimulationConfig { - log_level: self.log_level, - total_time: self.total_time, - expected_pmt_amt: self.expected_pmt_amt, - capacity_multiplier: self.capacity_multiplier, - no_results: self.no_results, - print_batch_size: self.print_batch_size, - data_dir: self.data_dir.to_path_buf(), - sim_file: self.sim_file.to_path_buf(), - log_interval: self.log_interval, - } - } -} - -/// Merge command line and configuration value `Cli`s -fn merge_cli() -> anyhow::Result { - let cli = Cli::parse(); - log::info!( - "Configuration file: {:?}.", - cli.config.canonicalize()?.display() - ); - - let mut cli_from_config = Cli::from_config_file(cli.config.to_path_buf())?; - - if cli.data_dir != PathBuf::from(DEFAULT_DATA_DIR) { - log::info!("Command line arg: data_dir={:?}.", cli.data_dir); - cli_from_config.data_dir = cli.data_dir - } - - if cli.sim_file != PathBuf::from(DEFAULT_SIM_FILE) { - log::info!("Command line arg: sim_file={:?}.", cli.sim_file); - cli_from_config.sim_file = cli.sim_file - } - - if cli.total_time != DEFAULT_TOTAL_TIME { - log::info!("Command line arg: total_time={:?}.", cli.total_time); - cli_from_config.total_time = cli.total_time - } - - if cli.print_batch_size != DEFAULT_PRINT_BATCH_SIZE { - log::info!( - "Command line arg: print_batch_size={:?}.", - cli.print_batch_size - ); - cli_from_config.print_batch_size = cli.print_batch_size - } - - if cli.log_level.as_str() != DEFAULT_LOG_LEVEL { - log::info!("Command line arg: log_level={:?}.", cli.log_level); - cli_from_config.log_level = cli.log_level - } - - if cli.expected_pmt_amt != EXPECTED_PAYMENT_AMOUNT { - log::info!( - "Command line arg: expected_pmt_amt={:?}.", - cli.expected_pmt_amt - ); - cli_from_config.expected_pmt_amt = cli.expected_pmt_amt - } - - if cli.capacity_multiplier != ACTIVITY_MULTIPLIER { - log::info!( - "Command line arg: capacity_multiplier={:?}.", - cli.capacity_multiplier - ); - cli_from_config.capacity_multiplier = cli.capacity_multiplier - } - - if cli.no_results != DEFAULT_NO_RESULTS { - log::info!("Command line arg: no_results={:?}.", cli.no_results); - cli_from_config.no_results = cli.no_results - } - - if cli.log_interval != DEFAULT_LOG_INTERVAL { - log::info!("Command line arg: log_interval={:?}.", cli.log_interval); - cli_from_config.log_interval = cli.log_interval; - } - - if cli.config != PathBuf::from(DEFAULT_CONFIGURATION_FILE) { - log::info!("Command line arg: config={:?}.", cli.config); - } - - anyhow::Ok(cli_from_config) -} +/// Colour pallette of the logger +const COLOUR_PALETTE: &str = "b1;3;2;4;6"; #[tokio::main] async fn main() -> anyhow::Result<()> { - let logger_handle = Logger::try_with_str("info")? - .set_palette("b1;3;2;4;6".to_string()) + let logger_handle = Logger::try_with_str(DEFAULT_LOG_LEVEL)? + .set_palette(COLOUR_PALETTE.to_string()) .start()?; let cli = merge_cli()?; From 590d834e7d86412eaf8e7e12cbfb9078d307bcb3 Mon Sep 17 00:00:00 2001 From: Enigbe Ochekliye Date: Mon, 11 Mar 2024 10:52:13 +0100 Subject: [PATCH 5/7] chore: format code to wrap at max width of 120 --- rustfmt.toml | 1 + sim-cli/src/main.rs | 26 +++----- sim-lib/src/cln.rs | 78 ++++++++-------------- sim-lib/src/defined_activity.rs | 8 +-- sim-lib/src/lib.rs | 110 ++++++++++---------------------- sim-lib/src/lnd.rs | 41 ++++-------- sim-lib/src/random_activity.rs | 49 ++++---------- sim-lib/src/serializers.rs | 5 +- 8 files changed, 97 insertions(+), 221 deletions(-) diff --git a/rustfmt.toml b/rustfmt.toml index 8a21b82e..f63faebd 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -2,3 +2,4 @@ edition = "2021" match_block_trailing_comma = true newline_style = "Unix" use_try_shorthand = true +max_width = 120 diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index ccf3b994..d3b78f7d 100644 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -36,9 +36,13 @@ async fn main() -> anyhow::Result<()> { ); let sim_path = read_sim_path(opts.data_dir.clone(), opts.sim_file).await?; - let SimParams { nodes, activity } = - serde_json::from_str(&std::fs::read_to_string(sim_path)?) - .map_err(|e| anyhow!("Could not deserialize node connection data or activity description from simulation file (line {}, col {}).", e.line(), e.column()))?; + let SimParams { nodes, activity } = serde_json::from_str(&std::fs::read_to_string(sim_path)?).map_err(|e| { + anyhow!( + "Nodes or activities not parsed from simulation file (line {}, col {}).", + e.line(), + e.column() + ) + })?; let mut clients: HashMap>> = HashMap::new(); let mut pk_node_map = HashMap::new(); @@ -55,11 +59,7 @@ async fn main() -> anyhow::Result<()> { let node_info = node.lock().await.get_info().clone(); - log::info!( - "Connected to {} - Node ID: {}.", - node_info.alias, - node_info.pubkey - ); + log::info!("Connected to {} - Node ID: {}.", node_info.alias, node_info.pubkey); if clients.contains_key(&node_info.pubkey) { anyhow::bail!(LightningError::ValidationError(format!( @@ -121,10 +121,7 @@ async fn main() -> anyhow::Result<()> { .await .map_err(|e| { log::debug!("{}", e); - LightningError::ValidationError(format!( - "Destination node unknown or invalid: {}.", - pk, - )) + LightningError::ValidationError(format!("Destination node unknown or invalid: {}.", pk,)) })? } }, @@ -196,10 +193,7 @@ async fn select_sim_file(data_dir: PathBuf) -> anyhow::Result { .collect::>(); if sim_files.is_empty() { - anyhow::bail!( - "no simulation files found in {}.", - data_dir.canonicalize()?.display() - ); + anyhow::bail!("no simulation files found in {}.", data_dir.canonicalize()?.display()); } let selection = dialoguer::Select::new() diff --git a/sim-lib/src/cln.rs b/sim-lib/src/cln.rs index 13396910..be13ccde 100644 --- a/sim-lib/src/cln.rs +++ b/sim-lib/src/cln.rs @@ -2,9 +2,8 @@ use async_trait::async_trait; use bitcoin::secp256k1::PublicKey; use bitcoin::Network; use cln_grpc::pb::{ - listpays_pays::ListpaysPaysStatus, node_client::NodeClient, Amount, GetinfoRequest, - KeysendRequest, KeysendResponse, ListchannelsRequest, ListnodesRequest, ListpaysRequest, - ListpaysResponse, + listpays_pays::ListpaysPaysStatus, node_client::NodeClient, Amount, GetinfoRequest, KeysendRequest, + KeysendResponse, ListchannelsRequest, ListnodesRequest, ListpaysRequest, ListpaysResponse, }; use lightning::ln::features::NodeFeatures; use lightning::ln::PaymentHash; @@ -16,9 +15,7 @@ use tokio::time::{self, Duration}; use tonic::transport::{Certificate, Channel, ClientTlsConfig, Identity}; use triggered::Listener; -use crate::{ - serializers, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome, PaymentResult, -}; +use crate::{serializers, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome, PaymentResult}; #[derive(Serialize, Deserialize, Debug, Clone)] pub struct ClnConnection { @@ -43,31 +40,25 @@ impl ClnNode { let tls = ClientTlsConfig::new() .domain_name("cln") .identity(Identity::from_pem( - reader(&connection.client_cert).await.map_err(|_| { - LightningError::ConnectionError("Cannot loads client certificate".to_string()) - })?, - reader(&connection.client_key).await.map_err(|_| { - LightningError::ConnectionError("Cannot loads client key".to_string()) - })?, + reader(&connection.client_cert) + .await + .map_err(|_| LightningError::ConnectionError("Cannot loads client certificate".to_string()))?, + reader(&connection.client_key) + .await + .map_err(|_| LightningError::ConnectionError("Cannot loads client key".to_string()))?, )) - .ca_certificate(Certificate::from_pem( - reader(&connection.ca_cert).await.map_err(|_| { - LightningError::ConnectionError("Cannot loads CA certificate".to_string()) - })?, - )); + .ca_certificate(Certificate::from_pem(reader(&connection.ca_cert).await.map_err( + |_| LightningError::ConnectionError("Cannot loads CA certificate".to_string()), + )?)); let mut client = NodeClient::new( Channel::from_shared(connection.address.to_string()) .map_err(|err| LightningError::ConnectionError(err.to_string()))? .tls_config(tls) - .map_err(|_| { - LightningError::ConnectionError("Cannot establish tls connection".to_string()) - })? + .map_err(|_| LightningError::ConnectionError("Cannot establish tls connection".to_string()))? .connect() .await - .map_err(|_| { - LightningError::ConnectionError("Cannot connect to gRPC server".to_string()) - })?, + .map_err(|_| LightningError::ConnectionError("Cannot connect to gRPC server".to_string()))?, ); let (id, mut alias, our_features) = client @@ -75,16 +66,11 @@ impl ClnNode { .await .map(|r| { let inner = r.into_inner(); - ( - inner.id, - inner.alias.unwrap_or_default(), - inner.our_features, - ) + (inner.id, inner.alias.unwrap_or_default(), inner.our_features) }) .map_err(|err| LightningError::GetInfoError(err.to_string()))?; - let pubkey = PublicKey::from_slice(&id) - .map_err(|err| LightningError::GetInfoError(err.to_string()))?; + let pubkey = PublicKey::from_slice(&id).map_err(|err| LightningError::GetInfoError(err.to_string()))?; connection.id.validate(&pubkey, &mut alias)?; let features = if let Some(features) = our_features { @@ -148,15 +134,10 @@ impl LightningNode for ClnNode { .map_err(|err| LightningError::GetInfoError(err.to_string()))? .into_inner(); - Ok(Network::from_core_arg(&info.network) - .map_err(|err| LightningError::ValidationError(err.to_string()))?) + Ok(Network::from_core_arg(&info.network).map_err(|err| LightningError::ValidationError(err.to_string()))?) } - async fn send_payment( - &mut self, - dest: PublicKey, - amount_msat: u64, - ) -> Result { + async fn send_payment(&mut self, dest: PublicKey, amount_msat: u64) -> Result { let KeysendResponse { payment_hash, .. } = self .client .key_send(KeysendRequest { @@ -186,11 +167,7 @@ impl LightningNode for ClnNode { Ok(PaymentHash(slice)) } - async fn track_payment( - &mut self, - hash: PaymentHash, - shutdown: Listener, - ) -> Result { + async fn track_payment(&mut self, hash: PaymentHash, shutdown: Listener) -> Result { loop { tokio::select! { biased; @@ -245,19 +222,14 @@ impl LightningNode for ClnNode { Ok(NodeInfo { pubkey: *node_id, alias: node.alias.unwrap_or(String::new()), - features: node - .features - .clone() - .map_or(NodeFeatures::empty(), |mut f| { - // We need to reverse this given it has the CLN wire encoding which is BE - f.reverse(); - NodeFeatures::from_le_bytes(f) - }), + features: node.features.clone().map_or(NodeFeatures::empty(), |mut f| { + // We need to reverse this given it has the CLN wire encoding which is BE + f.reverse(); + NodeFeatures::from_le_bytes(f) + }), }) } else { - Err(LightningError::GetNodeInfoError( - "Node not found".to_string(), - )) + Err(LightningError::GetNodeInfoError("Node not found".to_string())) } } diff --git a/sim-lib/src/defined_activity.rs b/sim-lib/src/defined_activity.rs index 98bbe07e..a46ceb02 100644 --- a/sim-lib/src/defined_activity.rs +++ b/sim-lib/src/defined_activity.rs @@ -40,10 +40,7 @@ impl PaymentGenerator for DefinedPaymentActivity { self.wait } - fn payment_amount( - &self, - destination_capacity: Option, - ) -> Result { + fn payment_amount(&self, destination_capacity: Option) -> Result { if destination_capacity.is_some() { Err(PaymentGenerationError( "destination amount must not be set for defined activity generator".to_string(), @@ -69,8 +66,7 @@ mod tests { let source = get_random_keypair(); let payment_amt = 50; - let generator = - DefinedPaymentActivity::new(node.clone(), Duration::from_secs(60), payment_amt); + let generator = DefinedPaymentActivity::new(node.clone(), Duration::from_secs(60), payment_amt); let (dest, dest_capacity) = generator.choose_destination(source.1); assert_eq!(node.pubkey, dest.pubkey); diff --git a/sim-lib/src/lib.rs b/sim-lib/src/lib.rs index 4042f3c1..702a066e 100644 --- a/sim-lib/src/lib.rs +++ b/sim-lib/src/lib.rs @@ -50,12 +50,18 @@ impl NodeId { crate::NodeId::PublicKey(pk) => { if pk != node_id { return Err(LightningError::ValidationError(format!( - "the provided node id does not match the one returned by the backend ({} != {}).", pk, node_id))); + "the provided node id does not match the one returned by the backend ({} != {}).", + pk, node_id + ))); } }, crate::NodeId::Alias(a) => { if alias != a { - log::warn!("The provided alias does not match the one returned by the backend ({} != {}).", a, alias) + log::warn!( + "The provided alias does not match the one returned by the backend ({} != {}).", + a, + alias + ) } *alias = a.to_string(); }, @@ -185,17 +191,9 @@ pub trait LightningNode: Send { /// Get the network this node is running at async fn get_network(&mut self) -> Result; /// Keysend payment worth `amount_msat` from a source node to the destination node. - async fn send_payment( - &mut self, - dest: PublicKey, - amount_msat: u64, - ) -> Result; + async fn send_payment(&mut self, dest: PublicKey, amount_msat: u64) -> Result; /// Track a payment with the specified hash. - async fn track_payment( - &mut self, - hash: PaymentHash, - shutdown: Listener, - ) -> Result; + async fn track_payment(&mut self, hash: PaymentHash, shutdown: Listener) -> Result; /// Gets information on a specific node async fn get_node_info(&mut self, node_id: &PublicKey) -> Result; /// Lists all channels, at present only returns a vector of channel capacities in msat because no further @@ -218,10 +216,7 @@ pub trait PaymentGenerator: Display + Send { fn next_payment_wait(&self) -> time::Duration; /// Returns a payment amount based, with a destination capacity optionally provided to inform the amount picked. - fn payment_amount( - &self, - destination_capacity: Option, - ) -> Result; + fn payment_amount(&self, destination_capacity: Option) -> Result; } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -396,7 +391,10 @@ impl Simulation { for node in self.nodes.values() { let node = node.lock().await; if !node.get_info().features.supports_keysend() { - return Err(LightningError::ValidationError(format!("All nodes eligible for random activity generation must support keysend, {} does not", node.get_info()))); + return Err(LightningError::ValidationError(format!( + "All nodes eligible for random activity generation must support keysend, {} does not", + node.get_info() + ))); } } } @@ -429,8 +427,7 @@ impl Simulation { async fn validate_node_network(&self) -> Result<(), LightningError> { if self.nodes.is_empty() { return Err(LightningError::ValidationError( - "we don't control any nodes. Specify at least one node in your config file" - .to_string(), + "we don't control any nodes. Specify at least one node in your config file".to_string(), )); } let mut running_network = Option::None; @@ -438,9 +435,7 @@ impl Simulation { for node in self.nodes.values() { let network = node.lock().await.get_network().await?; if network == Network::Bitcoin { - return Err(LightningError::ValidationError( - "mainnet is not supported".to_string(), - )); + return Err(LightningError::ValidationError("mainnet is not supported".to_string())); } running_network = running_network.take().or(Some(network)); @@ -504,10 +499,7 @@ impl Simulation { tasks.spawn(async move { if time::timeout(total_time, l).await.is_err() { - log::info!( - "Simulation run for {}s. Shutting down.", - total_time.as_secs() - ); + log::info!("Simulation run for {}s. Shutting down.", total_time.as_secs()); t.trigger() } }); @@ -533,11 +525,7 @@ impl Simulation { /// run_data_collection starts the tasks required for the simulation to report of the results of the activity that /// it generates. The simulation should report outputs via the receiver that is passed in. - fn run_data_collection( - &self, - output_receiver: Receiver, - tasks: &mut JoinSet<()>, - ) { + fn run_data_collection(&self, output_receiver: Receiver, tasks: &mut JoinSet<()>) { let listener = self.shutdown_listener.clone(); log::debug!("Setting up simulator data collection."); @@ -610,9 +598,7 @@ impl Simulation { for (pk, node) in self.nodes.iter() { let chan_capacity = node.lock().await.list_channels().await?.iter().sum::(); - if let Err(e) = - RandomPaymentActivity::validate_capacity(chan_capacity, self.expected_payment_msat) - { + if let Err(e) = RandomPaymentActivity::validate_capacity(chan_capacity, self.expected_payment_msat) { log::warn!("Node: {} not eligible for activity generation: {e}.", *pk); continue; } @@ -629,22 +615,15 @@ impl Simulation { .map_err(SimulationError::RandomActivityError)?, )); - log::info!( - "Created network generator: {}.", - network_generator.lock().await - ); + log::info!("Created network generator: {}.", network_generator.lock().await); for (node_info, capacity) in active_nodes.values() { generators.push(ExecutorKit { source_info: node_info.clone(), network_generator: network_generator.clone(), payment_generator: Box::new( - RandomPaymentActivity::new( - *capacity, - self.expected_payment_msat, - self.activity_multiplier, - ) - .map_err(SimulationError::RandomActivityError)?, + RandomPaymentActivity::new(*capacity, self.expected_payment_msat, self.activity_multiplier) + .map_err(SimulationError::RandomActivityError)?, ), }); } @@ -662,11 +641,7 @@ impl Simulation { ) -> HashMap> { let mut channels = HashMap::new(); - for (id, node) in self - .nodes - .iter() - .filter(|(id, _)| consuming_nodes.contains(id)) - { + for (id, node) in self.nodes.iter().filter(|(id, _)| consuming_nodes.contains(id)) { // For each node we have execution on, we'll create a sender and receiver channel to produce and consumer // events and insert producer in our tracking map. We do not buffer channels as we expect events to clear // quickly. @@ -695,12 +670,12 @@ impl Simulation { tasks: &mut JoinSet<()>, ) -> Result<(), SimulationError> { for executor in executors { - let sender = producer_channels.get(&executor.source_info.pubkey).ok_or( - SimulationError::RandomActivityError(RandomActivityError::ValueError(format!( - "Activity producer for: {} not found.", - executor.source_info.pubkey, - ))), - )?; + let sender = + producer_channels + .get(&executor.source_info.pubkey) + .ok_or(SimulationError::RandomActivityError(RandomActivityError::ValueError( + format!("Activity producer for: {} not found.", executor.source_info.pubkey,), + )))?; tasks.spawn(produce_events( executor.source_info, @@ -755,11 +730,7 @@ async fn consume_events( SimulationOutput::SendPaymentSuccess(payment) }, Err(e) => { - log::error!( - "Error while sending payment {} -> {}.", - node.get_info(), - dest - ); + log::error!("Error while sending payment {} -> {}.", node.get_info(), dest); match e { LightningError::PermanentError(s) => { @@ -767,10 +738,7 @@ async fn consume_events( shutdown.trigger(); break; }, - _ => SimulationOutput::SendPaymentFailure( - payment, - PaymentResult::not_dispatched(), - ), + _ => SimulationOutput::SendPaymentFailure(payment, PaymentResult::not_dispatched()), } }, }; @@ -924,9 +892,7 @@ struct PaymentResultLogger { impl PaymentResultLogger { fn new() -> Self { - PaymentResultLogger { - ..Default::default() - } + PaymentResultLogger { ..Default::default() } } fn report_result(&mut self, details: &Payment, result: &PaymentResult) { @@ -952,11 +918,7 @@ impl Display for PaymentResultLogger { } } -async fn run_results_logger( - listener: Listener, - logger: Arc>, - interval: Duration, -) { +async fn run_results_logger(listener: Listener, logger: Arc>, interval: Duration) { log::debug!("Results logger started."); log::info!("Summary of results will be reported every {:?}.", interval); @@ -1065,9 +1027,7 @@ async fn track_payment_result( }, // None means that the payment was not dispatched, so we cannot track it. None => { - log::error!( - "We cannot track a payment that has not been dispatched. Missing payment hash." - ); + log::error!("We cannot track a payment that has not been dispatched. Missing payment hash."); PaymentResult::not_dispatched() }, }; diff --git a/sim-lib/src/lnd.rs b/sim-lib/src/lnd.rs index b0148ee4..e9c502ad 100644 --- a/sim-lib/src/lnd.rs +++ b/sim-lib/src/lnd.rs @@ -1,9 +1,7 @@ use std::collections::HashSet; use std::{collections::HashMap, str::FromStr}; -use crate::{ - serializers, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome, PaymentResult, -}; +use crate::{serializers, LightningError, LightningNode, NodeId, NodeInfo, PaymentOutcome, PaymentResult}; use async_trait::async_trait; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::PublicKey; @@ -58,10 +56,9 @@ fn parse_node_features(features: HashSet) -> NodeFeatures { impl LndNode { pub async fn new(connection: LndConnection) -> Result { - let mut client = - tonic_lnd::connect(connection.address, connection.cert, connection.macaroon) - .await - .map_err(|err| LightningError::ConnectionError(err.to_string()))?; + let mut client = tonic_lnd::connect(connection.address, connection.cert, connection.macaroon) + .await + .map_err(|err| LightningError::ConnectionError(err.to_string()))?; let GetInfoResponse { identity_pubkey, @@ -75,8 +72,8 @@ impl LndNode { .map_err(|err| LightningError::GetInfoError(err.to_string()))? .into_inner(); - let pubkey = PublicKey::from_str(&identity_pubkey) - .map_err(|err| LightningError::GetInfoError(err.to_string()))?; + let pubkey = + PublicKey::from_str(&identity_pubkey).map_err(|err| LightningError::GetInfoError(err.to_string()))?; connection.id.validate(&pubkey, &mut alias)?; Ok(Self { @@ -123,21 +120,13 @@ impl LightningNode for LndNode { Ok(Network::from_str(match info.chains[0].network.as_str() { "mainnet" => "bitcoin", - "simnet" => { - return Err(LightningError::ValidationError( - "simnet is not supported".to_string(), - )) - }, + "simnet" => return Err(LightningError::ValidationError("simnet is not supported".to_string())), x => x, }) .map_err(|err| LightningError::ValidationError(err.to_string()))?) } - async fn send_payment( - &mut self, - dest: PublicKey, - amount_msat: u64, - ) -> Result { + async fn send_payment(&mut self, dest: PublicKey, amount_msat: u64) -> Result { let amt_msat: i64 = amount_msat .try_into() .map_err(|_| LightningError::SendPaymentError("Invalid send amount".to_string()))?; @@ -173,11 +162,7 @@ impl LightningNode for LndNode { Ok(payment_hash) } - async fn track_payment( - &mut self, - hash: PaymentHash, - shutdown: Listener, - ) -> Result { + async fn track_payment(&mut self, hash: PaymentHash, shutdown: Listener) -> Result { let response = self .client .router() @@ -250,9 +235,7 @@ impl LightningNode for LndNode { features: parse_node_features(node_info.features.keys().cloned().collect()), }) } else { - Err(LightningError::GetNodeInfoError( - "Node not found".to_string(), - )) + Err(LightningError::GetNodeInfoError("Node not found".to_string())) } } @@ -260,9 +243,7 @@ impl LightningNode for LndNode { let channels = self .client .lightning() - .list_channels(ListChannelsRequest { - ..Default::default() - }) + .list_channels(ListChannelsRequest { ..Default::default() }) .await .map_err(|err| LightningError::ListChannelsError(err.to_string()))? .into_inner(); diff --git a/sim-lib/src/random_activity.rs b/sim-lib/src/random_activity.rs index f6917d4d..84d267ec 100644 --- a/sim-lib/src/random_activity.rs +++ b/sim-lib/src/random_activity.rs @@ -124,9 +124,7 @@ impl RandomPaymentActivity { } if multiplier == 0.0 { - return Err(RandomActivityError::ValueError( - "multiplier cannot be zero".into(), - )); + return Err(RandomActivityError::ValueError("multiplier cannot be zero".into())); } RandomPaymentActivity::validate_capacity(source_capacity_msat, expected_payment_amt)?; @@ -134,11 +132,10 @@ impl RandomPaymentActivity { // Lamda for the exponential distribution that we'll use to randomly time events is equal to the number of // events that we expect to see within our set period. - let lamda = events_per_month(source_capacity_msat, multiplier, expected_payment_amt) - / (SECONDS_PER_MONTH as f64); + let lamda = + events_per_month(source_capacity_msat, multiplier, expected_payment_amt) / (SECONDS_PER_MONTH as f64); - let event_dist = - Exp::new(lamda).map_err(|e| RandomActivityError::ValueError(e.to_string()))?; + let event_dist = Exp::new(lamda).map_err(|e| RandomActivityError::ValueError(e.to_string()))?; Ok(RandomPaymentActivity { multiplier, @@ -150,10 +147,7 @@ impl RandomPaymentActivity { /// Validates that the generator will be able to generate payment amounts based on the node's capacity and the /// simulation's expected payment amount. - pub fn validate_capacity( - node_capacity_msat: u64, - expected_payment_amt: u64, - ) -> Result<(), RandomActivityError> { + pub fn validate_capacity(node_capacity_msat: u64, expected_payment_amt: u64) -> Result<(), RandomActivityError> { // We will not be able to generate payments if the variance of sigma squared for our log normal distribution // is < 0 (because we have to take a square root). // @@ -207,10 +201,7 @@ impl PaymentGenerator for RandomPaymentActivity { /// capacity. While the expected value of payments remains the same, scaling variance by node capacity means that /// nodes with more deployed capital will see a larger range of payment values than those with smaller total /// channel capacity. - fn payment_amount( - &self, - destination_capacity: Option, - ) -> Result { + fn payment_amount(&self, destination_capacity: Option) -> Result { let destination_capacity = destination_capacity.ok_or(PaymentGenerationError( "destination amount required for payment activity generator".to_string(), ))?; @@ -229,8 +220,7 @@ impl PaymentGenerator for RandomPaymentActivity { ))); } - let log_normal = LogNormal::new(mu, sigma_square.sqrt()) - .map_err(|e| PaymentGenerationError(e.to_string()))?; + let log_normal = LogNormal::new(mu, sigma_square.sqrt()).map_err(|e| PaymentGenerationError(e.to_string()))?; let mut rng = rand::thread_rng(); Ok(log_normal.sample(&mut rng) as u64) @@ -239,11 +229,7 @@ impl PaymentGenerator for RandomPaymentActivity { impl Display for RandomPaymentActivity { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - let monthly_events = events_per_month( - self.source_capacity, - self.multiplier, - self.expected_payment_amt, - ); + let monthly_events = events_per_month(self.source_capacity, self.multiplier, self.expected_payment_amt); write!( f, @@ -338,9 +324,7 @@ mod tests { // distribution must fail building given the inputs. The former will be thoroughly tested in its own unit test, but we'll test some basic cases // here. Mainly, if the `capacity < expected_payment_amnt / 2`, the generator will fail building let expected_payment = get_random_int(1, 100); - assert!( - RandomPaymentActivity::new(2 * expected_payment, expected_payment, 1.0).is_ok() - ); + assert!(RandomPaymentActivity::new(2 * expected_payment, expected_payment, 1.0).is_ok()); assert!(matches!( RandomPaymentActivity::new(2 * expected_payment, expected_payment + 1, 1.0), Err(RandomActivityError::InsufficientCapacity { .. }) @@ -372,10 +356,7 @@ mod tests { let r = RandomPaymentActivity::validate_capacity(capacity, payment_amt); if capacity < 2 * payment_amt { - assert!(matches!( - r, - Err(RandomActivityError::InsufficientCapacity { .. }) - )); + assert!(matches!(r, Err(RandomActivityError::InsufficientCapacity { .. }))); } else { assert!(r.is_ok()); } @@ -394,10 +375,7 @@ mod tests { // Wrong cases for i in 0..source_capacity { - assert!(matches!( - pag.payment_amount(Some(i)), - Err(PaymentGenerationError(..)) - )) + assert!(matches!(pag.payment_amount(Some(i)), Err(PaymentGenerationError(..)))) } // All other cases will work. We are not going to exhaustively test for the rest up to u64::MAX, let just pick a bunch @@ -410,10 +388,7 @@ mod tests { assert!(pag.payment_amount(Some(i)).is_ok()) } - assert!(matches!( - pag.payment_amount(None), - Err(PaymentGenerationError(..)) - )); + assert!(matches!(pag.payment_amount(None), Err(PaymentGenerationError(..)))); } } } diff --git a/sim-lib/src/serializers.rs b/sim-lib/src/serializers.rs index 3fd46fa7..57df186c 100644 --- a/sim-lib/src/serializers.rs +++ b/sim-lib/src/serializers.rs @@ -50,8 +50,5 @@ where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; - Ok(expanduser(s) - .map_err(serde::de::Error::custom)? - .display() - .to_string()) + Ok(expanduser(s).map_err(serde::de::Error::custom)?.display().to_string()) } From 2f816b6c0beaf520d5a69e53cff9f201933d7889 Mon Sep 17 00:00:00 2001 From: Enigbe Ochekliye Date: Mon, 11 Mar 2024 11:54:08 +0100 Subject: [PATCH 6/7] refactor: make log interval configurable --- sim-cli/src/cli.rs | 4 ++-- sim-cli/src/main.rs | 27 +++------------------------ sim-lib/src/lib.rs | 43 ++++++++++++++++++++++++++++++------------- 3 files changed, 35 insertions(+), 39 deletions(-) diff --git a/sim-cli/src/cli.rs b/sim-cli/src/cli.rs index 833e259b..3cfdfc74 100644 --- a/sim-cli/src/cli.rs +++ b/sim-cli/src/cli.rs @@ -34,7 +34,7 @@ pub const DEFAULT_LOG_LEVEL: &str = "info"; pub const DEFAULT_NO_RESULTS: bool = false; /// Default log interval -pub const DEFAULT_LOG_INTERVAL: u32 = 60; +pub const DEFAULT_LOG_INTERVAL: u64 = 60; /// Configuration header pub const CONFIG_HEADER: &str = "simln.conf"; @@ -145,7 +145,7 @@ pub struct Cli { default_value_t = DEFAULT_LOG_INTERVAL, value_parser = clap::builder::RangedU64ValueParser::::new().range(1..u32::MAX as u64) )] - log_interval: u32, + log_interval: u64, } /// Implementation of Cli with default values diff --git a/sim-cli/src/main.rs b/sim-cli/src/main.rs index d3b78f7d..fd6fbd1e 100644 --- a/sim-cli/src/main.rs +++ b/sim-cli/src/main.rs @@ -9,7 +9,7 @@ use anyhow::anyhow; use log::LevelFilter; use sim_lib::{ cln::ClnNode, lnd::LndNode, ActivityDefinition, LightningError, LightningNode, NodeConnection, NodeId, SimParams, - Simulation, WriteResults, + Simulation, }; mod cli; @@ -35,7 +35,7 @@ async fn main() -> anyhow::Result<()> { .build(), ); - let sim_path = read_sim_path(opts.data_dir.clone(), opts.sim_file).await?; + let sim_path = read_sim_path(opts.data_dir.clone(), opts.sim_file.clone()).await?; let SimParams { nodes, activity } = serde_json::from_str(&std::fs::read_to_string(sim_path)?).map_err(|e| { anyhow!( "Nodes or activities not parsed from simulation file (line {}, col {}).", @@ -135,23 +135,7 @@ async fn main() -> anyhow::Result<()> { }); } - let write_results = if !opts.no_results { - Some(WriteResults { - results_dir: mkdir(opts.data_dir.join("results")).await?, - batch_size: opts.print_batch_size, - }) - } else { - None - }; - - let sim = Simulation::new( - clients, - validated_activities, - opts.total_time, - opts.expected_pmt_amt, - opts.capacity_multiplier, - write_results, - ); + let sim = Simulation::new(clients, validated_activities, opts).await?; let sim2 = sim.clone(); ctrlc::set_handler(move || { @@ -207,8 +191,3 @@ async fn select_sim_file(data_dir: PathBuf) -> anyhow::Result { Ok(data_dir.join(sim_files[selection].clone())) } - -async fn mkdir(dir: PathBuf) -> anyhow::Result { - tokio::fs::create_dir_all(&dir).await?; - Ok(dir) -} diff --git a/sim-lib/src/lib.rs b/sim-lib/src/lib.rs index 702a066e..fe11baa5 100644 --- a/sim-lib/src/lib.rs +++ b/sim-lib/src/lib.rs @@ -335,6 +335,8 @@ pub struct Simulation { activity_multiplier: f64, /// Configurations for printing results to CSV. Results are not written if this option is None. write_results: Option, + /// Duration measuring how often the summary of results are logged. + log_interval: u64, } #[derive(Clone)] @@ -356,25 +358,35 @@ struct ExecutorKit { } impl Simulation { - pub fn new( + pub async fn new( nodes: HashMap>>, activity: Vec, - total_time: Option, - expected_payment_msat: u64, - activity_multiplier: f64, - write_results: Option, - ) -> Self { + config_options: SimulationConfig, + ) -> Result { let (shutdown_trigger, shutdown_listener) = triggered::trigger(); - Self { + let write_results = if !config_options.no_results { + Some(WriteResults { + results_dir: mkdir(config_options.data_dir.join("results")).await.map_err(|e| { + log::error!("Failed to create results directory. Error: {e}."); + SimulationError::FileError + })?, + batch_size: config_options.print_batch_size, + }) + } else { + None + }; + + Ok(Self { nodes, activity, shutdown_trigger, shutdown_listener, - total_time: total_time.map(|x| Duration::from_secs(x as u64)), - expected_payment_msat, - activity_multiplier, + total_time: config_options.total_time.map(|x| Duration::from_secs(x as u64)), + expected_payment_msat: config_options.expected_pmt_amt, + activity_multiplier: config_options.capacity_multiplier, write_results, - } + log_interval: config_options.log_interval, + }) } /// validate_activity validates that the user-provided activity description is achievable for the network that @@ -544,7 +556,7 @@ impl Simulation { tasks.spawn(run_results_logger( listener.clone(), result_logger.clone(), - Duration::from_secs(60), + Duration::from_secs(self.log_interval), )); tasks.spawn(consume_simulation_results( @@ -1050,5 +1062,10 @@ pub struct SimulationConfig { pub print_batch_size: u32, pub data_dir: PathBuf, pub sim_file: PathBuf, - pub log_interval: u32, + pub log_interval: u64, +} + +async fn mkdir(dir: PathBuf) -> anyhow::Result { + tokio::fs::create_dir_all(&dir).await?; + Ok(dir) } From 5f5e0753b9370fef99e26e6a8e159188035a70d6 Mon Sep 17 00:00:00 2001 From: Enigbe Ochekliye Date: Mon, 11 Mar 2024 13:51:23 +0100 Subject: [PATCH 7/7] docs: update README with note on config file --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index b63cc997..48dc372e 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,8 @@ Install the CLI: cargo install --locked --path sim-cli/ ``` +### Running the Simulation + To run the simulation, create a simulation file `sim.json` in the working directory (see [setup instructions](#simulation-file-setup) for details) and run: ``` @@ -49,6 +51,32 @@ sim-cli Run `sim-cli -h` for details on `--data-dir` and `--sim-file` options that allow specifying custom simulation file locations. +Additionally, a configuration file can be used to specify runtime options. To set this up, create an `INI` file in the project root directory + +```sh +$ touch conf.ini +``` + +Populate the `.ini` file with the options desired. For example: + +```ini +// conf.ini +[simln.conf] +data_dir = /path/to/custom/data_dir +sim_file = /path/to/custom/sim_file +... +``` + +The default configuration file is `conf.ini`. To explicitly specify it or any other configuration file, use the command line argument as shown below. + +```sh +$ sim-cli --config custom_config.ini +``` + +**Note**: The configuration keys used in the config file corresponds to the long format names of `sim-cli` options. + +### Contributing + Interested in contributing to the project? See [CONTRIBUTING](CONTRIBUTING.md) for more details. ### Simulation File Setup