somatize_fabric_broker/reaching.rs
1//! One host, reached through a broker, standing in the engine's `Transport`
2//! hole.
3//!
4//! The seam: above it the engine says *carry this slice to `w1`* and knows
5//! nothing else; below it a broker was asked where `w1` is and a wire was opened
6//! to whatever it said. Neither half learns about the other.
7//!
8//! Thin on purpose. Everything true across hosts — where each one turned out to
9//! be, which of them are the same place — belongs to the
10//! [`Session`](crate::Session), the only thing that sees more than one at a
11//! time. What is left here is one name, what is staged for it, and the wire once
12//! there is one.
13//!
14//! **The connection waits until somebody sends work.** A graph names hosts a run
15//! may never reach, so this opens nothing when it is built; the rendezvous may
16//! already have happened, but a rendezvous is tens of bytes and a connection is
17//! a socket or a process. The visible consequence: **an unreachable host now
18//! fails when it is needed rather than when it is named.**
19//!
20//! Packing an artifact is expensive and happens up front, because a worker has
21//! **one** catalog and half of one is a different catalog. Those two look like
22//! they conflict and do not: [`Reaching::offering`] *stages* the artifact, and
23//! the bytes only move inside the wire's own greeting, on the first dispatch —
24//! so the same artifact twice does nothing, and changing one out from under an
25//! open session fails rather than swapping a catalog with live state in it.
26//!
27//! A [`Reply::Met`](crate::Reply::Met) can carry a `good_for` and **nothing here
28//! enforces it**: no broker issues one today, so enforcing it would be a
29//! mechanism with no tenant. The day one does, the enforcement is this type's —
30//! it is the only thing that knows when the rendezvous was granted.
31
32use crate::{Host, Session};
33use somatize_core::{Cargo, Outcome, Plan, Transport, TransportError, Watcher};
34use somatize_fabric_wire::{Artifact, Worker};
35use std::sync::{Arc, Mutex, MutexGuard};
36
37/// One host, reached through a broker.
38pub struct Reaching {
39 /// The conversation this host is reached through. Shared: one session
40 /// serves every host of a run, greets once for all of them, and is what
41 /// notices that two of them are the same place.
42 session: Arc<Session>,
43 /// The name the graph gave it. Every message about this rendezvous names
44 /// it, including the one sent when this is dropped.
45 host: Host,
46 /// What to provision the far side with, staged until there is a wire to
47 /// stage it on. `None` is a worker that brings its own catalog.
48 carries: Mutex<Option<(Artifact, String)>>,
49 /// The wire, once there is one. Behind a lock because [`Transport`] is
50 /// `Sync` and two branches of a wave arrive here at the same time — the
51 /// first through opens it and the second finds it open.
52 open: Mutex<Option<Arc<Worker>>>,
53}
54
55impl Reaching {
56 /// A host that will be connected to through this session when somebody
57 /// needs it.
58 pub fn new(session: Arc<Session>, host: Host) -> Self {
59 Self {
60 session,
61 host,
62 carries: Mutex::new(None),
63 open: Mutex::new(None),
64 }
65 }
66
67 /// The name this reaches.
68 pub fn host(&self) -> &Host {
69 &self.host
70 }
71
72 /// Tells it what to provision the far side with, before the first job.
73 ///
74 /// Staged if the wire is not open yet, handed straight over if it is — and
75 /// either way the far side only receives it if it asks, because the wire
76 /// announces an artifact's **name** and sends the bytes on request.
77 pub fn offering(
78 &self,
79 artifact: Artifact,
80 runtime: impl Into<String>,
81 ) -> Result<(), TransportError> {
82 let open = locked(&self.open);
83 if let Some(worker) = open.as_ref() {
84 // Open session: the wire owns this rule and enforces it, including
85 // the refusal to swap a catalog with live state behind it.
86 return worker.offering(artifact, runtime);
87 }
88 drop(open);
89
90 let mut carries = locked(&self.carries);
91 // Nothing has greeted anybody yet, so replacing is free — and being
92 // handed the same one twice is still nothing, which is what a graph run
93 // in pieces does.
94 match carries.as_ref() {
95 Some((already, _)) if already.id == artifact.id => Ok(()),
96 _ => {
97 *carries = Some((artifact, runtime.into()));
98 Ok(())
99 }
100 }
101 }
102
103 /// The wire to this host, opening it if this is the first work for it.
104 fn wire(&self) -> Result<Arc<Worker>, TransportError> {
105 let mut open = locked(&self.open);
106 if let Some(worker) = open.as_ref() {
107 return Ok(Arc::clone(worker));
108 }
109 let worker = self
110 .session
111 .wire(&self.host, locked(&self.carries).clone())?;
112 *open = Some(Arc::clone(&worker));
113 Ok(worker)
114 }
115}
116
117impl Transport for Reaching {
118 /// The connection on the first call, and after that this is the wire's
119 /// dispatch with one `Arc` clone in front of it.
120 fn dispatch(
121 &self,
122 plan: &Plan,
123 cargo: &Cargo<'_>,
124 seen: Option<&dyn Watcher>,
125 ) -> Result<Outcome, TransportError> {
126 self.wire()?.dispatch(plan, cargo, seen)
127 }
128}
129
130impl Drop for Reaching {
131 /// Lets the rendezvous go, so that no client has to remember to.
132 ///
133 /// Only one that was taken: a handle nobody sent work to never held
134 /// anything. Nothing fails if this does not arrive, which is why the failure
135 /// is swallowed — a run that finished is not a run to report an error from.
136 fn drop(&mut self) {
137 if locked(&self.open).is_some() {
138 self.session.done(&self.host);
139 }
140 }
141}
142
143fn locked<T>(what: &Mutex<T>) -> MutexGuard<'_, T> {
144 match what.lock() {
145 Ok(one) => one,
146 Err(poisoned) => poisoned.into_inner(),
147 }
148}