somatize_fabric_wire/protocol.rs
1//! What the two sides say to each other, and in what order.
2//!
3//! Two enums, together because they are a single vocabulary: a message on its
4//! own means nothing without the one that answers it.
5//!
6//! ```text
7//! → Hello { runtime, offering } once per session
8//! ← Ready "I have a catalog, go ahead"
9//! | Send "I do not have that artifact"
10//! | Refused(why)
11//!
12//! → Provision { bytes } only if it answered Send
13//! ← Ready | Refused(why)
14//!
15//! → Work { plan, input, known, keys, placement, memory } n times
16//! ← Saw(fact) any number, and not the end
17//! | Done { last, produced, keys } | Failed(why)
18//! ```
19//!
20//! `Saw` is the one answer that ends nothing, and it is why an execution on
21//! another machine is watchable while it happens. It needed no port and no
22//! second connection: between `Work` and `Done` the client is **already
23//! blocked** on this socket, and that idle direction is the whole mechanism.
24//! Where there is no connection — a study handed out of a folder — facts go to
25//! the store and whoever wants them scans, which is the same rule: facts follow
26//! whatever channel is already there.
27//!
28//! The `Hello` carries the artifact's **name** and not the artifact, so asking
29//! *do you have `sha256:abc…`?* is forty bytes. And the consequence is worth
30//! more than the saving: **the day a store exists the worker tries it before
31//! answering `Send`, and the protocol does not change a line.** It is git's
32//! `have`/`want` and `docker push`'s layer exchange.
33//!
34//! In bytes it is MessagePack through `serde`. An earlier version wrote them by
35//! hand, 470 lines, on the argument that a `#[derive(Serialize)]` would hide the
36//! decision about [`Value::Opaque`]; that was wrong, since an `Arc<dyn Any>`
37//! cannot be derived at all and the decision has to be written by hand either
38//! way. What the 470 lines bought was an unversioned format and an inspector.
39//! MessagePack rather than `postcard` or `bincode` was measured: `postcard`
40//! throws away the message of a custom serialization error — here the one
41//! explaining why an opaque value cannot travel — and `bincode` writes more
42//! bytes.
43//!
44//! An `Opaque` carries something that **only exists in this process**, so it
45//! fails on the way out, with the node and the host in front of you. Asked
46//! through [`Value::travels`], so the refusal is [`MessageError::Opaque`] and
47//! not whatever a serializer felt like saying; catching it at compile time
48//! cannot be done, since which value travels along an edge is a run-time matter.
49//! Mind the asymmetry: an [`Artifact`](crate::Artifact)'s bytes **do** cross
50//! unlooked-at, being a pile of bytes opaque by design, while an `Opaque` is a
51//! pointer into this process disguised as a value.
52//!
53//! And still no version: both sides are the same binary from the same
54//! `cargo build`. The day they stop being so, the place for one is the `Hello`,
55//! which already negotiates the client's *runtime*.
56
57use crate::{Label, Outcome};
58use serde::{Deserialize, Serialize};
59use somatize_core::{Device, Fact, Keys, Memory, NodeId, Placement, Plan, Value};
60use std::fmt;
61
62/// What the client says.
63///
64/// The `allow` is deliberate: `Work` is far bigger than `Hello`, and boxing it
65/// would buy an allocation on every message to save a few hundred bytes of
66/// stack on one that is about to be serialized anyway — and `Hello` is sent
67/// **once per session** while `Work` is the whole conversation.
68#[allow(clippy::large_enum_variant)]
69#[derive(Debug, Clone, PartialEq)]
70pub enum Request {
71 /// Opens the session: who I am and what I would provision you with.
72 Hello {
73 /// How the client identifies itself, so the worker can say no. Opaque
74 /// here; the [`Provision`](crate::Provision) reads it.
75 runtime: String,
76 /// The name of the artifact I bring, if I bring one. `None` means "you
77 /// already have your catalog", and neither kind can pretend to be the
78 /// other.
79 offering: Option<Label>,
80 },
81 /// Here is the artifact, since you asked for it.
82 Provision {
83 /// The bytes. Nobody here looks at them.
84 bytes: Vec<u8>,
85 },
86 /// Execute this.
87 Work {
88 /// What gets executed.
89 plan: Plan,
90 /// The graph's input.
91 input: Value,
92 /// What was produced on the client that this plan reads and does not
93 /// produce.
94 known: Vec<(NodeId, Value)>,
95 /// What each of those is called, so what runs here can name what it
96 /// produces and the chain of keys does not stop at the wire.
97 keys: Vec<(NodeId, Keys)>,
98 /// Where each node of this slice runs.
99 placement: Placement,
100 /// What is remembered about the nodes of this slice: what implements
101 /// each, which are settled, which are worth keeping.
102 memory: Memory,
103 },
104}
105
106/// What the worker answers.
107#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
108pub enum Answer {
109 /// Ready to work.
110 Ready,
111 /// I do not have that artifact: send it to me.
112 Send,
113 /// No, and here is why. It belongs to the session, not to a job: after this
114 /// there is no conversation.
115 Refused(String),
116 /// Done, with what it produced.
117 Done(Outcome),
118 /// What you sent failed over there. Text on purpose: what is needed here is
119 /// for whoever launched the run to **read** what happened.
120 Failed(String),
121 /// Something happened over there, and the work is **not** over.
122 ///
123 /// The only non-terminal answer there is, and it costs no second connection
124 /// because the client is already blocked reading this one. Last in the enum
125 /// on purpose — the variant's index is what goes on the wire.
126 Saw(Fact),
127}
128
129impl Request {
130 /// This message in bytes. Fails if some value cannot leave this process.
131 pub fn to_bytes(&self) -> Result<Vec<u8>, MessageError> {
132 if let Request::Work { input, known, .. } = self {
133 travelling(std::iter::once(input).chain(known.iter().map(|(_, value)| value)))?;
134 }
135 write(&Sending::from(self))
136 }
137
138 /// And back from them.
139 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MessageError> {
140 read::<Received>(bytes).map(Request::from)
141 }
142}
143
144impl Answer {
145 /// This message in bytes. Fails if the slice produced something over there
146 /// that cannot come back.
147 pub fn to_bytes(&self) -> Result<Vec<u8>, MessageError> {
148 if let Answer::Done(outcome) = self {
149 travelling(
150 std::iter::once(&outcome.last)
151 .chain(outcome.produced.iter().map(|(_, value)| value)),
152 )?;
153 }
154 write(self)
155 }
156
157 /// And back from them.
158 pub fn from_bytes(bytes: &[u8]) -> Result<Self, MessageError> {
159 read(bytes)
160 }
161}
162
163/// That every one of these can leave the process.
164fn travelling<'v>(values: impl Iterator<Item = &'v Value>) -> Result<(), MessageError> {
165 match values.into_iter().all(Value::travels) {
166 true => Ok(()),
167 false => Err(MessageError::Opaque),
168 }
169}
170
171fn write<T: Serialize>(what: &T) -> Result<Vec<u8>, MessageError> {
172 rmp_serde::to_vec(what).map_err(|e| MessageError::Malformed(e.to_string()))
173}
174
175/// Reads one message, and **nothing may be left over**: leftovers are as
176/// suspicious as missing bytes, and no format checks that for you.
177fn read<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, MessageError> {
178 let mut rest = bytes;
179 let what: T =
180 rmp_serde::from_read(&mut rest).map_err(|e| MessageError::Malformed(e.to_string()))?;
181 match rest.len() {
182 0 => Ok(what),
183 left => Err(MessageError::Malformed(format!(
184 "{left} bytes left over at the end"
185 ))),
186 }
187}
188
189/// What a request looks like on the wire.
190///
191/// A mirror of [`Request`], and not the type itself, because of the fields
192/// `serde` cannot decide on its own: the **placement** and the **memory**. Only
193/// what belongs to *this plan's* nodes travels — sending the whole thing would
194/// put on the wire where nodes that do not exist there run — and of the
195/// placement the **host** half does not travel at all, having done its job when
196/// it decided this slice would leave. `serde` sees one field at a time, so
197/// those transformations live here.
198///
199/// Two mirrors and not one so that sending copies nothing. **Their variants
200/// have to stay in the same order**: what goes on the wire is the index.
201#[allow(clippy::large_enum_variant)]
202#[derive(Serialize)]
203enum Sending<'a> {
204 Hello {
205 runtime: &'a str,
206 offering: Option<&'a Label>,
207 },
208 Provision {
209 bytes: &'a [u8],
210 },
211 Work {
212 plan: &'a Plan,
213 input: &'a Value,
214 known: &'a [(NodeId, Value)],
215 keys: &'a [(NodeId, Keys)],
216 devices: Vec<(&'a NodeId, &'a Device)>,
217 memory: Memory,
218 },
219}
220
221#[allow(clippy::large_enum_variant)]
222#[derive(Deserialize)]
223enum Received {
224 Hello {
225 runtime: String,
226 offering: Option<Label>,
227 },
228 Provision {
229 bytes: Vec<u8>,
230 },
231 Work {
232 plan: Plan,
233 input: Value,
234 known: Vec<(NodeId, Value)>,
235 keys: Vec<(NodeId, Keys)>,
236 devices: Vec<(NodeId, Device)>,
237 memory: Memory,
238 },
239}
240
241impl<'a> From<&'a Request> for Sending<'a> {
242 fn from(request: &'a Request) -> Self {
243 match request {
244 Request::Hello { runtime, offering } => Sending::Hello {
245 runtime,
246 offering: offering.as_ref(),
247 },
248 Request::Provision { bytes } => Sending::Provision { bytes },
249 Request::Work {
250 plan,
251 input,
252 known,
253 keys,
254 placement,
255 memory,
256 } => Sending::Work {
257 plan,
258 input,
259 known,
260 keys,
261 devices: devices_in(plan, placement),
262 memory: memory_in(plan, memory),
263 },
264 }
265 }
266}
267
268impl From<Received> for Request {
269 fn from(received: Received) -> Self {
270 match received {
271 Received::Hello { runtime, offering } => Request::Hello { runtime, offering },
272 Received::Provision { bytes } => Request::Provision { bytes },
273 Received::Work {
274 plan,
275 input,
276 known,
277 keys,
278 devices,
279 memory,
280 } => {
281 let mut placement = Placement::new();
282 for (id, device) in devices {
283 placement.place(id, device);
284 }
285 Request::Work {
286 plan,
287 input,
288 known,
289 keys,
290 placement,
291 memory,
292 }
293 }
294 }
295 }
296}
297
298/// The device of each node of this plan that has one.
299fn devices_in<'a>(plan: &'a Plan, placement: &'a Placement) -> Vec<(&'a NodeId, &'a Device)> {
300 plan.steps()
301 .filter_map(|step| placement.of(step.node).map(|device| (step.node, device)))
302 .collect()
303}
304
305/// What is remembered about each node of this plan, and about no other.
306///
307/// **Written out one fact at a time, which is a hole with a name on it**: a new
308/// thing to remember that is not added here does not fail — it simply stops
309/// being true on the other side of the wire, which is the worst way for
310/// anything to be wrong.
311fn memory_in(plan: &Plan, memory: &Memory) -> Memory {
312 let mut mine = Memory::new();
313 for id in plan.steps().map(|step| step.node) {
314 if let Some(what) = memory.identity_of(id) {
315 mine.identify(id.clone(), what);
316 }
317 if memory.is_frozen(id) {
318 mine.freeze(id.clone(), memory.state_of(id).map(str::to_string));
319 }
320 if memory.is_cached(id) {
321 mine.cache(id.clone(), memory.salt_of(id).map(str::to_string));
322 }
323 if memory.is_mapped(id) {
324 mine.map(id.clone());
325 }
326 if let Some(written) = memory.fingerprint_of(id) {
327 mine.written_as(id.clone(), written);
328 }
329 }
330 mine
331}
332
333/// Why a message could not be put on the wire, or taken off it.
334#[derive(Debug, Clone, PartialEq, Eq)]
335pub enum MessageError {
336 /// A [`Value::Opaque`] cannot leave its process.
337 Opaque,
338 /// These bytes are not the ones that were written: truncated, left over, or
339 /// never a message at all.
340 Malformed(String),
341}
342
343impl fmt::Display for MessageError {
344 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
345 match self {
346 Self::Opaque => f.write_str(
347 "an opaque value does not cross to another process: what it carries only \
348 exists in this one. If it has to travel, take it out of `Opaque` and send \
349 it as data",
350 ),
351 Self::Malformed(why) => write!(f, "these are not the bytes that were written: {why}"),
352 }
353 }
354}
355
356impl std::error::Error for MessageError {}