Skip to main content

hopr_transport_probe/
config.rs

1use serde::{Deserialize, Serialize};
2use validator::Validate;
3
4fn validate_interval_ge_timeout(config: &ProbeConfig) -> Result<(), validator::ValidationError> {
5    if config.interval < config.timeout {
6        let mut err = validator::ValidationError::new("interval_less_than_timeout");
7        err.message = Some(
8            format!(
9                "probe interval ({:?}) must be >= timeout ({:?}) to prevent overlapping rounds",
10                config.interval, config.timeout
11            )
12            .into(),
13        );
14        return Err(err);
15    }
16    Ok(())
17}
18
19/// Configuration for the probing mechanism
20#[derive(Debug, Clone, Copy, PartialEq, smart_default::SmartDefault, Validate, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22#[validate(schema(function = "validate_interval_ge_timeout"))]
23pub struct ProbeConfig {
24    /// The waiting time for a reply from the probe.
25    #[default(default_max_probe_timeout())]
26    #[serde(default = "default_max_probe_timeout", with = "humantime_serde")]
27    pub timeout: std::time::Duration,
28
29    /// Maximum number of parallel probes performed by the mechanism
30    #[validate(range(min = 1))]
31    #[default(default_max_parallel_probes())]
32    #[serde(default = "default_max_parallel_probes")]
33    pub max_parallel_probes: usize,
34
35    /// The delay between individual probing rounds for neighbor discovery.
36    ///
37    /// Must be >= `timeout` to prevent overlapping probe rounds, which causes
38    /// pseudonym reuse in the probe cache and missed pong responses.
39    #[serde(default = "default_probing_interval", with = "humantime_serde")]
40    #[default(default_probing_interval())]
41    pub interval: std::time::Duration,
42
43    /// The time threshold after which it is reasonable to recheck the nearest neighbor
44    #[serde(default = "default_recheck_threshold", with = "humantime_serde")]
45    #[default(default_recheck_threshold())]
46    pub recheck_threshold: std::time::Duration,
47}
48
49/// The maximum time waiting for a reply from the probe
50const DEFAULT_MAX_PROBE_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(3);
51
52/// The maximum number of parallel probes the heartbeat performs
53const DEFAULT_MAX_PARALLEL_PROBES: usize = 50;
54
55/// Delay before repeating probing rounds, must include enough time to traverse NATs
56const DEFAULT_REPEATED_PROBING_DELAY: std::time::Duration = std::time::Duration::from_secs(5);
57
58/// Time after which the availability of a node gets rechecked
59const DEFAULT_PROBE_RECHECK_THRESHOLD: std::time::Duration = std::time::Duration::from_secs(60);
60
61#[inline]
62const fn default_max_probe_timeout() -> std::time::Duration {
63    DEFAULT_MAX_PROBE_TIMEOUT
64}
65
66#[inline]
67const fn default_max_parallel_probes() -> usize {
68    DEFAULT_MAX_PARALLEL_PROBES
69}
70
71#[inline]
72const fn default_probing_interval() -> std::time::Duration {
73    DEFAULT_REPEATED_PROBING_DELAY
74}
75
76#[inline]
77const fn default_recheck_threshold() -> std::time::Duration {
78    DEFAULT_PROBE_RECHECK_THRESHOLD
79}
80
81#[cfg(test)]
82mod tests {
83    use anyhow::Context;
84    use validator::Validate;
85
86    use super::*;
87
88    #[test]
89    fn probe_config_default_is_valid() -> anyhow::Result<()> {
90        let cfg = ProbeConfig::default();
91        cfg.validate().context("default ProbeConfig should be valid")?;
92        assert!(cfg.interval >= cfg.timeout);
93        insta::assert_yaml_snapshot!(cfg);
94        Ok(())
95    }
96
97    #[test]
98    fn probe_config_zero_parallel_probes_is_rejected() -> anyhow::Result<()> {
99        let cfg = ProbeConfig {
100            max_parallel_probes: 0,
101            ..Default::default()
102        };
103        let err = cfg.validate().err().context("expected validation error")?;
104        anyhow::ensure!(
105            err.field_errors().contains_key("max_parallel_probes"),
106            "expected max_parallel_probes field error, got: {err}"
107        );
108        Ok(())
109    }
110
111    #[test]
112    fn probe_config_one_parallel_probe_is_valid() -> anyhow::Result<()> {
113        let cfg = ProbeConfig {
114            max_parallel_probes: 1,
115            ..Default::default()
116        };
117        cfg.validate().context("single parallel probe should be valid")?;
118        Ok(())
119    }
120
121    #[test]
122    fn interval_less_than_timeout_should_be_rejected() {
123        let config = ProbeConfig {
124            timeout: std::time::Duration::from_secs(10),
125            interval: std::time::Duration::from_secs(5),
126            ..Default::default()
127        };
128        assert!(config.validate().is_err(), "interval < timeout must be rejected");
129    }
130
131    #[test]
132    fn interval_equal_to_timeout_should_be_accepted() {
133        let config = ProbeConfig {
134            timeout: std::time::Duration::from_secs(5),
135            interval: std::time::Duration::from_secs(5),
136            ..Default::default()
137        };
138        assert!(config.validate().is_ok());
139    }
140}