Skip to main content

somatize_fabric_broker/
protocol.rs

1//! What a client and a broker say to each other, and in what order.
2//!
3//! Two enums, together because they are a single vocabulary. They are **not**
4//! called `Request` and `Answer`: a client holds both conversations at once,
5//! and two same-named types in scope is a rename at every use site.
6//!
7//! ```text
8//! → Hello { protocol, who }            once per session
9//! ← Welcome { protocol } | Refused(why)
10//!
11//! → Reach { host, needs }              once per host
12//! ← Met { path, good_for } | Unreachable(why)
13//!
14//! → Done { host }                      lets the rendezvous go
15//! ```
16//!
17//! Six messages, and the whole point of them is the **first field of the first
18//! one**. The wire next door needs no version because both sides are the same
19//! binary from the same `cargo build`; for a broker that day is the first, since
20//! the platform's is deployed by us and the client is installed by whoever
21//! installs it. **The rule is exact match**: a broker refuses a [`PROTOCOL`] it
22//! does not speak rather than guessing which half of a stranger's vocabulary it
23//! understands, and [`Reply::Welcome`] carries its own number so the refusal can
24//! say something useful.
25//!
26//! A caution against a promise this does not make: MessagePack through `serde`
27//! writes these positionally, so **adding a field is a version bump** and not a
28//! free extension. What the version buys is that the mismatch is a sentence at
29//! the greeting instead of a struct read off by one at three in the morning.
30//!
31//! [`Ask::Hello::who`] and [`Reply::Met::good_for`] are `Option` and the
32//! embedded broker leaves both `None`. They are here because the platform's
33//! broker adds policy and not mechanism, and that is only true if the slots the
34//! policy writes into already exist: without `good_for` a lease cannot be
35//! revoked without inventing a message, and without `who` an identity has
36//! nowhere to go. The policy itself is the platform's opinion and stays there.
37
38use crate::Path;
39use serde::{Deserialize, Serialize};
40use somatize_core::Host;
41use std::fmt;
42use std::time::Duration;
43
44/// The version of this vocabulary that this binary speaks.
45///
46/// One number for the whole conversation and not one per message: a client that
47/// understands `Reach` but not `Met` is not a client.
48pub const PROTOCOL: u16 = 1;
49
50/// What the client says.
51#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
52pub enum Ask {
53    /// Opens the session: which vocabulary I speak, and who I claim to be.
54    Hello {
55        /// Always [`PROTOCOL`]. First field of the first message on purpose —
56        /// it is the one thing that must be readable by a binary that disagrees
57        /// with this one about everything else.
58        protocol: u16,
59        /// Whatever the far side's policy needs in order to know who is asking.
60        /// Opaque here, and `None` everywhere there is no policy. Named `who`
61        /// because `as` is a keyword.
62        who: Option<Identity>,
63    },
64    /// Introduce me to this host.
65    ///
66    /// The name is the graph's — `w1`, `gpu-a` — and resolving it is the whole
67    /// job of a broker. A [`Host`] and not a string, so that the thing the
68    /// engine placed and the thing the broker resolves are the same type all
69    /// the way down.
70    Reach {
71        /// The name the graph gave it.
72        host: Host,
73        /// What this slice needs of whoever runs it.
74        needs: Needs,
75    },
76    /// I am done with this host: let the rendezvous go.
77    ///
78    /// Nothing is held by an embedded broker, so nothing is released. It is
79    /// here because without it the platform cannot tell a session that ended
80    /// from one that died, which is metering rather than politeness.
81    Done {
82        /// Which one.
83        host: Host,
84    },
85}
86
87/// What the broker answers.
88#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
89pub enum Reply {
90    /// The session is open, and this is the vocabulary I speak.
91    Welcome {
92        /// Mine, so a mismatch can be reported with both numbers in it.
93        protocol: u16,
94    },
95    /// No session, and here is why. Belongs to the session and not to a
96    /// rendezvous: after this there is no conversation.
97    Refused(String),
98    /// You two have been introduced. Go and talk; I am out of it.
99    Met {
100        /// How to reach them.
101        path: Path,
102        /// How long this rendezvous is good for, counting from when you read
103        /// it. `None` is *nobody is taking this back*, which is every broker
104        /// with no policy.
105        ///
106        /// A **duration and not an instant**: an `Instant` does not serialize
107        /// and means nothing off its own process, and a wall clock was already
108        /// ruled out next door — two machines on a cluster disagree by minutes,
109        /// so an expiry stamped there and read here would be a lease that
110        /// expires in the past.
111        good_for: Option<Duration>,
112    },
113    /// That host cannot be reached, and here is why. Belongs to the rendezvous:
114    /// the session survives it, and another host may well be fine.
115    Unreachable(String),
116}
117
118/// Who is asking, for whoever has an opinion about it.
119///
120/// A string, and this crate never looks inside: the same boundary as the wire's
121/// `runtime`. What a token is worth is the platform's business, and the day it
122/// is a signed something rather than a string, it is still bytes with a name.
123#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
124#[serde(transparent)]
125pub struct Identity(pub String);
126
127/// What a slice needs of whoever runs it.
128///
129/// **Empty, and named.** This is where *this wants a GPU with 40 GB* will go,
130/// and it goes nowhere until there is a queue that reads it — inventing fields
131/// now would be describing a matching policy nobody has written. What it buys
132/// empty is that the day it fills, `Reach` does not change shape.
133///
134/// A struct and not a unit so that filling it is an edit here rather than a
135/// change of kind at every construction site.
136#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
137pub struct Needs {}
138
139impl Ask {
140    /// A greeting from this binary, claiming nothing.
141    pub fn hello() -> Self {
142        Self::Hello {
143            protocol: PROTOCOL,
144            who: None,
145        }
146    }
147
148    /// A greeting from this binary, claiming to be somebody.
149    pub fn hello_as(who: Identity) -> Self {
150        Self::Hello {
151            protocol: PROTOCOL,
152            who: Some(who),
153        }
154    }
155
156    /// This message in bytes.
157    pub fn to_bytes(&self) -> Result<Vec<u8>, Unreadable> {
158        write(self)
159    }
160
161    /// And back from them.
162    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Unreadable> {
163        read(bytes)
164    }
165}
166
167impl Reply {
168    /// This message in bytes.
169    pub fn to_bytes(&self) -> Result<Vec<u8>, Unreadable> {
170        write(self)
171    }
172
173    /// And back from them.
174    pub fn from_bytes(bytes: &[u8]) -> Result<Self, Unreadable> {
175        read(bytes)
176    }
177
178    /// The answer to a greeting: [`Reply::Welcome`] if that is a vocabulary we
179    /// speak, and a [`Reply::Refused`] naming **both** numbers if it is not.
180    ///
181    /// Here and not in a broker because every broker owes the same answer, and
182    /// three of them writing this comparison separately is three chances to
183    /// write `>=` and accept a stranger.
184    pub fn to_greeting(spoken: u16) -> Self {
185        match spoken == PROTOCOL {
186            true => Self::Welcome { protocol: PROTOCOL },
187            false => Self::Refused(format!(
188                "this broker speaks version {PROTOCOL} of the protocol and the client speaks \
189                 {spoken}; there is no half of a vocabulary worth guessing at, so upgrade \
190                 whichever of the two is behind"
191            )),
192        }
193    }
194}
195
196fn write<T: Serialize>(what: &T) -> Result<Vec<u8>, Unreadable> {
197    rmp_serde::to_vec(what).map_err(|e| Unreadable(e.to_string()))
198}
199
200/// Reads one message, and **nothing may be left over**. Lifted from the wire
201/// deliberately: leftovers are as suspicious as missing bytes, no format checks
202/// it for you, and the two conversations failing the same way is one thing to
203/// learn instead of two.
204fn read<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, Unreadable> {
205    let mut rest = bytes;
206    let what: T = rmp_serde::from_read(&mut rest).map_err(|e| Unreadable(e.to_string()))?;
207    match rest.len() {
208        0 => Ok(what),
209        left => Err(Unreadable(format!("{left} bytes left over at the end"))),
210    }
211}
212
213/// These bytes are not the ones that were written: truncated, left over, or
214/// never a message at all.
215///
216/// A struct and not an enum, unlike the wire's, and the difference is the
217/// domain: there a message can also fail because a value only exists in its own
218/// process. Nothing in this conversation carries a value, so there is one way
219/// to fail and a closed set of one is a struct.
220#[derive(Debug, Clone, PartialEq, Eq)]
221pub struct Unreadable(String);
222
223impl Unreadable {
224    /// What went wrong.
225    pub fn message(&self) -> &str {
226        &self.0
227    }
228}
229
230impl fmt::Display for Unreadable {
231    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232        write!(f, "these are not the bytes that were written: {}", self.0)
233    }
234}
235
236impl std::error::Error for Unreadable {}