somatize_fabric_broker/embedded.rs
1//! A broker inside the client's own process.
2//!
3//! The first of the three deployments and the one that makes soma work with no
4//! platform, no head node and no internet: a client that has no session falls
5//! back to this and the graph runs on whatever workers it can reach. Not a
6//! degraded mode with its own code — the same path with another broker.
7//!
8//! It is a thread and the messages are really serialized, and neither is waste.
9//! What crosses here is a rendezvous: a run with four workers is nine messages,
10//! a few tens of microseconds all told, once, outside the loop. **The broker is
11//! in the control route and steps out of the cargo one**, so the price of being
12//! honest here is not measurable there — and being honest buys the messages
13//! being exercised for real from the first day, by a round trip that actually
14//! happens.
15//!
16//! One thread, and it must not become a hang. The failure this type exists not
17//! to have is a client blocked forever on an answer that is never coming, so
18//! every channel operation maps to [`Unanswered::Gone`] and never to an
19//! `unwrap`, pinned by a test — which is why [`Embedded::served_by`] is public:
20//! without a way to stand up a desk that fails, the one failure mode worth
21//! testing is the one that cannot be.
22
23use crate::{Ask, Host, Path, Reply, Unreadable};
24use std::collections::BTreeMap;
25use std::fmt;
26use std::sync::Mutex;
27use std::sync::mpsc::{Sender, channel};
28use std::thread::JoinHandle;
29
30/// A broker running on a thread of this process.
31pub struct Embedded {
32 /// The way in, behind a lock for the same reason the wire's `Worker` holds
33 /// one: this has to be `Sync` so two branches of a wave can reach it at
34 /// once, and one channel does not fit two conversations halfway through.
35 ///
36 /// `Option` so that [`Drop`] can let it go **before** joining: the thread
37 /// ends when its end of the channel closes, and joining first would wait
38 /// for something that is waiting for us.
39 desk: Mutex<Option<Sender<Errand>>>,
40 /// Whether the session has been opened. **Once per broker and not once per
41 /// host**: a greeting belongs to the conversation, and a run across four
42 /// workers that sent four of them would be asking one question four times.
43 greeted: Mutex<bool>,
44 /// So the thread is joined rather than left behind. `Option` for the same
45 /// reason: `Drop` has to take it.
46 thread: Option<JoinHandle<()>>,
47}
48
49/// One question and where its answer goes. Bytes in both directions, because
50/// the point is that this is the same conversation a socket will carry.
51struct Errand {
52 asked: Vec<u8>,
53 back: Sender<Vec<u8>>,
54}
55
56impl Embedded {
57 /// A broker that knows where these hosts are.
58 ///
59 /// The listing is fixed when the broker opens and the thread owns it, which
60 /// is why there is no lock around it and no way for two threads to disagree
61 /// about where `w1` is. A host that has to be added is a broker that has to
62 /// be opened.
63 pub fn open(listing: impl IntoIterator<Item = (Host, Path)>) -> Self {
64 let listing: BTreeMap<Host, Path> = listing.into_iter().collect();
65 Self::served_by(move |ask| answer(&listing, ask))
66 }
67
68 /// A broker whose answers come from `desk`.
69 ///
70 /// Public for one reason, and a present one: the worst thing this type can
71 /// do is turn a panic into a hang, and there is no way to test that a desk
72 /// which fails is reported as a failure without being able to stand one up.
73 pub fn served_by(mut desk: impl FnMut(Ask) -> Reply + Send + 'static) -> Self {
74 let (to_desk, errands) = channel::<Errand>();
75 let thread = std::thread::Builder::new()
76 .name("soma-broker".into())
77 .spawn(move || {
78 // Ends when the last sender goes, which is this broker being
79 // dropped. No stop message and nothing to forget to send.
80 for errand in errands {
81 let reply = match Ask::from_bytes(&errand.asked) {
82 Ok(ask) => desk(ask),
83 // Not a panic: somebody spoke a language this does not
84 // read, and saying so is the answer.
85 Err(why) => Reply::Refused(why.to_string()),
86 };
87 let said = reply
88 .to_bytes()
89 .unwrap_or_else(|why| unanswerable(&why.to_string()));
90 // The client may have stopped waiting. That is its business.
91 let _ = errand.back.send(said);
92 }
93 })
94 .expect("a broker needs one thread, and the OS would not give one");
95 Self {
96 desk: Mutex::new(Some(to_desk)),
97 greeted: Mutex::new(false),
98 thread: Some(thread),
99 }
100 }
101
102 /// Opens the session, if it was not open.
103 ///
104 /// Idempotent on purpose: whoever needs a rendezvous calls this first and
105 /// does not have to know whether somebody else already did. A refusal is a
106 /// refusal of the **session**, which is why it is not swallowed and retried.
107 pub fn greet(&self) -> Result<(), Unanswered> {
108 let mut greeted = match self.greeted.lock() {
109 Ok(greeted) => greeted,
110 Err(poisoned) => poisoned.into_inner(),
111 };
112 if *greeted {
113 return Ok(());
114 }
115 match self.ask(&Ask::hello())? {
116 Reply::Welcome { .. } => {
117 *greeted = true;
118 Ok(())
119 }
120 Reply::Refused(why) => Err(Unanswered::Refused(why)),
121 other => Err(Unanswered::BesideThePoint(format!(
122 "greeting it answered {other:?}"
123 ))),
124 }
125 }
126
127 /// Says something and waits for the answer.
128 ///
129 /// Every message has exactly one answer except [`Ask::Done`], which has
130 /// none — use [`Embedded::done`] for that one. Asking it here is refused
131 /// rather than waited on, because the alternative is the hang this type is
132 /// built not to have.
133 pub fn ask(&self, ask: &Ask) -> Result<Reply, Unanswered> {
134 if let Ask::Done { .. } = ask {
135 return Err(Unanswered::NoAnswerToThat);
136 }
137 let (back, answered) = channel();
138 self.post(ask, back)?;
139 let said = answered.recv().map_err(|_| Unanswered::Gone)?;
140 Reply::from_bytes(&said).map_err(Unanswered::Garbled)
141 }
142
143 /// Lets a rendezvous go. Nothing answers, and nothing is waited for.
144 ///
145 /// An embedded broker holds nothing, so nothing is released. It is sent
146 /// anyway because the same client code talks to a broker that does hold
147 /// things.
148 pub fn done(&self, host: &Host) -> Result<(), Unanswered> {
149 let (back, _) = channel();
150 self.post(&Ask::Done { host: host.clone() }, back)
151 }
152
153 fn post(&self, ask: &Ask, back: Sender<Vec<u8>>) -> Result<(), Unanswered> {
154 let asked = ask.to_bytes().map_err(Unanswered::Garbled)?;
155 let desk = match self.desk.lock() {
156 Ok(desk) => desk,
157 // A poisoned lock is a panic somebody already heard about. The
158 // channel underneath is still a channel.
159 Err(poisoned) => poisoned.into_inner(),
160 };
161 desk.as_ref()
162 .ok_or(Unanswered::Gone)?
163 .send(Errand { asked, back })
164 .map_err(|_| Unanswered::Gone)
165 }
166}
167
168/// What an embedded broker answers, given what it knows.
169///
170/// A free function and not a method so that it is the thread's: the listing
171/// moves in when the broker opens and never comes back out.
172fn answer(listing: &BTreeMap<Host, Path>, ask: Ask) -> Reply {
173 match ask {
174 Ask::Hello { protocol, .. } => Reply::to_greeting(protocol),
175 Ask::Reach { host, .. } => match listing.get(&host) {
176 Some(path) => Reply::Met {
177 path: path.clone(),
178 // No policy here, so nothing is taking it back.
179 good_for: None,
180 },
181 // Naming what it does know, because the usual cause is a typo in an
182 // `.at()` and the list is three names long.
183 None => Reply::Unreachable(match listing.is_empty() {
184 true => format!(
185 "the graph sends work to `{host}` and this broker has no hosts listed at all"
186 ),
187 false => format!(
188 "the graph sends work to `{host}`, which this broker does not know; it knows {}",
189 listing
190 .keys()
191 .map(|known| format!("`{known}`"))
192 .collect::<Vec<_>>()
193 .join(", ")
194 ),
195 }),
196 },
197 // Nothing to release, and nobody is listening for the answer.
198 Ask::Done { .. } => Reply::Welcome {
199 protocol: crate::PROTOCOL,
200 },
201 }
202}
203
204/// The last resort when even the refusal will not encode. It cannot happen with
205/// today's messages, but the alternative is an `unwrap` on the one thread whose
206/// panic is a client that waits forever.
207fn unanswerable(why: &str) -> Vec<u8> {
208 Reply::Refused(format!(
209 "the broker could not put its own answer into bytes: {why}"
210 ))
211 .to_bytes()
212 .unwrap_or_default()
213}
214
215impl Drop for Embedded {
216 /// Lets the channel go, then waits for the thread. In that order: the
217 /// thread's loop ends when the last sender is dropped, so joining first
218 /// would be waiting for something that is waiting for us.
219 fn drop(&mut self) {
220 // The same shape as everywhere else a lock is taken here: a poisoned
221 // one is a panic somebody already heard about, and the channel under it
222 // is still a channel. `take` drops the sender there and then.
223 let mut desk = match self.desk.lock() {
224 Ok(desk) => desk,
225 Err(poisoned) => poisoned.into_inner(),
226 };
227 desk.take();
228 if let Some(thread) = self.thread.take() {
229 // A thread that panicked is already reported: whoever was waiting
230 // got `Gone`. Panicking here in turn would only lose that.
231 let _ = thread.join();
232 }
233 }
234}
235
236/// Why an ask got no answer.
237#[derive(Debug, Clone, PartialEq, Eq)]
238pub enum Unanswered {
239 /// The broker's thread is not there any more: it ended, or it panicked.
240 /// Either way there is nobody to answer, which is a thing to be told rather
241 /// than a thing to wait for.
242 Gone,
243 /// The bytes were not a message, in one direction or the other.
244 Garbled(Unreadable),
245 /// [`Ask::Done`] is the one message with no answer. Waiting for one is the
246 /// hang this refuses to perform.
247 NoAnswerToThat,
248 /// The broker will not open a session, and here is why. Belongs to the
249 /// session: after it there is nothing to retry.
250 Refused(String),
251 /// It answered something that does not answer what was asked. Not a failure
252 /// of the errand but of the vocabulary — the two sides do not agree about
253 /// what this conversation is.
254 BesideThePoint(String),
255}
256
257impl fmt::Display for Unanswered {
258 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
259 match self {
260 Self::Gone => f.write_str(
261 "the broker is not answering: its thread ended or panicked. Nothing was \
262 placed, so nothing is half-done — open another one",
263 ),
264 Self::Garbled(why) => write!(f, "{why}"),
265 Self::NoAnswerToThat => f.write_str(
266 "`Done` is the one message a broker does not answer; send it with `done` \
267 rather than waiting for something that is not coming",
268 ),
269 Self::Refused(why) => write!(f, "the broker does not open a session: {why}"),
270 Self::BesideThePoint(what) => write!(
271 f,
272 "the broker answered something beside the point: {what}. The two sides do \
273 not agree about what this conversation is"
274 ),
275 }
276 }
277}
278
279impl std::error::Error for Unanswered {}