Skip to main content

somatize_fabric_broker/
session.rs

1//! One client's conversation with one broker, and the wires it has opened.
2//!
3//! [`Reaching`](crate::Reaching) is one host; this is everything that has to be
4//! true across all of them, and there are exactly two such things.
5//!
6//! **Ask eagerly, connect lazily.** Asking where a host is costs tens of bytes
7//! and has to happen before the first node runs, because what gets packed for a
8//! host depends on which hosts turn out to be the same place. Connecting costs
9//! a socket, a process, or both, and a graph names hosts a run may never reach.
10//! So a rendezvous is asked for once and remembered here, and the wire it
11//! describes is opened the first time somebody actually sends work — which also
12//! means the ask happens **once** however it was triggered.
13//!
14//! **Two names for one place are one wire**, the rule [`Path::shared`] states,
15//! enforced here because here is the only place that can see two hosts at once.
16//! Without it a process named twice gets provisioned twice, and since a worker
17//! has one catalog, the second half replaces the first and takes every
18//! activation with it.
19
20use crate::{Ask, Embedded, Endpoint, Host, Needs, Path, Reply, Unanswered};
21use somatize_core::{Codec, TransportError};
22use somatize_fabric_wire::{Artifact, Worker};
23use std::collections::BTreeMap;
24use std::process::Command;
25use std::sync::{Arc, Mutex, MutexGuard};
26
27/// A client's side of one conversation with one broker.
28pub struct Session {
29    /// Who to ask.
30    broker: Arc<Embedded>,
31    /// Where each host turned out to be. One `Reach` per host per session,
32    /// whoever asked for it.
33    found: Mutex<BTreeMap<Host, Path>>,
34    /// The wires already open, by the path they were opened for. Only paths
35    /// that [`Path::shared`] agrees about are looked up here; a command is run
36    /// again rather than shared.
37    wires: Mutex<BTreeMap<Path, Arc<Worker>>>,
38    /// Who knows how to write down what would not otherwise cross. Every wire
39    /// this opens gets it.
40    codec: Option<Arc<dyn Codec>>,
41}
42
43impl Session {
44    /// A session with this broker.
45    pub fn with(broker: Arc<Embedded>) -> Self {
46        Self {
47            broker,
48            found: Mutex::new(BTreeMap::new()),
49            wires: Mutex::new(BTreeMap::new()),
50            codec: None,
51        }
52    }
53
54    /// The same session, with somebody who knows how to write down what an
55    /// opaque carries. Set before use, so every wire gets the same one.
56    pub fn packing(mut self, codec: Arc<dyn Codec>) -> Self {
57        self.codec = Some(codec);
58        self
59    }
60
61    /// Where this host is, asking the broker if nobody has yet.
62    ///
63    /// Public because deciding what to pack needs it before the run starts:
64    /// what goes to a host depends on which hosts are the same place.
65    pub fn find(&self, host: &Host) -> Result<Path, TransportError> {
66        let mut found = locked(&self.found);
67        if let Some(path) = found.get(host) {
68            return Ok(path.clone());
69        }
70        // Not held across the ask: the greeting and the rendezvous are two
71        // messages and another host may be resolving at the same time.
72        drop(found);
73
74        self.broker.greet().map_err(|why| about(host, &why))?;
75        let answer = self
76            .broker
77            .ask(&Ask::Reach {
78                host: host.clone(),
79                needs: Needs::default(),
80            })
81            .map_err(|why| about(host, &why))?;
82
83        let path = match answer {
84            Reply::Met { path, .. } => path,
85            Reply::Unreachable(why) => return Err(TransportError::new(why)),
86            Reply::Refused(why) => {
87                return Err(TransportError::new(format!(
88                    "the broker closed the session while looking for `{host}`: {why}"
89                )));
90            }
91            other => {
92                return Err(TransportError::new(format!(
93                    "asked where `{host}` is, the broker answered {other:?}"
94                )));
95            }
96        };
97
98        found = locked(&self.found);
99        // Whoever got here first wins, so two threads asking at once still end
100        // up agreeing about where `w1` is.
101        Ok(found.entry(host.clone()).or_insert(path).clone())
102    }
103
104    /// The wire to this host, opening it if this is the first work for it.
105    ///
106    /// `carries` is what to provision the far side with, handed to the wire when
107    /// it is born and only sent if the far side asks. If this wire was already
108    /// open for another name of the same place, the artifact is the same one by
109    /// construction, which the wire treats as nothing to do.
110    pub fn wire(
111        &self,
112        host: &Host,
113        carries: Option<(Artifact, String)>,
114    ) -> Result<Arc<Worker>, TransportError> {
115        let path = self.find(host)?;
116        let shared = path.shared();
117
118        if shared && let Some(worker) = locked(&self.wires).get(&path) {
119            {
120                let worker = Arc::clone(worker);
121                // Another name for a place already open. The artifact is the
122                // same by construction; saying so again is nothing, and saying
123                // something different is the refusal the wire owns.
124                if let Some((artifact, runtime)) = carries {
125                    worker.offering(artifact, runtime)?;
126                }
127                return Ok(worker);
128            }
129        }
130
131        let worker = Arc::new(self.open(host, &path, carries)?);
132        if shared {
133            locked(&self.wires).insert(path, Arc::clone(&worker));
134        }
135        Ok(worker)
136    }
137
138    /// A token that is **equal for two hosts that share a wire** and different
139    /// for two that do not.
140    ///
141    /// It exists for whoever decides what to pack: a worker has one catalog, so
142    /// what is packed is packed per *wire* and not per *name*, and the only
143    /// thing that knows which names are one wire is this. Handing out a token
144    /// rather than the path keeps the rule here.
145    ///
146    /// The bytes of the path itself, so two tokens are equal exactly when the
147    /// paths are — with the host appended when the path is not shared, so a
148    /// command listed twice is two tokens and gets run twice.
149    pub fn wire_token(&self, host: &Host) -> Result<Vec<u8>, TransportError> {
150        let path = self.find(host)?;
151        let mut token = rmp_serde::to_vec(&path).map_err(|e| {
152            TransportError::new(format!(
153                "the broker's answer about `{host}` will not write: {e}"
154            ))
155        })?;
156        if !path.shared() {
157            token.push(0);
158            token.extend_from_slice(host.as_str().as_bytes());
159        }
160        Ok(token)
161    }
162
163    /// Lets a rendezvous go. Best effort, like the message it sends.
164    pub fn done(&self, host: &Host) {
165        let _ = self.broker.done(host);
166    }
167
168    /// The wire that path calls for.
169    fn open(
170        &self,
171        host: &Host,
172        path: &Path,
173        carries: Option<(Artifact, String)>,
174    ) -> Result<Worker, TransportError> {
175        let worker = match path {
176            Path::Direct {
177                endpoint: Endpoint::Address(addr),
178            } => Worker::connect(addr).map_err(|e| {
179                TransportError::new(format!(
180                    "the broker says `{host}` is at {addr}, and nobody is listening there: {e}"
181                ))
182            })?,
183            Path::Direct {
184                endpoint: Endpoint::Command(argv),
185            } => {
186                let (program, rest) = argv.split_first().ok_or_else(|| {
187                    TransportError::new(format!(
188                        "the broker says `{host}` is a command with no program in it"
189                    ))
190                })?;
191                let mut command = Command::new(program);
192                command.args(rest);
193                Worker::spawn(command).map_err(|e| {
194                    TransportError::new(format!(
195                        "the broker says `{host}` is `{}`, which would not start: {e}",
196                        argv.join(" ")
197                    ))
198                })?
199            }
200            // In the message since the first version on purpose, so that the
201            // day the negotiation picks one it is new behaviour and not a new
202            // protocol.
203            not_yet => {
204                return Err(TransportError::new(format!(
205                    "the broker put `{host}` {not_yet}, and this client only knows how to \
206                     take the direct path so far; the other three arrive with the negotiation"
207                )));
208            }
209        };
210
211        let worker = match &self.codec {
212            Some(codec) => worker.packing(Arc::clone(codec)),
213            None => worker,
214        };
215        Ok(match carries {
216            Some((artifact, runtime)) => worker.carrying(artifact, runtime),
217            None => worker,
218        })
219    }
220}
221
222/// A poisoned lock is a panic somebody already heard about; what is under it is
223/// still what it was.
224fn locked<T>(what: &Mutex<T>) -> MutexGuard<'_, T> {
225    match what.lock() {
226        Ok(one) => one,
227        Err(poisoned) => poisoned.into_inner(),
228    }
229}
230
231/// Whatever went wrong, said with the host in front of it: *the broker is not
232/// answering* is not actionable, and *the broker is not answering about `w1`* is.
233fn about(host: &Host, why: &Unanswered) -> TransportError {
234    TransportError::new(format!("looking for `{host}`: {why}"))
235}