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}