Skip to main content

liminal_server/console/
model.rs

1//! The topology the server holds in memory (SRV-012 R7).
2//!
3//! It is built once from the roster, the responders, the live connections and
4//! the membership at start, then kept current by one change per event at the
5//! sites that cause them. It never reads a store row and never re-reads the roster to answer a
6//! change: the latest document is always ready for a late subscriber.
7
8use std::collections::{BTreeMap, BTreeSet};
9use std::time::Instant;
10
11use super::error::ConsoleError;
12use super::records::{
13    Connection, Device, DeviceBox, DeviceKind, Line, LineKind, LineLife, LineState, Link, Mount,
14    Station, StationKind, StationState, TopologyDocument,
15};
16use super::wall::WallBase;
17use crate::server::connection::is_console_subject;
18
19/// One change to the topology, emitted where it happens.
20#[derive(Clone, Debug, PartialEq, Eq)]
21pub enum TopologyChange {
22    /// A responder (a service) was registered or removed.
23    Service {
24        /// Its subject.
25        subject: String,
26        /// Whether it is registered now.
27        present: bool,
28    },
29    /// A channel joined the roster or changed state.
30    Channel {
31        /// The channel.
32        name: String,
33        /// How long it keeps messages.
34        life: LineLife,
35        /// Whether it admits.
36        state: LineState,
37    },
38    /// A connection opened.
39    ConnectionOpened {
40        /// The connection id (the supervisor pid).
41        id: u64,
42        /// The transport it arrived on.
43        mount: Mount,
44        /// The participant its pass names, when it has one.
45        participant: Option<String>,
46    },
47    /// A connection closed.
48    ConnectionClosed {
49        /// The connection id.
50        id: u64,
51    },
52    /// A connection registered as a worker: its identity is a station
53    /// while the connection is open.
54    Worker {
55        /// The connection id.
56        connection: u64,
57        /// The worker's identity, its station id.
58        identity: String,
59    },
60    /// A connection began (`present`) or stopped using a line one way.
61    Flow {
62        /// The connection id.
63        connection: u64,
64        /// The line.
65        line: String,
66        /// Which way it uses the line.
67        role: FlowRole,
68        /// Whether it does so now.
69        present: bool,
70    },
71    /// A cluster peer joined or left.
72    Peer {
73        /// The peer's node name.
74        name: String,
75        /// Whether it is a member now.
76        present: bool,
77    },
78}
79
80/// Which way a connection uses a line. Publishers order before
81/// subscribers, so a line's stops run from where its messages enter to where
82/// they leave.
83#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
84pub enum FlowRole {
85    /// It publishes on the line.
86    Publishes,
87    /// It subscribes to the line.
88    Subscribes,
89}
90
91/// What this device is, fixed at start.
92#[derive(Clone, Debug, PartialEq, Eq)]
93pub struct ThisDevice {
94    /// The device id: the cluster node name, or `local` without a cluster.
95    pub id: String,
96    /// The liminal version running here, from the build.
97    pub liminal: String,
98    /// When this server's incarnation started, for its uptime.
99    pub started: Instant,
100    /// Whether this server keeps a persistent store: its depot, a station
101    /// that durable lines pass before the exchange (lane brief ruling 5).
102    pub depot: bool,
103}
104
105/// The held topology and the revision of its latest document.
106#[derive(Clone, Debug)]
107pub struct TopologyModel {
108    this: ThisDevice,
109    rev: u64,
110    stations: BTreeSet<String>,
111    lines: BTreeMap<String, (LineLife, LineState)>,
112    connections: BTreeMap<u64, (Mount, Option<String>)>,
113    peers: BTreeSet<String>,
114    /// Registered workers, by connection.
115    workers: BTreeMap<u64, String>,
116    /// Each line's users, by role then connection.
117    flows: BTreeMap<String, BTreeSet<(FlowRole, u64)>>,
118}
119
120impl TopologyModel {
121    /// The model at start, revision 1, from what the server already holds.
122    /// `console.*` channels are never lines.
123    #[must_use]
124    pub fn new(
125        this: ThisDevice,
126        stations: impl IntoIterator<Item = String>,
127        lines: impl IntoIterator<Item = (String, LineLife, LineState)>,
128        connections: impl IntoIterator<Item = (u64, Mount, Option<String>)>,
129        peers: impl IntoIterator<Item = String>,
130    ) -> Self {
131        Self {
132            this,
133            rev: 1,
134            stations: stations.into_iter().collect(),
135            lines: lines
136                .into_iter()
137                .filter(|(name, _, _)| !is_console_subject(name))
138                .map(|(name, life, state)| (name, (life, state)))
139                .collect(),
140            connections: connections
141                .into_iter()
142                .map(|(id, mount, participant)| (id, (mount, participant)))
143                .collect(),
144            peers: peers.into_iter().collect(),
145            workers: BTreeMap::new(),
146            flows: BTreeMap::new(),
147        }
148    }
149
150    /// Whether this server keeps a persistent store, its depot.
151    #[must_use]
152    pub const fn depot(&self) -> bool {
153        self.this.depot
154    }
155
156    /// The revision of the latest document.
157    #[must_use]
158    pub const fn rev(&self) -> u64 {
159        self.rev
160    }
161
162    /// Applies `change`. A change that alters the topology moves the revision
163    /// on by one and answers true; one that alters nothing (a repeat, or a
164    /// `console.*` channel) leaves it and answers false, so nothing is
165    /// published for it.
166    pub fn apply(&mut self, change: TopologyChange) -> bool {
167        let changed = match change {
168            TopologyChange::Service { subject, present } => {
169                if present {
170                    self.stations.insert(subject)
171                } else {
172                    self.stations.remove(&subject)
173                }
174            }
175            TopologyChange::Channel { name, life, state } => {
176                !is_console_subject(&name)
177                    && self.lines.insert(name, (life, state)) != Some((life, state))
178            }
179            TopologyChange::ConnectionOpened {
180                id,
181                mount,
182                participant,
183            } => {
184                let entry = (mount, participant);
185                if self.connections.get(&id) == Some(&entry) {
186                    false
187                } else {
188                    self.connections.insert(id, entry);
189                    true
190                }
191            }
192            TopologyChange::ConnectionClosed { id } => {
193                let closed = self.connections.remove(&id).is_some();
194                let worker = self.workers.remove(&id).is_some();
195                for users in self.flows.values_mut() {
196                    users.retain(|(_, connection)| *connection != id);
197                }
198                self.flows.retain(|_, users| !users.is_empty());
199                closed || worker
200            }
201            TopologyChange::Worker {
202                connection,
203                identity,
204            } => self.workers.insert(connection, identity.clone()) != Some(identity),
205            TopologyChange::Flow {
206                connection,
207                line,
208                role,
209                present,
210            } => {
211                if is_console_subject(&line) {
212                    false
213                } else {
214                    let moved = if present {
215                        self.flows
216                            .entry(line)
217                            .or_default()
218                            .insert((role, connection))
219                    } else if let Some(users) = self.flows.get_mut(&line) {
220                        let removed = users.remove(&(role, connection));
221                        if users.is_empty() {
222                            self.flows.remove(&line);
223                        }
224                        removed
225                    } else {
226                        false
227                    };
228                    moved && self.workers.contains_key(&connection)
229                }
230            }
231            TopologyChange::Peer { name, present } => {
232                if present {
233                    self.peers.insert(name)
234                } else {
235                    self.peers.remove(&name)
236                }
237            }
238        };
239        if changed {
240            self.rev = self.rev.saturating_add(1);
241        }
242        changed
243    }
244
245    /// The stations `line` passes: its registered publishers, then this
246    /// server's depot when the line is durable and the server keeps a
247    /// persistent store (the append comes before the fan-out), then this
248    /// server's exchange, once on every line, then its registered
249    /// subscribers, each once.
250    fn stops(&self, line: &str, line_life: LineLife) -> Vec<String> {
251        let mut stops: Vec<String> = Vec::new();
252        let depot = (self.this.depot && line_life == LineLife::Kept).then(|| self.depot_id());
253        // The server's own stops, in the order a message reaches them.
254        let mut own = depot.into_iter().chain([self.this.id.clone()]);
255        let users = self.flows.get(line).into_iter().flatten();
256        for (role, connection) in users {
257            if *role == FlowRole::Subscribes {
258                stops.extend(own.by_ref());
259            }
260            if let Some(identity) = self.workers.get(connection)
261                && self.reserved(identity).is_none()
262                && !stops.contains(identity)
263            {
264                stops.push(identity.clone());
265            }
266        }
267        stops.extend(own);
268        stops
269    }
270
271    /// The station `id` would collide with: this server's exchange (its
272    /// device id), or its depot when it keeps one. A worker or service with
273    /// such an id is kept off the map, never merged into that station
274    /// (Buttercup's V1, Hermes 04:27).
275    #[must_use]
276    pub fn reserved(&self, id: &str) -> Option<&'static str> {
277        if id == self.this.id {
278            Some("exchange")
279        } else if self.this.depot
280            && id
281                .strip_prefix(self.this.id.as_str())
282                .is_some_and(|rest| rest == ".depot")
283        {
284            Some("depot")
285        } else {
286            None
287        }
288    }
289
290    /// This server's depot's station id: `<device>.depot`.
291    fn depot_id(&self) -> String {
292        format!("{}.depot", self.this.id)
293    }
294
295    /// This server's incarnation started at `started`: its uptime runs
296    /// from there (SRV-012 R7). Answers whether that moved it.
297    pub fn incarnation_started(&mut self, started: Instant) -> bool {
298        let moved = self.this.started != started;
299        self.this.started = started;
300        moved
301    }
302
303    /// Each connection's participant, by connection id. Test-only.
304    #[cfg(test)]
305    pub(crate) fn participants(&self) -> Vec<Option<String>> {
306        self.connections
307            .values()
308            .map(|(_, participant)| participant.clone())
309            .collect()
310    }
311
312    /// How many connections the model holds. Test-only.
313    #[cfg(test)]
314    pub(crate) fn connection_count(&self) -> usize {
315        self.connections.len()
316    }
317
318    /// The whole document as it stands, always. This device's start is wall
319    /// time from `wall`, the same whenever the document is built, so a reader
320    /// derives the uptime on its own clock and a document never goes stale.
321    /// When the start falls outside u64 milliseconds since the epoch, the
322    /// document names that fault where the start would be, and the fault
323    /// comes back beside it for the caller to count: one bad reading never
324    /// withholds the topology.
325    #[must_use]
326    pub fn document(&self, wall: &WallBase) -> (TopologyDocument, Option<ConsoleError>) {
327        let this = &self.this.id;
328        let (started, fault) = match wall.unix_ms_of(self.this.started) {
329            Ok(started) => (Some(started), None),
330            Err(error) => (None, Some(error)),
331        };
332        let mut devices = vec![Device {
333            id: this.clone(),
334            name: this.clone(),
335            kind: DeviceKind::Device,
336            liminal: Some(self.this.liminal.clone()),
337            started,
338            started_fault: fault.as_ref().map(ToString::to_string),
339            zone: Some(DeviceBox::THIS),
340        }];
341        devices.extend(self.peers.iter().map(|peer| Device {
342            id: peer.clone(),
343            name: peer.clone(),
344            kind: DeviceKind::Device,
345            liminal: None,
346            started: None,
347            started_fault: None,
348            zone: None,
349        }));
350        // A registered responder is a service; a registered worker that is
351        // not also one is a worker. Neither takes this server's exchange or
352        // depot id: one that would is kept off the map.
353        let depot = self.this.depot.then(|| self.depot_id());
354        let mut stations: BTreeMap<&String, StationKind> = self
355            .workers
356            .values()
357            .filter(|identity| self.reserved(identity).is_none())
358            .map(|identity| (identity, StationKind::Worker))
359            .collect();
360        stations.extend(
361            self.stations
362                .iter()
363                .filter(|subject| self.reserved(subject).is_none())
364                .map(|subject| (subject, StationKind::Service)),
365        );
366        stations.extend(depot.iter().map(|id| (id, StationKind::Depot)));
367        stations.insert(this, StationKind::Exchange);
368        let document = TopologyDocument::new(
369            self.rev,
370            this.clone(),
371            devices,
372            stations
373                .into_iter()
374                .map(|(subject, kind)| Station {
375                    id: subject.clone(),
376                    name: subject.clone(),
377                    device: this.clone(),
378                    kind,
379                    state: StationState::Live,
380                })
381                .collect(),
382            self.lines
383                .iter()
384                .map(|(name, (life, state))| Line {
385                    id: name.clone(),
386                    kind: LineKind::Stream,
387                    life: *life,
388                    state: *state,
389                    stops: self.stops(name, *life),
390                })
391                .collect(),
392            self.peers
393                .iter()
394                .map(|peer| Link::to_peer(this, peer))
395                .collect(),
396            self.connections
397                .iter()
398                .map(|(id, (mount, participant))| Connection {
399                    id: id.to_string(),
400                    mount: *mount,
401                    participant: participant.clone(),
402                })
403                .collect(),
404        );
405        (document, fault)
406    }
407}