Skip to content

Commit

Permalink
sim-lib: Allows amount and interval ranges in manual activity definition
Browse files Browse the repository at this point in the history
Amounts and intervals in activities can now be fixed values (e.g. 1000) or ranges (e.g.
[500, 1000]). If a range is provided, the value will be sampled uniformly at
random from it.
  • Loading branch information
sr-gi committed Nov 2, 2023
1 parent 3001a22 commit ec8670c
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 7 deletions.
72 changes: 65 additions & 7 deletions sim-lib/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use bitcoin::Network;
use csv::WriterBuilder;
use lightning::ln::features::NodeFeatures;
use lightning::ln::PaymentHash;
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
use std::fmt::{Display, Formatter};
Expand Down Expand Up @@ -86,6 +87,52 @@ pub struct SimParams {
pub activity: Vec<ActivityParser>,
}

/// Either a value or a range parsed from the simulation file.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ValueOrRange<T> {
Value(T),
Range(T, T),
}

impl<T> ValueOrRange<T>
where
T: std::cmp::PartialOrd + rand_distr::uniform::SampleUniform + Copy,
{
/// Get the enclosed value. If value is defined aa a range, sample from it uniformly at random.
pub fn value(&self) -> T {
match self {
ValueOrRange::Value(x) => *x,
ValueOrRange::Range(x, y) => {
let mut rng = rand::thread_rng();
rng.gen_range(*x..*y)
}
}
}

/// Whether this is a range or not
pub fn is_range(&self) -> bool {
matches!(self, ValueOrRange::Range(..))
}
}

impl<T> Display for ValueOrRange<T>
where
T: Display,
{
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
ValueOrRange::Value(x) => write!(f, "{x}"),
ValueOrRange::Range(x, y) => write!(f, "({x}-{y})"),
}
}
}

/// The payment amount in msat. Either a value or a range.
type Amount = ValueOrRange<u64>;
/// The interval of seconds between payments. Either a value or a range.
type Interval = ValueOrRange<u16>;

/// Data structure used to parse information from the simulation file. It allows source and destination to be
/// [NodeId], which enables the use of public keys and aliases in the simulation description.
#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -97,9 +144,10 @@ pub struct ActivityParser {
#[serde(with = "serializers::serde_node_id")]
pub destination: NodeId,
// The interval of the event, as in every how many seconds the payment is performed.
pub interval_secs: u16,
pub interval_secs: Interval,
// The amount of m_sat to used in this payment.
pub amount_msat: u64,
#[serde(with = "serializers::serde_value_or_range")]
pub amount_msat: Amount,
}

/// Data structure used internally by the simulator. Both source and destination are represented as [PublicKey] here.
Expand All @@ -111,9 +159,9 @@ pub struct ActivityDefinition {
// The destination of the payment.
pub destination: NodeInfo,
// The interval of the event, as in every how many seconds the payment is performed.
pub interval_secs: u16,
pub interval_secs: Interval,
// The amount of m_sat to used in this payment.
pub amount_msat: u64,
pub amount_msat: Amount,
}

#[derive(Debug, Error)]
Expand Down Expand Up @@ -762,7 +810,8 @@ async fn produce_events(
shutdown: Trigger,
listener: Listener,
) {
let interval = time::Duration::from_secs(act.interval_secs as u64);
let mut interval = time::Duration::from_secs(act.interval_secs.value() as u64);
let mut amt = act.amount_msat.value();

log::debug!(
"Started producer for {} every {}s: {} -> {}.",
Expand All @@ -776,8 +825,17 @@ async fn produce_events(
tokio::select! {
biased;
_ = time::sleep(interval) => {
// Consumer was dropped
if sender.send(SimulationEvent::SendPayment(act.destination.clone(), act.amount_msat)).await.is_err() {
// Resample if needed
if act.interval_secs.is_range() {
interval = time::Duration::from_secs(act.interval_secs.value() as u64);
log::debug!("Resampling interval. New value: {}", interval.as_secs());
}
if act.amount_msat.is_range() {
amt = act.amount_msat.value();
log::debug!("Resampling payment amount. New value: {}", amt);
}
if sender.send(SimulationEvent::SendPayment(act.destination.clone(), amt)).await.is_err() {
// Consumer was dropped
log::debug!(
"Stopped producer for {}: {} -> {}. Consumer cannot be reached.",
act.amount_msat,
Expand Down
36 changes: 36 additions & 0 deletions sim-lib/src/serializers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,42 @@ pub mod serde_node_id {
}
}

pub mod serde_value_or_range {
use super::*;
use serde::de::Error;

use crate::ValueOrRange;

pub fn serialize<S, T>(x: &ValueOrRange<T>, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
T: std::fmt::Display,
{
serializer.serialize_str(&match x {
ValueOrRange::Value(p) => p.to_string(),
ValueOrRange::Range(x, y) => format!("[{}, {}]", x, y),
})
}

pub fn deserialize<'de, D, T>(deserializer: D) -> Result<ValueOrRange<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::Deserialize<'de> + std::cmp::PartialOrd + std::fmt::Display + Copy,
{
let a = ValueOrRange::deserialize(deserializer)?;
if let ValueOrRange::Range(x, y) = a {
if x >= y {
return Err(D::Error::custom(format!(
"Cannot parse range. Ranges must be strictly increasing (i.e. [x, y] with x > y). Received [{}, {}]",
x, y
)));
}
}

Ok(a)
}
}

pub fn deserialize_path<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
Expand Down

0 comments on commit ec8670c

Please sign in to comment.