agent_uri_dht/config.rs
1//! Configuration for the simulated DHT.
2
3use std::num::NonZeroUsize;
4use std::time::Duration;
5
6/// Configuration for the simulated DHT.
7///
8/// Controls behavior such as replication factor, TTL, paging, and verification.
9#[derive(Debug, Clone)]
10pub struct SimulationConfig {
11 /// Maximum registrations at one capability path.
12 ///
13 /// This bounds the key an agent registers at, and only that key. A record
14 /// is materialized at every ancestor key as well, but it appears there
15 /// because of the path it chose rather than by choosing that key, so an
16 /// ancestor's occupancy is not charged to it. Bounding ancestors instead
17 /// would let whoever fills a popular top-level prefix decide whether an
18 /// unrelated agent may register anywhere beneath it (issue #54).
19 ///
20 /// An ancestor key therefore holds whatever its subtree holds, which is
21 /// the direct model's premise: it applies where one store owns the
22 /// namespace and a stored value has no practical size bound. A store that
23 /// does bound a value is required to shard instead, per SPECIFICATION.md
24 /// ยง6.2.
25 ///
26 /// The default of 1000 is therefore a simulator affordance, not a figure a
27 /// deployment can be sized against. A Kademlia overlay bounds a record at
28 /// the wire size a peer will accept, which for `libp2p-kad` is 1 to 27
29 /// registrations, which is exactly why `agent-uri-dht-libp2p` shards rather
30 /// than storing a subtree at one key. Code that handles
31 /// [`DhtError::KeyCapacityExceeded`] ports to it. A capacity chosen against
32 /// this number does not.
33 ///
34 /// [`DhtError::KeyCapacityExceeded`]: crate::DhtError::KeyCapacityExceeded
35 ///
36 /// Default: 1000
37 pub max_registrations_per_capability: usize,
38
39 /// Default TTL for registrations.
40 ///
41 /// Default: 1 hour
42 pub default_ttl: Duration,
43
44 /// Whether to verify attestations on registration.
45 ///
46 /// Set to false only for isolated indexing evaluations.
47 /// Default: true
48 pub verify_attestations: bool,
49
50 /// How many copies the simulator claims to hold.
51 ///
52 /// The simulator holds exactly one authoritative copy, so this is 1 and
53 /// every [`crate::Quorum`] resolves to 1 against it. It exists so that
54 /// quorum arithmetic is exercised rather than bypassed.
55 /// Default: 1
56 pub replication_factor: NonZeroUsize,
57
58 /// Maximum registrations returned in one lookup page.
59 ///
60 /// The simulator could return every match at once, but a distributed
61 /// backend cannot, so it pages deliberately. Callers written against an
62 /// unpaged simulator break on a real backend.
63 /// Default: 64
64 pub page_size: NonZeroUsize,
65
66 /// Whether expiry happens without being asked.
67 ///
68 /// When set, lookups omit lapsed records and writes evict them, so a store
69 /// in use stays clean without anyone calling
70 /// [`SimulatedDht::expire_stale`]. When clear, lapsed records stay visible
71 /// and stay stored until that call, which is what an evaluation measuring
72 /// how much of a namespace has gone stale needs.
73 ///
74 /// [`SimulatedDht::expire_stale`]: crate::SimulatedDht::expire_stale
75 ///
76 /// Default: true
77 pub auto_expire: bool,
78}
79
80impl Default for SimulationConfig {
81 fn default() -> Self {
82 Self {
83 max_registrations_per_capability: 1000,
84 default_ttl: Duration::from_hours(1),
85 verify_attestations: true,
86 replication_factor: NonZeroUsize::new(1).unwrap(),
87 page_size: NonZeroUsize::new(64).unwrap(),
88 auto_expire: true,
89 }
90 }
91}
92
93impl SimulationConfig {
94 /// Creates a new configuration with defaults.
95 #[must_use]
96 pub fn new() -> Self {
97 Self::default()
98 }
99
100 /// Sets the maximum registrations at one capability path.
101 #[must_use]
102 pub const fn with_max_registrations_per_capability(mut self, max: usize) -> Self {
103 self.max_registrations_per_capability = max;
104 self
105 }
106
107 /// Sets the default TTL.
108 #[must_use]
109 pub const fn with_default_ttl(mut self, ttl: Duration) -> Self {
110 self.default_ttl = ttl;
111 self
112 }
113
114 /// Enables or disables attestation verification.
115 #[must_use]
116 pub const fn with_verify_attestations(mut self, verify: bool) -> Self {
117 self.verify_attestations = verify;
118 self
119 }
120
121 /// Sets the number of copies the simulator claims to hold.
122 #[must_use]
123 pub const fn with_replication_factor(mut self, factor: NonZeroUsize) -> Self {
124 self.replication_factor = factor;
125 self
126 }
127
128 /// Sets the maximum number of registrations returned per lookup page.
129 #[must_use]
130 pub const fn with_page_size(mut self, page_size: NonZeroUsize) -> Self {
131 self.page_size = page_size;
132 self
133 }
134
135 /// Enables or disables automatic expiration.
136 #[must_use]
137 pub const fn with_auto_expire(mut self, auto_expire: bool) -> Self {
138 self.auto_expire = auto_expire;
139 self
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 #[test]
148 fn default_config() {
149 let config = SimulationConfig::default();
150 assert_eq!(config.max_registrations_per_capability, 1000);
151 assert_eq!(config.default_ttl, Duration::from_hours(1));
152 assert!(config.verify_attestations);
153 assert_eq!(config.replication_factor.get(), 1);
154 assert_eq!(config.page_size.get(), 64);
155 assert!(config.auto_expire);
156 }
157
158 #[test]
159 fn builder_pattern() {
160 let config = SimulationConfig::new()
161 .with_max_registrations_per_capability(10)
162 .with_default_ttl(Duration::from_mins(30))
163 .with_verify_attestations(true)
164 .with_page_size(NonZeroUsize::new(8).unwrap())
165 .with_auto_expire(false);
166
167 assert_eq!(config.max_registrations_per_capability, 10);
168 assert_eq!(config.default_ttl, Duration::from_mins(30));
169 assert!(config.verify_attestations);
170 assert_eq!(config.page_size.get(), 8);
171 assert!(!config.auto_expire);
172 }
173}