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}