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