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}