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 {}