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}