Skip to main content

somatize_fabric_broker/
path.rs

1//! How a pair of endpoints ends up talking, once the broker has introduced them.
2//!
3//! Four variants, cheapest first, and **all four are here while only two can be
4//! answered**. That is the one place this crate builds ahead of its consumer,
5//! deliberately: the alternative is that adding the shared mount and the relay
6//! later changes [`Reply::Met`](crate::Reply), and a message that changes is a
7//! version that changes for everybody. The ladder is the design; what arrives
8//! later is the **probing that chooses**, not the vocabulary.
9//!
10//! [`Path::InProcess`] transfers nothing at all, and the temptation is to
11//! answer it with a pointer to something standing in this process. It cannot:
12//! every message here has to survive a round trip through bytes, including the
13//! ones an embedded broker answers without leaving the process. So it answers
14//! with a [`SlotId`] the client resolves against its own registry — which cost
15//! nothing and bought a consistency nobody planned, since [`Path::Relayed`] has
16//! exactly the same shape.
17//!
18//! [`Path::Direct`] means **a duplex byte stream between the two ends, with the
19//! broker out of it**, and not *a TCP address*: a worker started as a child and
20//! spoken to over its pipes is the same path, which the wire next door already
21//! decided by making `frame` work over `impl Read`/`impl Write`. So what varies
22//! is **how the stream is obtained**, which is why the variant carries an
23//! [`Endpoint`]. Getting this wrong is how the ladder grows a fifth rung that
24//! is really the third one twice.
25
26use serde::{Deserialize, Serialize};
27use std::fmt;
28use std::path::PathBuf;
29
30/// How two endpoints reach each other. Cheapest first.
31#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
32pub enum Path {
33    /// Nothing is transferred: both ends are this process, and the value is
34    /// passed. The slice still runs where it was placed — this is the `.at()`
35    /// that never actually left home and pays for a trip anyway.
36    ///
37    /// **Never inferred.** Whoever registers a host says it is in-process; a
38    /// broker that worked it out by comparing addresses would quietly undo the
39    /// reason a worker is a separate process, which is the GIL.
40    InProcess {
41        /// Where the client finds it, in a registry only the client has.
42        slot: SlotId,
43    },
44    /// Both ends see the same filesystem: a path is written and read. Free, and
45    /// a cluster already has one.
46    Mount {
47        /// The directory both ends agree they can see. That they really do is
48        /// what the probing establishes, and the probing is not written yet.
49        dir: PathBuf,
50    },
51    /// The two ends reach each other and speak directly, whether over a socket
52    /// or over a child's pipes. One crossing, lowest latency, broker gone.
53    Direct {
54        /// How to obtain the stream.
55        endpoint: Endpoint,
56    },
57    /// Neither can reach the other, so the bytes stream through the broker.
58    /// No disk, no durability, and never more than a window in flight.
59    Relayed {
60        /// Which stream through the relay this pair was given.
61        session: SessionId,
62    },
63}
64
65/// How a direct stream is obtained.
66///
67/// Two ways of arriving at the same thing, which is why this is not two paths.
68#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
69pub enum Endpoint {
70    /// A worker that was already standing: `"node3:7000"`.
71    Address(String),
72    /// A worker to be started here, as a child, and spoken to over its pipes:
73    /// `["python", "-m", "somatize.worker"]`.
74    ///
75    /// A whole `argv` and not a path because whoever stands a worker up decides
76    /// what it is called, what environment it needs, and whether it goes inside
77    /// an `srun`.
78    Command(Vec<String>),
79}
80
81/// Where the client finds something already standing in its own process.
82///
83/// Opaque, and meaningless anywhere else: it indexes a registry the client
84/// holds. Crossing a wire it is just a number, which is correct — a broker that
85/// is not this process can never answer [`Path::InProcess`] anyway.
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
87#[serde(transparent)]
88pub struct SlotId(pub u64);
89
90/// Which stream through a relay a pair of endpoints was given.
91///
92/// A string because whoever issues it decides what it looks like, and the day
93/// there is a real relay it has to be **unguessable**: holding one is what lets
94/// you attach to that stream. The embedded broker never issues one.
95#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
96#[serde(transparent)]
97pub struct SessionId(pub String);
98
99impl Path {
100    /// Whether two hosts given **this same path** are one place, and so share
101    /// one wire and one catalog.
102    ///
103    /// It matters more than it looks. A worker has *one* catalog, and half of
104    /// one is a different catalog: provisioning the same process twice, once
105    /// per host name, swaps what it had live and takes every activation over
106    /// there with it. Getting this wrong is not an extra socket, it is a run
107    /// that quietly loses its state.
108    ///
109    /// An **address** is an identity: the same host and port is the same
110    /// process. A **command** is not — it is a thing to run, and running it
111    /// twice gives two of them.
112    pub fn shared(&self) -> bool {
113        !matches!(
114            self,
115            Path::Direct {
116                endpoint: Endpoint::Command(_)
117            }
118        )
119    }
120}
121
122impl fmt::Display for SlotId {
123    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
124        write!(f, "slot {}", self.0)
125    }
126}
127
128impl fmt::Display for SessionId {
129    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
130        f.write_str(&self.0)
131    }
132}
133
134impl fmt::Display for Endpoint {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        match self {
137            Self::Address(addr) => f.write_str(addr),
138            Self::Command(argv) => f.write_str(&argv.join(" ")),
139        }
140    }
141}
142
143impl fmt::Display for Path {
144    /// Named the way a reader would say it out loud, because these end up in
145    /// the record and in error messages both.
146    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
147        match self {
148            Self::InProcess { slot } => write!(f, "in this process, {slot}"),
149            Self::Mount { dir } => write!(f, "over the mount at {}", dir.display()),
150            Self::Direct { endpoint } => write!(f, "straight to {endpoint}"),
151            Self::Relayed { session } => write!(f, "relayed, session {session}"),
152        }
153    }
154}