Skip to main content

liminal_server/console/
records.rs

1//! The console's records, in the field names of the console's own reader.
2//!
3//! The reader is `apps/liminal-console/src/live/types.ts` (`TopologyDocument`
4//! and `TapBatch`), so a record the server publishes is a record
5//! `parseRecord` reads.
6//!
7//! The bytes are the server's canonical form: the record is turned into a JSON
8//! value, whose maps keep their keys sorted, and serialised from there. That is
9//! the same output the channel's schema normaliser gives, so `v` arrives in
10//! sorted position, not first, and a reader takes every field by key.
11//!
12//! Left out of the topology because nothing in the server holds them yet:
13//! archives, exports, gates, federation and proposed routes, a link's round
14//! trip (sent as `null`) and refusals, and a line's product and stops beyond
15//! what the roster knows. Nothing is sent as a made-up value in their place.
16
17use std::num::NonZeroU32;
18
19use serde::Serialize;
20
21use super::error::ConsoleError;
22use super::wall::WallBase;
23use crate::tap::{self, TapIdent, TapOutcome, TapSource};
24
25/// The record version the console reads.
26pub const RECORD_VERSION: u64 = 1;
27
28/// A device: this one, or a cluster peer.
29#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
30pub struct Device {
31    /// The device id (the cluster node name, or `local` without a cluster).
32    pub id: String,
33    /// The device's name as shown.
34    pub name: String,
35    /// Always a device: federation has no source.
36    pub kind: DeviceKind,
37    /// The liminal version running there: known for this device (from the
38    /// build), not for a peer, whose version nothing here holds.
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub liminal: Option<String>,
41    /// When the server's incarnation started, in wall-clock milliseconds
42    /// since the epoch: known for this device, not for a peer. A reader takes
43    /// the uptime as its own now less this, so the figure never goes stale
44    /// between documents (Hermes 7 Oct 01:49, which replaced `uptime`).
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub started: Option<u64>,
47    /// Why this device's start is missing, in the server's words: the clock
48    /// fault met building the document (Buttercup's finding 3, Hermes 03:05).
49    /// Present only when `started` is absent on this device, so never both.
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub started_fault: Option<String>,
52    /// Where the map draws the device's zone (`box` in types.ts), when this
53    /// server places it.
54    #[serde(rename = "box", skip_serializing_if = "Option::is_none")]
55    pub zone: Option<DeviceBox>,
56}
57
58/// A device's zone on the console's map, in the map's own units.
59#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
60pub struct DeviceBox {
61    /// Left edge.
62    pub x: i64,
63    /// Top edge.
64    pub y: i64,
65    /// Width.
66    pub w: i64,
67    /// Height.
68    pub h: i64,
69}
70
71impl DeviceBox {
72    /// This server's own zone: the map's fitted area inset by 40 on each
73    /// side, the zone the console draws for a lone device (`NetworkPage`'s
74    /// `FIT` of -180, 20, 1420, 600).
75    pub const THIS: Self = Self {
76        x: -140,
77        y: 60,
78        w: 1340,
79        h: 520,
80    };
81}
82
83/// What a device is.
84#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
85#[serde(rename_all = "lowercase")]
86pub enum DeviceKind {
87    /// A liminal server.
88    Device,
89}
90
91/// A station: a registered service.
92#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
93pub struct Station {
94    /// The station id (the responder's subject).
95    pub id: String,
96    /// Its name as shown.
97    pub name: String,
98    /// The device it runs on.
99    pub device: String,
100    /// What kind of station.
101    pub kind: StationKind,
102    /// Its state.
103    pub state: StationState,
104}
105
106/// What a station is. Only services have a source until the services
107/// registry exists.
108#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
109#[serde(rename_all = "lowercase")]
110pub enum StationKind {
111    /// A registered conversation responder.
112    Service,
113    /// A registered worker.
114    Worker,
115    /// This server's persistent store, which a durable line's messages
116    /// reach before the fan-out (lane brief ruling 5).
117    Depot,
118    /// This server's exchange, the fan-out every line passes; its id is the
119    /// device's (lane brief ruling 2).
120    Exchange,
121}
122
123/// A station's state. A registered responder is live; nothing in the server
124/// is asleep, held or retired as a station yet.
125#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
126#[serde(rename_all = "lowercase")]
127pub enum StationState {
128    /// Registered and answering.
129    Live,
130}
131
132/// A line: a stream on the roster.
133#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
134pub struct Line {
135    /// The channel name.
136    pub id: String,
137    /// Always a stream: calls, archives and exports have no source.
138    pub kind: LineKind,
139    /// How long its messages are kept.
140    pub life: LineLife,
141    /// Whether it admits.
142    pub state: LineState,
143    /// The stations it passes, in order.
144    pub stops: Vec<String>,
145}
146
147/// What a line is.
148#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
149#[serde(rename_all = "lowercase")]
150pub enum LineKind {
151    /// A channel.
152    Stream,
153}
154
155/// How long a line keeps its messages, from the channel's mode.
156#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
157#[serde(rename_all = "lowercase")]
158pub enum LineLife {
159    /// Ephemeral: messages pass and are not stored.
160    Passing,
161    /// Durable: messages are stored.
162    Kept,
163}
164
165/// Whether a line admits.
166#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
167#[serde(rename_all = "lowercase")]
168pub enum LineState {
169    /// Admitting publishes and subscribes.
170    Live,
171    /// Quiesced: refusing new publishes and subscribes.
172    Quiesced,
173}
174
175/// A link to a cluster peer, present while the peer is.
176#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
177pub struct Link {
178    /// The link id, `<this device>~<peer>`.
179    pub id: String,
180    /// The peer's name as shown.
181    pub name: String,
182    /// How the two devices are joined.
183    pub kind: LinkKind,
184    /// The link's state.
185    pub state: LinkState,
186    /// Round trip in milliseconds. No source: always `null`, never `0`.
187    pub rtt_ms: Option<u64>,
188    /// The two device ids it joins, in sorted order, so the same link has
189    /// the same bytes whichever end publishes it.
190    pub ends: [String; 2],
191}
192
193impl Link {
194    /// The link from `local` to `peer`, up, with no round trip measured.
195    #[must_use]
196    pub fn to_peer(local: &str, peer: &str) -> Self {
197        let ends = if peer < local {
198            [peer.to_owned(), local.to_owned()]
199        } else {
200            [local.to_owned(), peer.to_owned()]
201        };
202        Self {
203            id: format!("{local}~{peer}"),
204            name: peer.to_owned(),
205            kind: LinkKind::SameNetwork,
206            state: LinkState::Up,
207            rtt_ms: None,
208            ends,
209        }
210    }
211}
212
213/// How two devices are joined.
214#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
215pub enum LinkKind {
216    /// Members of one cluster.
217    #[serde(rename = "same network")]
218    SameNetwork,
219}
220
221/// A link's state. A departed peer's link is absent, not down: membership
222/// keeps no record of why it left.
223#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
224#[serde(rename_all = "lowercase")]
225pub enum LinkState {
226    /// The peer is a member.
227    Up,
228}
229
230/// A live connection to this server.
231#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
232pub struct Connection {
233    /// The connection id, as a tap message's `from` names it.
234    pub id: String,
235    /// The transport it arrived on.
236    pub mount: Mount,
237    /// The participant its pass names, when it presented one.
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub participant: Option<String>,
240}
241
242/// The transport a connection arrived on.
243#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
244#[serde(rename_all = "lowercase")]
245pub enum Mount {
246    /// The TCP wire listener.
247    Tcp,
248    /// The WebSocket listener.
249    Websocket,
250    /// The in-process transport.
251    Loopback,
252}
253
254impl From<crate::server::mount::MountKind> for Mount {
255    fn from(mount: crate::server::mount::MountKind) -> Self {
256        match mount {
257            crate::server::mount::MountKind::Tcp => Self::Tcp,
258            crate::server::mount::MountKind::WebSocket => Self::Websocket,
259            crate::server::mount::MountKind::Loopback => Self::Loopback,
260        }
261    }
262}
263
264/// One `console.topology` record: the whole document as it stands.
265#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
266pub struct TopologyDocument {
267    v: u64,
268    #[serde(rename = "type")]
269    record_type: &'static str,
270    /// One more than the previous document's, from 1 at start.
271    pub rev: u64,
272    /// The id of the device this server is, one of `devices`' ids (lane
273    /// brief ruling 1, Hermes 7 Oct 03:51): named once here, never as a flag
274    /// on each device.
275    pub this_device: String,
276    /// This device, then each peer.
277    pub devices: Vec<Device>,
278    /// The registered services.
279    pub stations: Vec<Station>,
280    /// Every roster channel outside `console.`.
281    pub lines: Vec<Line>,
282    /// One per peer.
283    pub links: Vec<Link>,
284    /// One per live connection.
285    pub connections: Vec<Connection>,
286}
287
288impl TopologyDocument {
289    /// A document at `rev`, from the server that is device `this_device`.
290    #[must_use]
291    pub const fn new(
292        rev: u64,
293        this_device: String,
294        devices: Vec<Device>,
295        stations: Vec<Station>,
296        lines: Vec<Line>,
297        peer_links: Vec<Link>,
298        connections: Vec<Connection>,
299    ) -> Self {
300        Self {
301            v: RECORD_VERSION,
302            record_type: "topology",
303            rev,
304            this_device,
305            devices,
306            stations,
307            lines,
308            links: peer_links,
309            connections,
310        }
311    }
312}
313
314/// What became of a tapped message, in the console's words.
315///
316/// Only the words the server has an honest cause for: the console's call words (reply,
317/// died, deadline), deferred, dropped and redelivered have none here.
318#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
319#[serde(rename_all = "lowercase")]
320pub enum Outcome {
321    /// Taken by at least one subscriber.
322    Delivered,
323    /// Admitted, and no subscriber matched.
324    Nobody,
325    /// Admitted; subscribers matched and none took it.
326    Unaccepted,
327    /// Refused at admission, by the schema, or by the library.
328    Refused,
329    /// The store could not say whether the append landed.
330    Unresolved,
331}
332
333impl From<TapOutcome> for Outcome {
334    fn from(outcome: TapOutcome) -> Self {
335        match outcome {
336            TapOutcome::Delivered => Self::Delivered,
337            TapOutcome::NoSubscriber => Self::Nobody,
338            TapOutcome::NoneAccepted => Self::Unaccepted,
339            TapOutcome::Unresolved => Self::Unresolved,
340            TapOutcome::Refused => Self::Refused,
341        }
342    }
343}
344
345/// One point a message crossed.
346#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
347pub struct Hop {
348    /// The device id.
349    pub node: String,
350    /// Milliseconds after the message's `t`.
351    pub at_ms: u64,
352}
353
354/// The facts a tap carries about one message. Payloads never ride here.
355#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
356pub struct TapMessage {
357    /// `<device>:<message id>` when admitted, `<device>:x<observation>`
358    /// otherwise.
359    pub id: String,
360    /// The subject (channel name).
361    pub subject: String,
362    /// Payload size in bytes.
363    pub size: u64,
364    /// What became of it.
365    pub outcome: Outcome,
366    /// Wall time it was first seen, in milliseconds since the epoch.
367    pub t: u64,
368    /// The publishing connection's id, or `liminal.<device>` for the server.
369    pub from: String,
370    /// The station that published it: the registered worker identity of the
371    /// publishing connection. Absent when that connection is not a registered
372    /// worker, and for the server's own records.
373    #[serde(skip_serializing_if = "Option::is_none")]
374    pub from_station: Option<String>,
375    /// This device's id.
376    pub to: String,
377    /// The stations that received it: each registered worker whose
378    /// subscription's inbox on this device queued the message, once, in
379    /// delivery order. A subscriber whose inbox refused it is not here; nor is
380    /// one that is no registered worker (it is counted in the outcome, never
381    /// named), nor a receiver on another device. `[]` when the fan-out was
382    /// recorded and reached no registered worker here; absent when it was not
383    /// recorded (a tap went live while the publish ran, or the tap could not
384    /// lend it a buffer), so a reader shows its receivers as not known rather
385    /// than as none.
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub to_stations: Option<Vec<String>>,
388    /// Where it went, in order.
389    pub hops: Vec<Hop>,
390    /// The id of the message it answers (its causal parent), when it has one.
391    #[serde(skip_serializing_if = "Option::is_none")]
392    pub reply_to: Option<String>,
393    /// Held by a rule. Rules do not exist yet, so always false.
394    pub held: bool,
395    /// Where a rule diverted it. Rules do not exist yet, so always absent.
396    #[serde(skip_serializing_if = "Option::is_none")]
397    pub diverted: Option<String>,
398}
399
400/// One `console.tap.<target>` record: one batch, every message in it admitted
401/// at the one divisor `sampled`.
402#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
403pub struct TapBatch {
404    v: u64,
405    #[serde(rename = "type")]
406    record_type: &'static str,
407    /// The node or stream the tap is on.
408    pub target: String,
409    /// One more than this tap's previous batch, from 1.
410    pub seq: u64,
411    /// N in "sampled 1 in N" for every message in this batch.
412    pub sampled: NonZeroU32,
413    /// Messages evicted from the tap's ring since its previous batch.
414    pub dropped: u64,
415    /// The messages, oldest first.
416    pub messages: Vec<TapMessage>,
417}
418
419impl TapBatch {
420    /// A batch for `target`.
421    #[must_use]
422    pub const fn new(
423        target: String,
424        seq: u64,
425        sampled: NonZeroU32,
426        dropped: u64,
427        messages: Vec<TapMessage>,
428    ) -> Self {
429        Self {
430            v: RECORD_VERSION,
431            record_type: "tap.batch",
432            target,
433            seq,
434            sampled,
435            dropped,
436            messages,
437        }
438    }
439}
440
441/// The canonical bytes of a record: through a JSON value, so every key is in
442/// sorted order at every depth, as the schema normaliser writes them.
443///
444/// # Errors
445/// Returns [`ConsoleError::Encode`] naming the record when serialisation fails.
446pub fn canonical_bytes<Record: Serialize>(
447    record: &Record,
448    kind: &'static str,
449) -> Result<Vec<u8>, ConsoleError> {
450    serde_json::to_value(record)
451        .and_then(|value| serde_json::to_vec(&value))
452        .map_err(|source| ConsoleError::Encode {
453            record: kind,
454            source,
455        })
456}
457
458/// The `console.tap.<target>` record for one tap batch on `device`.
459///
460/// Every message is named, timed and attributed in the console's terms. A
461/// payload, which only a test grant can carry today, does not ride the
462/// record: the console's reader names no field for one.
463///
464/// # Errors
465/// Returns [`ConsoleError::Clock`] when a message's time passes u64
466/// milliseconds.
467pub fn tap_batch_record(
468    device: &str,
469    target: &str,
470    batch: &tap::TapBatch,
471    wall: &WallBase,
472) -> Result<TapBatch, ConsoleError> {
473    let messages = batch
474        .messages
475        .iter()
476        .map(|message| tap_message(device, message, wall))
477        .collect::<Result<Vec<_>, _>>()?;
478    Ok(TapBatch::new(
479        target.to_owned(),
480        batch.seq,
481        batch.sampled,
482        batch.dropped,
483        messages,
484    ))
485}
486
487fn tap_message(
488    device: &str,
489    message: &tap::TapMessage,
490    wall: &WallBase,
491) -> Result<TapMessage, ConsoleError> {
492    Ok(TapMessage {
493        id: match message.ident {
494            TapIdent::Message(id) => format!("{device}:{id}"),
495            TapIdent::Observation(observation) => format!("{device}:x{observation}"),
496        },
497        subject: message.subject.clone(),
498        size: message.size,
499        outcome: message.outcome.into(),
500        t: wall.unix_ms_at(message.seen_at)?,
501        from: match message.source {
502            TapSource::Connection(connection) => connection.to_string(),
503            TapSource::Server => message
504                .station
505                .as_deref()
506                .map_or_else(|| format!("liminal.{device}"), str::to_owned),
507        },
508        from_station: message.station.as_deref().map(str::to_owned),
509        to: device.to_owned(),
510        to_stations: message.to_stations.as_deref().map(|stations| {
511            stations
512                .iter()
513                .map(|reached| reached.station.to_string())
514                .collect()
515        }),
516        hops: message
517            .hops
518            .iter()
519            .map(|hop| Hop {
520                node: hop.node.to_string(),
521                at_ms: hop.at_ms,
522            })
523            .collect(),
524        reply_to: message.reply_to.clone(),
525        held: message.held,
526        diverted: None,
527    })
528}