Skip to main content

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 {}