Skip to main content

agent_uri_dht/
options.rs

1//! Per-operation options for distributed DHT operations.
2
3use std::num::NonZeroUsize;
4use std::time::Duration;
5
6/// How many replicas must respond for an operation to be considered successful.
7///
8/// A DHT operation is answered by several nodes, and a caller trades latency
9/// against confidence by choosing how many of them to wait for.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum Quorum {
12    /// One replica suffices. Lowest latency, weakest guarantee.
13    One,
14    /// A majority of the replication factor.
15    Majority,
16    /// Every replica in the replication factor.
17    All,
18    /// An explicit number of replicas.
19    N(NonZeroUsize),
20}
21
22impl Quorum {
23    /// Resolves this quorum to a concrete replica count for a replication factor.
24    ///
25    /// The result is clamped to `replication_factor`, so `N` larger than the
26    /// replication factor is treated as `All` rather than being unsatisfiable.
27    #[must_use]
28    pub fn resolve(self, replication_factor: NonZeroUsize) -> usize {
29        let k = replication_factor.get();
30        match self {
31            Self::One => 1,
32            Self::Majority => k / 2 + 1,
33            Self::All => k,
34            Self::N(n) => n.get().min(k),
35        }
36    }
37}
38
39/// Options for operations that write to the DHT.
40#[derive(Debug, Clone, PartialEq, Eq)]
41pub struct WriteOptions {
42    /// Maximum time to wait before abandoning the operation.
43    pub timeout: Duration,
44    /// How many replicas must acknowledge the write.
45    pub quorum: Quorum,
46}
47
48impl WriteOptions {
49    /// Default write timeout.
50    pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(30);
51
52    /// Creates write options with the default timeout and quorum.
53    #[must_use]
54    pub const fn new() -> Self {
55        Self {
56            timeout: Self::DEFAULT_TIMEOUT,
57            quorum: Quorum::Majority,
58        }
59    }
60
61    /// Sets the timeout.
62    #[must_use]
63    pub const fn with_timeout(mut self, timeout: Duration) -> Self {
64        self.timeout = timeout;
65        self
66    }
67
68    /// Sets the quorum.
69    #[must_use]
70    pub const fn with_quorum(mut self, quorum: Quorum) -> Self {
71        self.quorum = quorum;
72        self
73    }
74}
75
76impl Default for WriteOptions {
77    /// Majority quorum, because a write that lands on one replica is lost to
78    /// the next churn event.
79    fn default() -> Self {
80        Self::new()
81    }
82}
83
84/// Options for operations that read from the DHT.
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct ReadOptions {
87    /// Maximum time to wait before abandoning the operation.
88    pub timeout: Duration,
89    /// How many replicas must answer before the result is returned.
90    pub quorum: Quorum,
91    /// Maximum number of DHT keys read to satisfy one lookup call.
92    ///
93    /// A broad capability path may be spread across several keys, so a lookup
94    /// can fan out. This bounds both the latency and the attacker-funded work
95    /// of a single call; a truncated result reports a cursor.
96    pub max_keys: usize,
97    /// Where to resume a previous lookup, if any.
98    pub cursor: Option<crate::Cursor>,
99}
100
101impl ReadOptions {
102    /// Default read timeout.
103    pub const DEFAULT_TIMEOUT: Duration = Duration::from_secs(10);
104
105    /// Default maximum keys read per lookup.
106    pub const DEFAULT_MAX_KEYS: usize = 16;
107
108    /// Creates read options with the defaults.
109    #[must_use]
110    pub const fn new() -> Self {
111        Self {
112            timeout: Self::DEFAULT_TIMEOUT,
113            quorum: Quorum::One,
114            max_keys: Self::DEFAULT_MAX_KEYS,
115            cursor: None,
116        }
117    }
118
119    /// Sets the timeout.
120    #[must_use]
121    pub const fn with_timeout(mut self, timeout: Duration) -> Self {
122        self.timeout = timeout;
123        self
124    }
125
126    /// Sets the quorum.
127    #[must_use]
128    pub const fn with_quorum(mut self, quorum: Quorum) -> Self {
129        self.quorum = quorum;
130        self
131    }
132
133    /// Sets the maximum number of DHT keys read per lookup.
134    #[must_use]
135    pub const fn with_max_keys(mut self, max_keys: usize) -> Self {
136        self.max_keys = max_keys;
137        self
138    }
139
140    /// Resumes a lookup from a cursor returned by a previous page.
141    #[must_use]
142    pub fn with_cursor(mut self, cursor: crate::Cursor) -> Self {
143        self.cursor = Some(cursor);
144        self
145    }
146}
147
148impl Default for ReadOptions {
149    /// `Quorum::One`, matching the usual DHT read: the first replica to answer
150    /// wins. Raise it when a stale or withheld record is a concern, since
151    /// records are signed but a replica can still answer with an older one.
152    fn default() -> Self {
153        Self::new()
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    fn k(n: usize) -> NonZeroUsize {
162        NonZeroUsize::new(n).unwrap()
163    }
164
165    #[test]
166    fn one_resolves_to_one() {
167        assert_eq!(Quorum::One.resolve(k(20)), 1);
168    }
169
170    #[test]
171    fn majority_is_strictly_more_than_half() {
172        assert_eq!(Quorum::Majority.resolve(k(20)), 11);
173        assert_eq!(Quorum::Majority.resolve(k(3)), 2);
174        assert_eq!(Quorum::Majority.resolve(k(1)), 1);
175    }
176
177    #[test]
178    fn all_resolves_to_the_replication_factor() {
179        assert_eq!(Quorum::All.resolve(k(20)), 20);
180    }
181
182    #[test]
183    fn explicit_n_is_honored_below_the_replication_factor() {
184        assert_eq!(Quorum::N(k(5)).resolve(k(20)), 5);
185    }
186
187    #[test]
188    fn explicit_n_clamps_to_the_replication_factor() {
189        // An unsatisfiable quorum would fail every write; clamping degrades to
190        // All instead, which is the strictest achievable request.
191        assert_eq!(Quorum::N(k(100)).resolve(k(20)), 20);
192    }
193
194    #[test]
195    fn write_defaults_to_majority() {
196        assert_eq!(WriteOptions::default().quorum, Quorum::Majority);
197        assert_eq!(WriteOptions::default().timeout, Duration::from_secs(30));
198    }
199
200    #[test]
201    fn read_defaults_to_one() {
202        let opts = ReadOptions::default();
203        assert_eq!(opts.quorum, Quorum::One);
204        assert_eq!(opts.max_keys, 16);
205        assert!(opts.cursor.is_none());
206    }
207
208    #[test]
209    fn builders_override_defaults() {
210        let w = WriteOptions::new()
211            .with_timeout(Duration::from_secs(5))
212            .with_quorum(Quorum::All);
213        assert_eq!(w.timeout, Duration::from_secs(5));
214        assert_eq!(w.quorum, Quorum::All);
215
216        let r = ReadOptions::new()
217            .with_timeout(Duration::from_secs(1))
218            .with_quorum(Quorum::Majority)
219            .with_max_keys(4);
220        assert_eq!(r.timeout, Duration::from_secs(1));
221        assert_eq!(r.quorum, Quorum::Majority);
222        assert_eq!(r.max_keys, 4);
223    }
224}