Skip to main content

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}