Skip to main content

somatize_fabric_wire/
serve.rs

1//! The far side: serving whatever arrives, over standard input or a port.
2//!
3//! Two axes, and they do not mix. **Where it listens**:
4//! [`Serving::over_stdin`] talks to a client that started it, and
5//! [`Serving::listen`] opens a port and serves whoever comes, which is what
6//! makes a worker an independent process. **Where it gets what it executes**:
7//!
8//! | | where it gets it | who uses it |
9//! |---|---|---|
10//! | [`Serving::own`] | it brings it | a Rust binary with its nodes |
11//! | [`Serving::provisioned`] | the client sends it | the generic worker: `pip install` and nothing else |
12//!
13//! Two constructors and not an optional parameter because they reject different
14//! things: offering the first an artifact is an error, and not offering the
15//! second one is too. An `Option` would have turned both into a branch that
16//! gets forgotten.
17//!
18//! What arrives is **cached by the artifact's id**, so a second run resends
19//! nothing. One is kept and not a map: collecting Python catalogs would be
20//! collecting live objects with nobody saying when they are released.
21//!
22//! **One thread per conversation.** The first version served connections one at
23//! a time and deadlocked: two branches of a wave against one worker open two
24//! connections, the second sits in the `accept` queue, and the first does not
25//! release its own until the `forward` finishes. Serializing was right, but at
26//! **message** granularity and not session granularity.
27//!
28//! `stdout` **is** the wire, so not one `println!` in a worker; for talking
29//! there is `stderr`, which [`Worker`](crate::Worker) leaves inherited. In
30//! Python this is more dangerous, because a stray `print` in a user's node — or
31//! in a library on import — does the same thing.
32
33use crate::frame;
34use crate::machine::{self, Machine};
35use crate::{Answer, Label, Provision, Provisioned, Request};
36use somatize_core::{Catalog, Executor, Fact, Keeper, Outcome, Watcher};
37use somatize_core::{Codec, unpacked_all};
38use somatize_store::{Store, StoreError};
39use std::io::{self, BufReader, Read, Write};
40use std::net::{SocketAddr, TcpListener, ToSocketAddrs};
41use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
42use std::sync::{Mutex, MutexGuard};
43use std::time::{Duration, Instant};
44
45/// A worker about to serve: what it executes with, and where it listens.
46pub struct Serving<'a> {
47    source: Source<'a>,
48    store: Option<&'a dyn Store>,
49    keeper: Option<&'a dyn Keeper>,
50    codec: Option<&'a dyn Codec>,
51    every: Option<Duration>,
52}
53
54impl<'a> Serving<'a> {
55    /// A worker that brings its own catalog.
56    pub fn own(catalog: &'a Catalog) -> Self {
57        Self {
58            source: Source::Own(catalog),
59            store: None,
60            every: None,
61            keeper: None,
62            codec: None,
63        }
64    }
65
66    /// A worker that starts **empty** and is sent what to execute, with this to
67    /// interpret it.
68    pub fn provisioned(provision: &'a dyn Provision) -> Self {
69        Self {
70            source: Source::Sent(provision),
71            store: None,
72            every: None,
73            keeper: None,
74            codec: None,
75        }
76    }
77
78    /// The same worker, with somewhere to keep the artifacts it is sent.
79    ///
80    /// The `have`/`want` finally having a `have`: offered an artifact it already
81    /// has **in the store**, it says `Ready` and not a byte crosses. A shared
82    /// folder means the second worker stood up is provisioned without the
83    /// client noticing.
84    pub fn store(mut self, store: &'a dyn Store) -> Self {
85        self.store = Some(store);
86        self
87    }
88
89    /// Writes a reading of this machine to the store this often, whether or not
90    /// anybody is asking it to do anything.
91    ///
92    /// The idle half, and the pipe is CU20's rule: *where a connection is open,
93    /// facts come back down it; where there is none, they go to the store*. An
94    /// idle worker's connection is one **nobody is reading**, so beating down it
95    /// would fill a buffer nobody drains and hand over the **oldest** beats
96    /// whenever somebody finally looked.
97    ///
98    /// Off unless asked for, and it does nothing without a
99    /// [`store`](Serving::store) to write to.
100    pub fn reporting(mut self, every: Duration) -> Self {
101        self.every = Some(every);
102        self
103    }
104
105    /// The same worker, able to keep what the slices it runs produce.
106    ///
107    /// Separate from [`Serving::store`] and not derived from it: that one keeps
108    /// **artifacts**, so a worker is not sent a catalog it already has; this one
109    /// keeps **values**, so a node whose answer is already known is not run at
110    /// all. Both can be the same directory underneath.
111    ///
112    /// What is remembered about each node does not come from here: it arrives
113    /// with the work, because it belongs to the graph.
114    pub fn keeping(mut self, keeper: &'a dyn Keeper) -> Self {
115        self.keeper = Some(keeper);
116        self
117    }
118
119    /// The same worker, able to read and write down what only exists in a
120    /// process.
121    ///
122    /// **The same codecs as the client's**, or the two ends do not understand
123    /// each other — which is what the error says when it happens. Whoever stands
124    /// this worker up installs it; it does not travel.
125    pub fn packing(mut self, codec: &'a dyn Codec) -> Self {
126        self.codec = Some(codec);
127        self
128    }
129
130    /// Serves over standard input, until the client closes.
131    ///
132    /// Returns `Ok(())` when the input ends **between** messages, which is how
133    /// it normally finishes. A failure of what gets executed is not an error
134    /// here: it travels back as an answer.
135    pub fn over_stdin(self) -> io::Result<()> {
136        let every = self.every;
137        let shared = Shared::of(self);
138        let stop = AtomicBool::new(false);
139        // The handle and not the lock guard: a `StdoutLock` is not `Send`, and
140        // what writes down this pipe is now also whatever is watching the run,
141        // from a wave's threads. `attend` holds its own lock over it.
142        std::thread::scope(|scope| {
143            if let Some(every) = every {
144                let (shared, stop) = (&shared, &stop);
145                scope.spawn(move || reporting(shared, every, stop));
146            }
147            let said = attend(&shared, io::stdin().lock(), io::stdout());
148            // A pipe ends when the client goes, and the scope will not return
149            // while the clock is still ticking in it.
150            stop.store(true, Ordering::Relaxed);
151            said
152        })
153    }
154
155    /// Stands on `addr` and serves whoever connects. It does not return: it
156    /// stops by being shut down, and a client that cuts out does not stop it.
157    pub fn listen(self, addr: impl ToSocketAddrs) -> io::Result<()> {
158        self.listen_at(addr, |_| {})
159    }
160
161    /// The same, reporting which address it ended up open on, so port `0` can
162    /// be asked for.
163    pub fn listen_at(
164        self,
165        addr: impl ToSocketAddrs,
166        opened: impl FnOnce(SocketAddr),
167    ) -> io::Result<()> {
168        let listener = TcpListener::bind(addr)?;
169        opened(listener.local_addr()?);
170
171        let every = self.every;
172        let shared = Shared::of(self);
173        let shared = &shared;
174        let stop = AtomicBool::new(false);
175        let stop = &stop;
176
177        std::thread::scope(|scope| {
178            if let Some(every) = every {
179                scope.spawn(move || reporting(shared, every, stop));
180            }
181            let mut alive: Vec<std::thread::ScopedJoinHandle<'_, ()>> = Vec::new();
182            for arrival in listener.incoming() {
183                let socket = match arrival {
184                    Ok(socket) => socket,
185                    // An `accept` that fails is noted, and we keep listening.
186                    Err(e) => {
187                        eprintln!("could not accept a connection: {e}");
188                        continue;
189                    }
190                };
191                let _ = socket.set_nodelay(true);
192                // Or a months-old worker accumulates one handle per client served.
193                alive.retain(|thread| !thread.is_finished());
194                alive.push(scope.spawn(move || {
195                    let Ok(copy) = socket.try_clone() else {
196                        return;
197                    };
198                    if let Err(e) = attend(shared, BufReader::new(copy), socket) {
199                        eprintln!("a session was cut off: {e}");
200                    }
201                }));
202            }
203            // The listener only ends if it broke, and the scope will not
204            // return while the reporting thread is still in it.
205            stop.store(true, Ordering::Relaxed);
206        });
207        Ok(())
208    }
209}
210
211/// What every session on this worker has in common. One per [`Serving`], lent
212/// to each session, so the catalog that arrives serves the next client too.
213struct Shared<'a> {
214    source: Source<'a>,
215    store: Option<&'a dyn Store>,
216    keeper: Option<&'a dyn Keeper>,
217    codec: Option<&'a dyn Codec>,
218    loaded: Mutex<Loaded<'a>>,
219    /// When this **process** came up, and how much it has run in total.
220    ///
221    /// Here and not on a `Session`, which is one client's conversation: an
222    /// uptime that restarted whenever somebody reconnected would be a figure of
223    /// connections dressed as a figure of machines.
224    since: Instant,
225    served: AtomicU64,
226}
227
228impl<'a> Shared<'a> {
229    fn of(serving: Serving<'a>) -> Self {
230        let loaded = match &serving.source {
231            Source::Own(catalog) => Loaded::Own(catalog),
232            Source::Sent(_) => Loaded::Empty,
233        };
234        Self {
235            source: serving.source,
236            store: serving.store,
237            keeper: serving.keeper,
238            codec: serving.codec,
239            loaded: Mutex::new(loaded),
240            since: Instant::now(),
241            served: AtomicU64::new(0),
242        }
243    }
244
245    /// A reading of this machine right now.
246    fn reading(&self) -> Machine {
247        Machine::here(self.since.elapsed(), self.served.load(Ordering::Relaxed))
248    }
249
250    /// What is inside, even if another session broke: a poisoned `Mutex` holds
251    /// a catalog, not a half-finished invariant.
252    fn held(&self) -> MutexGuard<'_, Loaded<'a>> {
253        match self.loaded.lock() {
254            Ok(inside) => inside,
255            Err(poisoned) => poisoned.into_inner(),
256        }
257    }
258}
259
260/// What one client's conversation remembers between messages. Per session, so a
261/// client that cuts out leaves nothing behind.
262#[derive(Default)]
263struct Session {
264    /// What was asked for, between the `Send` and the `Provision`, which
265    /// arrives without its name.
266    awaiting: Option<(String, String)>,
267    /// The artifact this client greeted with, if it brought one. What it gets
268    /// checked against is [`Shared::loaded`], on every job.
269    mine: Option<String>,
270}
271
272/// Where this worker's catalog comes from.
273enum Source<'a> {
274    /// It brings it.
275    Own(&'a Catalog),
276    /// The client sends it, and this knows how to interpret it.
277    Sent(&'a dyn Provision),
278}
279
280/// What this worker can execute right now.
281enum Loaded<'a> {
282    /// Nothing yet: nobody has greeted.
283    Empty,
284    /// The one it brought.
285    Own(&'a Catalog),
286    /// One that arrived, and which artifact it came from.
287    Sent { id: String, catalog: Catalog },
288}
289
290impl Loaded<'_> {
291    /// The catalog to execute with right now, if there is one.
292    fn ready(&self) -> Option<Catalog> {
293        match self {
294            Self::Empty => None,
295            Self::Own(catalog) => Some((*catalog).clone()),
296            Self::Sent { catalog, .. } => Some(catalog.clone()),
297        }
298    }
299}
300
301/// A watcher that puts what it saw back down the same connection.
302///
303/// The far half of the live view, and it decides nothing: a fact goes out
304/// exactly as the engine here emitted it, and the client is the one that says
305/// it came from this host — because the name of this host is the graph's, and a
306/// worker does not know it.
307///
308/// Behind a `Mutex` because a [`Wave`](somatize_core::Plan::Wave) emits from
309/// several threads at once, and half a frame interleaved with half of another
310/// is a connection that cannot be resynchronised.
311struct Relaying<'a, W: Write + Send> {
312    to: &'a Mutex<W>,
313}
314
315impl<W: Write + Send> Watcher for Relaying<'_, W> {
316    fn saw(&self, fact: &Fact) {
317        // Nothing here can be reported and nothing here should stop the run: a
318        // fact that cannot be written means the connection is gone, and the
319        // answer about to be sent down it will say so properly.
320        let Ok(encoded) = Answer::Saw(fact.clone()).to_bytes() else {
321            return;
322        };
323        if let Ok(mut out) = self.to.lock() {
324            let _ = frame::send(&mut *out, &encoded);
325        }
326    }
327}
328
329/// Writes a reading of this machine to the store on a clock, until told to stop.
330///
331/// The idle half. There is no name for a machine here — `w1` is the client's
332/// word and there is no client — so it files under what the machine calls
333/// itself, and whoever reads joins the two by seeing the same `id` on a reading
334/// that **did** come down a wire.
335///
336/// One name, rewritten. The store stamps every write, so a reading that has not
337/// moved is a machine that has stopped, and finding that out is a scan with no
338/// fetches.
339fn reporting(shared: &Shared<'_>, every: Duration, stop: &AtomicBool) {
340    let Some(store) = shared.store else {
341        return;
342    };
343    while !stop.load(Ordering::Relaxed) {
344        let reading = shared.reading();
345        let said = reading.said();
346        let (kind, mut meta) = said.flattened();
347        meta.insert(0, ("fact".into(), kind.to_string()));
348        // The whole of it is in the record and the blob has nothing to add,
349        // which is what the price list is for: this costs a scan to read and
350        // never a fetch.
351        if let Ok(digest) = store.put(&[]) {
352            // A store that will not take it is not something to stop serving
353            // over, and there is nobody to tell: a worker's job is the work.
354            let _ = store.bind(&machine::filed(&reading.id), &digest, meta);
355        }
356        // Slept in slices so shutting down does not wait out the interval.
357        let mut left = every;
358        while left > Duration::ZERO && !stop.load(Ordering::Relaxed) {
359            let nap = left.min(Duration::from_millis(100));
360            std::thread::sleep(nap);
361            left -= nap;
362        }
363    }
364}
365
366/// The loop, with both ends as arguments so it is testable without a process.
367fn attend(shared: &Shared<'_>, mut input: impl Read, output: impl Write + Send) -> io::Result<()> {
368    let mut session = Session::default();
369    // Shared with whatever is watching the run, which writes down the same
370    // socket while the answer is still being worked out.
371    let output = Mutex::new(output);
372
373    while let Some(payload) = frame::recv(&mut input)? {
374        let answer = match Request::from_bytes(&payload) {
375            Err(e) => Answer::Refused(e.to_string()),
376            Ok(request) => reply(shared, &mut session, request, &output),
377        };
378        let encoded = answer.to_bytes().unwrap_or_else(|e| {
379            // If not even the answer can be encoded, that fact is the answer:
380            // staying quiet would leave the other side waiting forever.
381            Answer::Failed(e.to_string())
382                .to_bytes()
383                .expect("text can always be written")
384        });
385        let mut out = output
386            .lock()
387            .map_err(|_| io::Error::other("this connection was poisoned by an earlier panic"))?;
388        frame::send(&mut *out, &encoded)?;
389    }
390    Ok(())
391}
392
393fn reply<W: Write + Send>(
394    shared: &Shared<'_>,
395    session: &mut Session,
396    request: Request,
397    output: &Mutex<W>,
398) -> Answer {
399    match request {
400        Request::Hello { runtime, offering } => match (&shared.source, offering) {
401            // A worker with its own catalog, and a client that brings nothing.
402            (Source::Own(_), None) => Answer::Ready,
403            (Source::Own(_), Some(label)) => Answer::Refused(format!(
404                "this worker already brings its catalog and does not accept a `{}` artifact",
405                label.kind
406            )),
407            (Source::Sent(_), None) => Answer::Refused(
408                "this worker starts empty and the client brings nothing to provision it with"
409                    .into(),
410            ),
411            (Source::Sent(provision), Some(label)) => {
412                if let Err(e) = provision.accepts(&runtime, &label.kind) {
413                    return Answer::Refused(e.to_string());
414                }
415                session.mine = Some(label.id.clone());
416                // The `have`/`want`. Already open with this artifact, or in the
417                // store, means not a byte crosses.
418                if matches!(&*shared.held(), Loaded::Sent { id, .. } if *id == label.id) {
419                    return Answer::Ready;
420                }
421                match kept(shared.store, *provision, &label) {
422                    Err(e) => Answer::Refused(e),
423                    Ok(Some(provisioned)) => {
424                        *shared.held() = Loaded::Sent {
425                            id: label.id,
426                            catalog: provisioned.catalog,
427                        };
428                        Answer::Ready
429                    }
430                    Ok(None) => {
431                        session.awaiting = Some((label.kind, label.id));
432                        Answer::Send
433                    }
434                }
435            }
436        },
437        Request::Provision { bytes } => {
438            let Source::Sent(provision) = &shared.source else {
439                return Answer::Refused("this worker is not provisioned".into());
440            };
441            let Some((kind, id)) = session.awaiting.take() else {
442                return Answer::Refused("an artifact arrived that nobody had asked for".into());
443            };
444            match provision.provide(&kind, &bytes) {
445                Ok(provisioned) => {
446                    // Kept for the next worker to be stood up, and for this one
447                    // if it is restarted. A failure to keep it is not a failure
448                    // to work: it will just be sent again.
449                    if let Some(Err(e)) = shared.store.map(|store| keep(store, &kind, &id, &bytes))
450                    {
451                        eprintln!("the artifact could not be kept: {e}");
452                    }
453                    *shared.held() = Loaded::Sent {
454                        id,
455                        catalog: provisioned.catalog,
456                    };
457                    Answer::Ready
458                }
459                Err(e) => Answer::Refused(e.to_string()),
460            }
461        }
462        Request::Work {
463            plan,
464            input,
465            known,
466            keys,
467            placement,
468            memory,
469        } => {
470            // What this machine looks like, said **before** the work rather
471            // than after: a reading taken once the slice is over is a reading
472            // of a machine that has just stopped, and the question is what it
473            // was like while it was asked.
474            shared.served.fetch_add(1, Ordering::Relaxed);
475            Relaying { to: output }.saw(&shared.reading().said());
476
477            // The lock is released before executing — a `Catalog` clones by
478            // `Arc` — or every client would serialize against the run.
479            let ready = {
480                let loaded = shared.held();
481                match (&session.mine, &*loaded) {
482                    // A worker holds **one** catalog. If somebody else
483                    // provisioned it with another artifact after this client
484                    // greeted, executing now would run their implementations —
485                    // and an id that exists in both would do it in silence.
486                    // Checked here because this is where it can go wrong.
487                    (Some(mine), Loaded::Sent { id, .. }) if mine != id => {
488                        return Answer::Failed(format!(
489                            "this worker was provisioned with `{id}` after you greeted \
490                             with `{mine}`, and it holds one catalog: reconnect, and \
491                             stand up a second worker if both are needed at once"
492                        ));
493                    }
494                    _ => loaded.ready(),
495                }
496            };
497            let Some(catalog) = ready else {
498                return Answer::Failed(
499                    "this worker has no catalog yet: work arrived before the greeting".into(),
500                );
501            };
502            // What is remembered arrived with the work and is fed in whether or
503            // not there is anywhere to keep things here: it is the graph's, and a
504            // slice that carries on to a third host has to take it along.
505            let relaying = Relaying { to: output };
506            let mut executor = Executor::new(&catalog)
507                .placed(&placement)
508                .remembering(&memory)
509                // The engine here is told exactly what the engine at home is
510                // told, and knows no more about where it ends up. That it ends
511                // up on a socket is this file's secret.
512                .watching(&relaying);
513            if let Some(keeper) = shared.keeper {
514                executor = executor.keeping(keeper);
515            }
516            // Alive again before anything reads it, and not at the boundary
517            // where a node is handed its argument: a value that only passes
518            // through here is never handed to anybody, and the two ends have to
519            // be the same one.
520            let (input, known) = match shared.codec {
521                None => (input, known),
522                Some(codec) => match (codec.unpacked(&input), unpacked_all(codec, &known)) {
523                    (Ok(input), Ok(known)) => (input, known),
524                    (Err(e), _) | (_, Err(e)) => return Answer::Failed(e.to_string()),
525                },
526            };
527            // What arrived is fed in as if this run had produced it.
528            match executor.resume(&plan, input, known, keys) {
529                Ok(outcome) => answering(shared.codec, outcome),
530                Err(e) => Answer::Failed(e.to_string()),
531            }
532        }
533    }
534}
535
536/// The answer to a slice that ran: written down, and with whatever stays here
537/// left out of it.
538///
539/// **Packing goes first.** `travelling` drops what does not travel, and a tensor
540/// with a codec does travel — asking before writing it down would leave behind
541/// exactly what this exists to carry.
542///
543/// The two halves are not treated alike: `produced` is what the steps here read,
544/// so one that cannot be written down **stays here** and is named by
545/// `RunError::Lost` if anybody reads it; `last` is the value of the slice itself
546/// and has a reader over there by definition.
547fn answering(codec: Option<&dyn Codec>, outcome: Outcome) -> Answer {
548    let Some(codec) = codec else {
549        return Answer::Done(outcome.travelling());
550    };
551    let last = match codec.packed(&outcome.last) {
552        Ok(last) => last,
553        Err(e) => return Answer::Failed(e.to_string()),
554    };
555    let produced = outcome
556        .produced
557        .into_iter()
558        .map(|(id, value)| {
559            let written = codec.packed(&value).unwrap_or(value);
560            (id, written)
561        })
562        .collect();
563    Answer::Done(
564        Outcome {
565            last,
566            produced,
567            keys: outcome.keys,
568        }
569        .travelling(),
570    )
571}
572
573/// What the store has for this artifact, opened. `None` if it does not have it.
574///
575/// A store that cannot be reached is **not** a refusal: it is one trip more, so
576/// it is noted and we ask the client. What does refuse is an artifact that is
577/// there and cannot be opened, which is the same failure as one arriving broken.
578fn kept(
579    store: Option<&dyn Store>,
580    provision: &dyn Provision,
581    label: &Label,
582) -> Result<Option<Provisioned>, String> {
583    let Some(store) = store else { return Ok(None) };
584    let bytes = match store.resolve(&name_of(&label.kind, &label.id)) {
585        Err(e) => {
586            eprintln!("the store could not be asked: {e}");
587            return Ok(None);
588        }
589        Ok(None) => return Ok(None),
590        Ok(Some(bound)) => match store.get(&bound.digest) {
591            Ok(Some(bytes)) => bytes,
592            // Bound to bytes that are not there: the record is right and the
593            // blob is missing, so it is the client's turn again.
594            Ok(None) => return Ok(None),
595            Err(e) => {
596                eprintln!("the store could not be read: {e}");
597                return Ok(None);
598            }
599        },
600    };
601    provision
602        .provide(&label.kind, &bytes)
603        .map(Some)
604        .map_err(|e| e.to_string())
605}
606
607/// Keeps an artifact under the name it announced itself with.
608fn keep(store: &dyn Store, kind: &str, id: &str, bytes: &[u8]) -> Result<(), StoreError> {
609    let digest = store.put(bytes)?;
610    store.bind(
611        &name_of(kind, id),
612        &digest,
613        vec![("kind".to_string(), kind.to_string())],
614    )
615}
616
617/// What an artifact is called in the store.
618///
619/// The kind is in the name because two artifacts of different kinds can
620/// honestly be given the same id by whoever produces them — the same catalog
621/// pickled and packed as a manifest — and opening one with the other's
622/// `Provision` is not a mistake worth allowing.
623fn name_of(kind: &str, id: &str) -> String {
624    format!("artifact:{kind}:{id}")
625}