Skip to main content

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