majordomus_cli/mesh/provider.rs
1//! The provider contract: a discovery mechanism is a source of observations and nothing
2//! more. A provider watches one transport (a multicast group, a broadcast port, a
3//! rendezvous endpoint set), hands every datagram it hears to the manager as raw bytes,
4//! and answers for its own health. It parses nothing, verifies nothing, trusts nothing
5//! and holds no peers — the manager owns the one verification path and the one registry,
6//! which is what makes adding a provider a registration rather than a rewrite.
7//!
8//! ```
9//! use std::sync::Arc;
10//! use majordomus_cli::mesh::identity::NodeIdentity;
11//! use majordomus_cli::mesh::provider::Beacon;
12//!
13//! // The beacon signs this node's advertisements with one rising sequence, so every
14//! // transmitting provider shares the replay window.
15//! let identity = NodeIdentity::ephemeral().unwrap();
16//! let beacon = Beacon::new(Arc::new(identity), vec!["127.0.0.1:8741".into()],
17//! vec!["http".into()], vec![], "docs");
18//! let first = beacon.next_envelope();
19//! let second = beacon.next_envelope();
20//! assert!(second.adv.seq > first.adv.seq, "the sequence only rises");
21//! ```
22
23use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
24use std::sync::Arc;
25
26use schemars::JsonSchema;
27use serde::{Deserialize, Serialize};
28
29use super::identity::NodeIdentity;
30use super::protocol::{advertise_as, Envelope};
31use super::registry::MeshSource;
32use super::MeshError;
33
34/// One raw observation: where it came from, and the bytes as heard. Parsing and
35/// verification happen once, in the manager, for every provider alike.
36pub struct Observation {
37 /// The provider kind that heard it.
38 pub source: MeshSource,
39 /// The network path, as text: a sender address, a rendezvous URL.
40 pub path: String,
41 /// The datagram, unparsed and untrusted.
42 pub bytes: Vec<u8>,
43}
44
45/// What this node says about itself: the signer plus the advertised facts, with one
46/// monotonically increasing sequence shared by every transmitting provider — a reader
47/// deduplicates by (instance, seq) regardless of which transport delivered first.
48pub struct Beacon {
49 identity: Arc<NodeIdentity>,
50 runtime: String,
51 seq: AtomicU64,
52 endpoints: Vec<String>,
53 caps: Vec<String>,
54 repos: Vec<String>,
55 version: String,
56}
57
58impl Beacon {
59 /// A beacon over this node's identity and advertised facts, for the node's unnamed
60 /// runtime; [`Beacon::with_runtime`] names one.
61 pub fn new(
62 identity: Arc<NodeIdentity>,
63 endpoints: Vec<String>,
64 caps: Vec<String>,
65 repos: Vec<String>,
66 version: &str,
67 ) -> Self {
68 Beacon {
69 identity,
70 runtime: String::new(),
71 seq: AtomicU64::new(0),
72 endpoints,
73 caps,
74 repos,
75 version: version.into(),
76 }
77 }
78
79 /// The same beacon, announcing one runtime slot of the node (16 hex). A node may run a
80 /// server per checkout, and a listener that only heard the node could not tell those
81 /// apart or address one of them; naming the runtime is what makes an advertisement
82 /// answerable by the particular server that sent it. It is a builder step rather than
83 /// an argument to [`Beacon::new`] because a node with a single unnamed runtime is a
84 /// complete configuration.
85 ///
86 /// ```
87 /// use std::sync::Arc;
88 /// use majordomus_cli::mesh::identity::NodeIdentity;
89 /// use majordomus_cli::mesh::provider::Beacon;
90 ///
91 /// let identity = Arc::new(NodeIdentity::ephemeral().unwrap());
92 /// let bare = Beacon::new(identity.clone(), vec![], vec![], vec![], "docs");
93 /// assert_eq!(bare.runtime(), "", "a node may run one unnamed runtime");
94 ///
95 /// let named = Beacon::new(identity, vec![], vec![], vec![], "docs").with_runtime("0123456789abcdef");
96 /// assert_eq!(named.runtime(), "0123456789abcdef");
97 /// assert_eq!(named.next_envelope().adv.rt, "0123456789abcdef");
98 /// ```
99 pub fn with_runtime(mut self, runtime: &str) -> Self {
100 self.runtime = runtime.into();
101 self
102 }
103
104 /// The next signed envelope, sequence advanced.
105 pub fn next_envelope(&self) -> Envelope {
106 let seq = self.seq.fetch_add(1, Ordering::SeqCst) + 1;
107 advertise_as(
108 &self.identity,
109 &self.runtime,
110 seq,
111 &self.endpoints,
112 &self.caps,
113 &self.repos,
114 &self.version,
115 )
116 }
117
118 /// The public identity behind the beacon.
119 pub fn identity(&self) -> &NodeIdentity {
120 &self.identity
121 }
122
123 /// The runtime slot this beacon announces; empty for the unnamed runtime.
124 pub fn runtime(&self) -> &str {
125 &self.runtime
126 }
127
128 /// The `host:port` authorities this beacon puts into every advertisement it signs.
129 /// They are fixed when the beacon is built, so that every transport announces the same
130 /// reachability and a listener never has to reconcile two accounts of where one node
131 /// answers.
132 pub fn endpoints(&self) -> &[String] {
133 &self.endpoints
134 }
135}
136
137/// The provider's health, as every surface reports it.
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
139#[serde(rename_all = "snake_case")]
140pub enum MeshProviderState {
141 /// Threads up, socket bound (where the provider has one).
142 Running,
143 /// The provider could not start or died; `ProviderStatus::detail` says why. One
144 /// provider failing never stops another — failure isolation is the contract.
145 Failed,
146 /// Stopped by the manager.
147 Stopped,
148}
149
150/// One provider's status: state, why, and its counters.
151#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
152pub struct ProviderStatus {
153 /// The provider id: `udp_multicast`, `udp_broadcast`, `rendezvous`, `synthetic`.
154 pub id: String,
155 /// The state.
156 pub state: MeshProviderState,
157 /// Why, when `Failed`; what it watches, when `Running`.
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 pub detail: Option<String>,
160 /// Advertisements transmitted.
161 pub sent: u64,
162 /// Datagrams heard and handed to the manager.
163 pub received: u64,
164}
165
166/// A provider id is unique in a mesh, so it is both what a reader looks for and the
167/// identity that makes the status list total.
168impl crate::order::Ordered for ProviderStatus {
169 fn order_key(&self) -> crate::order::OrderKey<'_> {
170 crate::order::OrderKey::plain(&self.id, &self.id)
171 }
172}
173
174/// The counters a provider's threads share with its status.
175#[derive(Default)]
176pub struct Counters {
177 /// Advertisements transmitted.
178 pub sent: AtomicU64,
179 /// Datagrams heard.
180 pub received: AtomicU64,
181}
182
183/// How many observations wait for the manager at most. A flood beyond it is dropped at the
184/// provider and counted, so a segment shouting datagrams costs this process a bounded queue
185/// and one counter, never memory without end (I2133).
186pub const OBSERVATION_QUEUE: usize = 1024;
187
188/// The providers' half of the bounded queue to the manager: a send that would wait is a drop,
189/// counted in the counter the manager reports as `overflow`.
190///
191/// ```
192/// use majordomus_cli::mesh::provider::{Observation, ObservationSender};
193/// use majordomus_cli::mesh::MeshSource;
194/// let (tx, rx) = ObservationSender::bounded(1);
195/// let seen = || Observation { source: MeshSource::UdpMulticast, path: "x".into(), bytes: vec![] };
196/// assert!(tx.send(seen()));
197/// assert!(!tx.send(seen()), "the queue is full: dropped");
198/// assert_eq!(tx.dropped(), 1);
199/// assert!(rx.try_recv().is_ok());
200/// ```
201#[derive(Clone)]
202pub struct ObservationSender {
203 tx: std::sync::mpsc::SyncSender<Observation>,
204 dropped: Arc<AtomicU64>,
205}
206
207impl ObservationSender {
208 /// A queue of `capacity`, with a counter of its own.
209 ///
210 /// ```
211 /// use majordomus_cli::mesh::provider::{Observation, ObservationSender};
212 /// use majordomus_cli::mesh::MeshSource;
213 /// let (tx, rx) = ObservationSender::bounded(2);
214 /// assert!(tx.send(Observation { source: MeshSource::UdpMulticast, path: "a".into(), bytes: vec![] }));
215 /// assert!(rx.try_recv().is_ok());
216 /// ```
217 pub fn bounded(capacity: usize) -> (Self, std::sync::mpsc::Receiver<Observation>) {
218 Self::counting(capacity, Arc::new(AtomicU64::new(0)))
219 }
220
221 /// A queue of `capacity` whose drops land in `dropped`.
222 ///
223 /// ```
224 /// use majordomus_cli::mesh::provider::{Observation, ObservationSender};
225 /// use majordomus_cli::mesh::MeshSource;
226 /// use std::sync::{atomic::{AtomicU64, Ordering}, Arc};
227 /// let counter = Arc::new(AtomicU64::new(0));
228 /// let (tx, _rx) = ObservationSender::counting(0, Arc::clone(&counter));
229 /// tx.send(Observation { source: MeshSource::UdpMulticast, path: "a".into(), bytes: vec![] });
230 /// assert_eq!(counter.load(Ordering::Relaxed), 1, "a queue of none drops everything, counted");
231 /// ```
232 pub fn counting(
233 capacity: usize,
234 dropped: Arc<AtomicU64>,
235 ) -> (Self, std::sync::mpsc::Receiver<Observation>) {
236 let (tx, rx) = std::sync::mpsc::sync_channel(capacity);
237 (ObservationSender { tx, dropped }, rx)
238 }
239
240 /// Hand one observation over without waiting; false when it was dropped or nobody reads.
241 ///
242 /// ```
243 /// use majordomus_cli::mesh::provider::{Observation, ObservationSender};
244 /// use majordomus_cli::mesh::MeshSource;
245 /// let (tx, rx) = ObservationSender::bounded(1);
246 /// drop(rx);
247 /// assert!(!tx.send(Observation { source: MeshSource::UdpMulticast, path: "a".into(), bytes: vec![] }));
248 /// assert_eq!(tx.dropped(), 0, "nobody reading is not a drop");
249 /// ```
250 pub fn send(&self, observation: Observation) -> bool {
251 match self.tx.try_send(observation) {
252 Ok(()) => true,
253 Err(std::sync::mpsc::TrySendError::Full(_)) => {
254 self.dropped
255 .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
256 false
257 }
258 Err(std::sync::mpsc::TrySendError::Disconnected(_)) => false,
259 }
260 }
261
262 /// Observations dropped because the queue was full.
263 ///
264 /// ```
265 /// use majordomus_cli::mesh::provider::{Observation, ObservationSender};
266 /// use majordomus_cli::mesh::MeshSource;
267 /// let (tx, _rx) = ObservationSender::bounded(1);
268 /// assert_eq!(tx.dropped(), 0);
269 /// ```
270 pub fn dropped(&self) -> u64 {
271 self.dropped.load(std::sync::atomic::Ordering::Relaxed)
272 }
273}
274
275/// What a provider gets to work with: the channel to the manager, the shared stop flag,
276/// and the beacon it announces.
277pub struct ProviderContext {
278 /// Where observations go: a bounded queue whose overflow is counted.
279 pub tx: ObservationSender,
280 /// Set once by the manager; every provider thread ends at its next bounded wait.
281 pub stop: Arc<AtomicBool>,
282 /// What this node announces.
283 pub beacon: Arc<Beacon>,
284}
285
286/// A discovery mechanism. Implementing this — and handing an instance to the manager —
287/// is the whole registration: no consumer, surface or registry learns provider names.
288pub trait MeshProvider: Send {
289 /// The provider id, stable, snake_case: the word status listings and sighting
290 /// provenance carry for this mechanism.
291 fn id(&self) -> &'static str;
292 /// Start the provider's threads. An `Err` marks this provider `Failed` and starts
293 /// the others regardless.
294 fn start(&mut self, ctx: &ProviderContext) -> Result<(), MeshError>;
295 /// The current status: state, the reason or subject, and this provider's counters.
296 fn status(&self) -> ProviderStatus;
297}
298
299/// A deterministic small jitter in `0..bound` milliseconds, from OS entropy; zero when
300/// entropy is unavailable — jitter is a nicety, never a dependency.
301pub fn jitter_ms(bound: u64) -> u64 {
302 if bound == 0 {
303 return 0;
304 }
305 let mut b = [0u8; 8];
306 if getrandom::getrandom(&mut b).is_err() {
307 return 0;
308 }
309 u64::from_le_bytes(b) % bound
310}
311
312#[cfg(test)]
313mod tests {
314 use super::*;
315 use std::sync::Arc;
316
317 #[test]
318 fn the_beacon_signs_with_one_rising_sequence() {
319 let identity = crate::mesh::identity::NodeIdentity::ephemeral().unwrap();
320 let node = identity.public.node_id.clone();
321 let beacon = Beacon::new(Arc::new(identity), vec![], vec![], vec![], "test");
322 let a = beacon.next_envelope();
323 let b = beacon.next_envelope();
324 assert_eq!(a.adv.seq + 1, b.adv.seq);
325 assert_eq!(a.adv.node_id(), Some(node));
326 }
327
328 #[test]
329 fn a_full_queue_counts_its_drop_and_a_closed_one_does_not() {
330 let observation = || Observation {
331 source: MeshSource::UdpMulticast,
332 path: "a".into(),
333 bytes: vec![],
334 };
335 let (tx, rx) = ObservationSender::bounded(1);
336 assert!(tx.send(observation()));
337 assert!(!tx.send(observation()), "the queue holds one");
338 assert_eq!(tx.dropped(), 1);
339 drop(rx);
340 assert!(!tx.send(observation()));
341 assert_eq!(tx.dropped(), 1, "nobody reading is not a drop");
342 }
343
344 #[test]
345 fn jitter_stays_under_its_bound_and_zero_is_zero() {
346 assert_eq!(jitter_ms(0), 0);
347 for _ in 0..32 {
348 assert!(jitter_ms(50) < 50);
349 }
350 }
351}