Skip to main content

dig_peer_selector/
config.rs

1//! [`SelectorConfig`] — **wiring only**, never behavior (SPEC.md §1.5, §5.6).
2//!
3//! The selector exposes NO settings that change its selection behavior. Every tradeoff (scoring
4//! weights, decay constants, saturation limits, the relayed penalty) is a *learned internal state*,
5//! not a knob. `SelectorConfig` therefore carries only pure wiring:
6//!
7//! - an injectable **clock** so tests drive time deterministically (no wall-clock reads inside the
8//!   pure decision path);
9//! - an optional **RNG seed** so exploration tie-breaks are reproducible (SPEC §4.4-F);
10//! - a **registry capacity** — a pure resource bound (SPEC §2.5), not a behavior knob.
11//!
12//! Adding a scoring weight / decay constant / saturation limit / relayed-penalty field here is a
13//! conformance failure (SPEC §5.6): such a quantity is learned, never configured.
14
15use std::sync::{
16    atomic::{AtomicU64, Ordering},
17    Arc,
18};
19
20/// An injectable source of "now" in unix seconds.
21///
22/// The selector never reads the wall clock directly — every timestamp it needs (registry
23/// `first_seen`, staleness for eviction) flows through a `ClockSource` so a test can advance time
24/// deterministically and the §8 harness is reproducible. Production wires [`ClockSource::system`];
25/// tests wire [`ClockSource::manual`].
26#[derive(Clone)]
27pub struct ClockSource {
28    inner: ClockInner,
29}
30
31#[derive(Clone)]
32enum ClockInner {
33    /// Reads the real system clock (unix seconds).
34    System,
35    /// A test-controlled clock the harness advances explicitly.
36    Manual(Arc<AtomicU64>),
37}
38
39impl ClockSource {
40    /// A clock backed by the real system time (unix seconds).
41    pub fn system() -> Self {
42        ClockSource {
43            inner: ClockInner::System,
44        }
45    }
46
47    /// A deterministic, test-controlled clock starting at `start` unix seconds.
48    ///
49    /// Advance it with [`ClockSource::advance`] / [`ClockSource::set`]. This is what the conformance
50    /// harness uses so time-dependent behavior (staleness, eviction age) is reproducible.
51    pub fn manual(start: u64) -> Self {
52        ClockSource {
53            inner: ClockInner::Manual(Arc::new(AtomicU64::new(start))),
54        }
55    }
56
57    /// The current time in unix seconds.
58    pub fn now(&self) -> u64 {
59        match &self.inner {
60            ClockInner::System => std::time::SystemTime::now()
61                .duration_since(std::time::UNIX_EPOCH)
62                .map(|d| d.as_secs())
63                .unwrap_or(0),
64            ClockInner::Manual(t) => t.load(Ordering::Relaxed),
65        }
66    }
67
68    /// Advance a manual clock by `secs`. No-op on a system clock.
69    pub fn advance(&self, secs: u64) {
70        if let ClockInner::Manual(t) = &self.inner {
71            t.fetch_add(secs, Ordering::Relaxed);
72        }
73    }
74
75    /// Set a manual clock to an absolute `secs`. No-op on a system clock.
76    pub fn set(&self, secs: u64) {
77        if let ClockInner::Manual(t) = &self.inner {
78            t.store(secs, Ordering::Relaxed);
79        }
80    }
81}
82
83impl Default for ClockSource {
84    fn default() -> Self {
85        ClockSource::system()
86    }
87}
88
89impl std::fmt::Debug for ClockSource {
90    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
91        match &self.inner {
92            ClockInner::System => f.write_str("ClockSource::System"),
93            ClockInner::Manual(t) => {
94                write!(f, "ClockSource::Manual({})", t.load(Ordering::Relaxed))
95            }
96        }
97    }
98}
99
100/// The default registry capacity — a pure resource bound (SPEC §2.5), NOT a behavior knob.
101///
102/// Large enough that a healthy node never sheds a useful peer under normal operation; when exceeded,
103/// the lowest-value entries are evicted (never a connected peer or one with a range in flight).
104pub const DEFAULT_REGISTRY_CAPACITY: usize = 4096;
105
106/// Wiring-only configuration for a [`crate::PeerSelector`] (SPEC §5.6).
107///
108/// Carries an injectable clock, an optional RNG seed for deterministic exploration tie-breaks, and a
109/// registry capacity bound. It MUST NOT carry scoring weights, decay constants, saturation limits, or
110/// a relayed penalty — those are learned (SPEC §1.5). Adding such a field is a conformance failure.
111#[derive(Clone, Debug)]
112pub struct SelectorConfig {
113    /// The injectable clock (tests drive time deterministically). Default: the system clock.
114    pub clock: ClockSource,
115    /// Seed for the exploration tie-break PRNG (SPEC §4.4-F). `None` derives a fixed default seed so
116    /// behavior is still deterministic across runs of the same registry + outcome stream.
117    pub rng_seed: Option<u64>,
118    /// The registry capacity — a resource bound (SPEC §2.5), not a behavior knob.
119    pub registry_capacity: usize,
120}
121
122impl Default for SelectorConfig {
123    fn default() -> Self {
124        SelectorConfig {
125            clock: ClockSource::system(),
126            rng_seed: None,
127            registry_capacity: DEFAULT_REGISTRY_CAPACITY,
128        }
129    }
130}
131
132impl SelectorConfig {
133    /// A config with a deterministic manual clock (starting at `start` unix seconds) and a fixed RNG
134    /// seed — the shape the §8 conformance harness uses for reproducibility.
135    pub fn deterministic(start: u64, seed: u64) -> Self {
136        SelectorConfig {
137            clock: ClockSource::manual(start),
138            rng_seed: Some(seed),
139            registry_capacity: DEFAULT_REGISTRY_CAPACITY,
140        }
141    }
142
143    /// The effective RNG seed (the configured seed, or a fixed default so runs are reproducible even
144    /// without an explicit seed — SPEC §4.4-F).
145    pub(crate) fn effective_seed(&self) -> u64 {
146        self.rng_seed.unwrap_or(0x5EED_D16C_0DE0_u64)
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use super::*;
153
154    #[test]
155    fn manual_clock_advances_and_sets() {
156        let c = ClockSource::manual(100);
157        assert_eq!(c.now(), 100);
158        c.advance(50);
159        assert_eq!(c.now(), 150);
160        c.set(10);
161        assert_eq!(c.now(), 10);
162    }
163
164    #[test]
165    fn system_clock_is_nonzero_and_ignores_manual_ops() {
166        let c = ClockSource::system();
167        assert!(c.now() > 1_600_000_000); // after 2020
168        c.advance(1000); // no-op on system clock
169        c.set(0); // no-op
170        assert!(c.now() > 1_600_000_000);
171    }
172
173    #[test]
174    fn default_config_uses_system_clock_and_default_capacity() {
175        let cfg = SelectorConfig::default();
176        assert_eq!(cfg.registry_capacity, DEFAULT_REGISTRY_CAPACITY);
177        assert!(cfg.rng_seed.is_none());
178        // effective seed is deterministic even without an explicit seed.
179        assert_eq!(
180            cfg.effective_seed(),
181            super::SelectorConfig::default().effective_seed()
182        );
183        assert_ne!(
184            cfg.effective_seed(),
185            0,
186            "the fixed default seed is deterministic + non-zero"
187        );
188    }
189
190    #[test]
191    fn deterministic_config_is_reproducible() {
192        let a = SelectorConfig::deterministic(1000, 42);
193        let b = SelectorConfig::deterministic(1000, 42);
194        assert_eq!(a.clock.now(), b.clock.now());
195        assert_eq!(a.effective_seed(), b.effective_seed());
196        assert_eq!(a.effective_seed(), 42);
197    }
198
199    #[test]
200    fn clock_debug_renders() {
201        assert!(format!("{:?}", ClockSource::system()).contains("System"));
202        assert!(format!("{:?}", ClockSource::manual(7)).contains('7'));
203    }
204}