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 /// SRV-027 R3: the subjects a registered worker serves (its
105 /// registration's task queue). Absent on a station that is not a worker.
106 #[serde(skip_serializing_if = "Option::is_none")]
107 pub subjects: Option<Vec<String>>,
108 /// SRV-027 R3: what a registered worker accepts and returns, each
109 /// activity's schemas as the exact strings it registered; empty for a
110 /// worker that declared none (raw bytes in and out). Absent on a
111 /// station that is not a worker.
112 #[serde(skip_serializing_if = "Option::is_none")]
113 pub activities: Option<Vec<StationActivity>>,
114}
115
116/// One activity a worker declared at registration (SRV-027 R3): its name and
117/// its input and output schemas, carried as the strings the registration
118/// held, never parsed or normalised here.
119#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
120pub struct StationActivity {
121 /// The activity's name.
122 pub name: String,
123 /// The input schema as registered.
124 pub input_schema_json: String,
125 /// The output schema as registered.
126 pub output_schema_json: String,
127}
128
129/// What a station is. Only services have a source until the services
130/// registry exists.
131#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
132#[serde(rename_all = "lowercase")]
133pub enum StationKind {
134 /// A registered conversation responder.
135 Service,
136 /// A registered worker.
137 Worker,
138 /// This server's persistent store, which a durable line's messages
139 /// reach before the fan-out (lane brief ruling 5).
140 Depot,
141 /// This server's exchange, the fan-out every line passes; its id is the
142 /// device's (lane brief ruling 2).
143 Exchange,
144}
145
146/// A station's state. A registered responder is live; nothing in the server
147/// is asleep, held or retired as a station yet.
148#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
149#[serde(rename_all = "lowercase")]
150pub enum StationState {
151 /// Registered and answering.
152 Live,
153}
154
155/// A line: a stream on the roster.
156#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
157pub struct Line {
158 /// The channel name.
159 pub id: String,
160 /// Always a stream: calls, archives and exports have no source.
161 pub kind: LineKind,
162 /// How long its messages are kept.
163 pub life: LineLife,
164 /// Whether it admits.
165 pub state: LineState,
166 /// The stations it passes, in order.
167 pub stops: Vec<String>,
168}
169
170/// What a line is.
171#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
172#[serde(rename_all = "lowercase")]
173pub enum LineKind {
174 /// A channel.
175 Stream,
176}
177
178/// How long a line keeps its messages, from the channel's mode.
179#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
180#[serde(rename_all = "lowercase")]
181pub enum LineLife {
182 /// Ephemeral: messages pass and are not stored.
183 Passing,
184 /// Durable: messages are stored.
185 Kept,
186}
187
188/// Whether a line admits.
189#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
190#[serde(rename_all = "lowercase")]
191pub enum LineState {
192 /// Admitting publishes and subscribes.
193 Live,
194 /// Quiesced: refusing new publishes and subscribes.
195 Quiesced,
196}
197
198/// A link to a cluster peer, present while the peer is.
199#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
200pub struct Link {
201 /// The link id, `<this device>~<peer>`.
202 pub id: String,
203 /// The peer's name as shown.
204 pub name: String,
205 /// How the two devices are joined.
206 pub kind: LinkKind,
207 /// The link's state.
208 pub state: LinkState,
209 /// Round trip in milliseconds. No source: always `null`, never `0`.
210 pub rtt_ms: Option<u64>,
211 /// The two device ids it joins, in sorted order, so the same link has
212 /// the same bytes whichever end publishes it.
213 pub ends: [String; 2],
214}
215
216impl Link {
217 /// The link from `local` to `peer`, up, with no round trip measured.
218 #[must_use]
219 pub fn to_peer(local: &str, peer: &str) -> Self {
220 let ends = if peer < local {
221 [peer.to_owned(), local.to_owned()]
222 } else {
223 [local.to_owned(), peer.to_owned()]
224 };
225 Self {
226 id: format!("{local}~{peer}"),
227 name: peer.to_owned(),
228 kind: LinkKind::SameNetwork,
229 state: LinkState::Up,
230 rtt_ms: None,
231 ends,
232 }
233 }
234}
235
236/// How two devices are joined.
237#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
238pub enum LinkKind {
239 /// Members of one cluster.
240 #[serde(rename = "same network")]
241 SameNetwork,
242}
243
244/// A link's state. A departed peer's link is absent, not down: membership
245/// keeps no record of why it left.
246#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
247#[serde(rename_all = "lowercase")]
248pub enum LinkState {
249 /// The peer is a member.
250 Up,
251}
252
253/// A live connection to this server.
254#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
255pub struct Connection {
256 /// The connection id, as a tap message's `from` names it.
257 pub id: String,
258 /// The transport it arrived on.
259 pub mount: Mount,
260 /// The participant its pass names, when it presented one.
261 #[serde(skip_serializing_if = "Option::is_none")]
262 pub participant: Option<String>,
263}
264
265/// The transport a connection arrived on.
266#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
267#[serde(rename_all = "lowercase")]
268pub enum Mount {
269 /// The TCP wire listener.
270 Tcp,
271 /// The WebSocket listener.
272 Websocket,
273 /// The in-process transport.
274 Loopback,
275}
276
277impl From<crate::server::mount::MountKind> for Mount {
278 fn from(mount: crate::server::mount::MountKind) -> Self {
279 match mount {
280 crate::server::mount::MountKind::Tcp => Self::Tcp,
281 crate::server::mount::MountKind::WebSocket => Self::Websocket,
282 crate::server::mount::MountKind::Loopback => Self::Loopback,
283 }
284 }
285}
286
287/// One `console.topology` record: the whole document as it stands.
288#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
289pub struct TopologyDocument {
290 v: u64,
291 #[serde(rename = "type")]
292 record_type: &'static str,
293 /// One more than the previous document's, from 1 at start.
294 pub rev: u64,
295 /// The id of the device this server is, one of `devices`' ids (lane
296 /// brief ruling 1, Hermes 7 Oct 03:51): named once here, never as a flag
297 /// on each device.
298 pub this_device: String,
299 /// This device, then each peer.
300 pub devices: Vec<Device>,
301 /// The registered services.
302 pub stations: Vec<Station>,
303 /// Every roster channel outside `console.`.
304 pub lines: Vec<Line>,
305 /// One per peer.
306 pub links: Vec<Link>,
307 /// One per live connection.
308 pub connections: Vec<Connection>,
309}
310
311impl TopologyDocument {
312 /// A document at `rev`, from the server that is device `this_device`.
313 #[must_use]
314 pub const fn new(
315 rev: u64,
316 this_device: String,
317 devices: Vec<Device>,
318 stations: Vec<Station>,
319 lines: Vec<Line>,
320 peer_links: Vec<Link>,
321 connections: Vec<Connection>,
322 ) -> Self {
323 Self {
324 v: RECORD_VERSION,
325 record_type: "topology",
326 rev,
327 this_device,
328 devices,
329 stations,
330 lines,
331 links: peer_links,
332 connections,
333 }
334 }
335}
336
337/// What became of a tapped message, in the console's words.
338///
339/// Only the words the server has an honest cause for: the console's call words (reply,
340/// died, deadline), deferred, dropped and redelivered have none here.
341#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
342#[serde(rename_all = "lowercase")]
343pub enum Outcome {
344 /// Taken by at least one subscriber.
345 Delivered,
346 /// Admitted, and no subscriber matched.
347 Nobody,
348 /// Admitted; subscribers matched and none took it.
349 Unaccepted,
350 /// Refused at admission, by the schema, or by the library.
351 Refused,
352 /// The store could not say whether the append landed.
353 Unresolved,
354}
355
356impl From<TapOutcome> for Outcome {
357 fn from(outcome: TapOutcome) -> Self {
358 match outcome {
359 TapOutcome::Delivered => Self::Delivered,
360 TapOutcome::NoSubscriber => Self::Nobody,
361 TapOutcome::NoneAccepted => Self::Unaccepted,
362 TapOutcome::Unresolved => Self::Unresolved,
363 TapOutcome::Refused => Self::Refused,
364 }
365 }
366}
367
368/// One point a message crossed.
369#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
370pub struct Hop {
371 /// The device id.
372 pub node: String,
373 /// Milliseconds after the message's `t`.
374 pub at_ms: u64,
375}
376
377/// The facts a tap carries about one message. Payloads never ride here.
378#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
379pub struct TapMessage {
380 /// `<device>:<message id>` when admitted, `<device>:x<observation>`
381 /// otherwise.
382 pub id: String,
383 /// The subject (channel name).
384 pub subject: String,
385 /// Payload size in bytes.
386 pub size: u64,
387 /// What became of it.
388 pub outcome: Outcome,
389 /// Wall time it was first seen, in milliseconds since the epoch.
390 pub t: u64,
391 /// The publishing connection's id, or `liminal.<device>` for the server.
392 pub from: String,
393 /// The station that published it: the registered worker identity of the
394 /// publishing connection. Absent when that connection is not a registered
395 /// worker, and for the server's own records.
396 #[serde(skip_serializing_if = "Option::is_none")]
397 pub from_station: Option<String>,
398 /// This device's id.
399 pub to: String,
400 /// The stations that received it: each registered worker whose
401 /// subscription's inbox on this device queued the message, once, in
402 /// delivery order. A subscriber whose inbox refused it is not here; nor is
403 /// one that is no registered worker (it is counted in the outcome, never
404 /// named), nor a receiver on another device. `[]` when the fan-out was
405 /// recorded and reached no registered worker here; absent when it was not
406 /// recorded (a tap went live while the publish ran, or the tap could not
407 /// lend it a buffer), so a reader shows its receivers as not known rather
408 /// than as none.
409 #[serde(skip_serializing_if = "Option::is_none")]
410 pub to_stations: Option<Vec<String>>,
411 /// Where it went, in order.
412 pub hops: Vec<Hop>,
413 /// The id of the message it answers (its causal parent), when it has one.
414 #[serde(skip_serializing_if = "Option::is_none")]
415 pub reply_to: Option<String>,
416 /// Held by a rule. Rules do not exist yet, so always false.
417 pub held: bool,
418 /// Where a rule diverted it. Rules do not exist yet, so always absent.
419 #[serde(skip_serializing_if = "Option::is_none")]
420 pub diverted: Option<String>,
421}
422
423/// One `console.tap.<target>` record: one batch, every message in it admitted
424/// at the one divisor `sampled`.
425#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
426pub struct TapBatch {
427 v: u64,
428 #[serde(rename = "type")]
429 record_type: &'static str,
430 /// The node or stream the tap is on.
431 pub target: String,
432 /// One more than this tap's previous batch, from 1.
433 pub seq: u64,
434 /// N in "sampled 1 in N" for every message in this batch.
435 pub sampled: NonZeroU32,
436 /// Messages evicted from the tap's ring since its previous batch.
437 pub dropped: u64,
438 /// The messages, oldest first.
439 pub messages: Vec<TapMessage>,
440}
441
442impl TapBatch {
443 /// A batch for `target`.
444 #[must_use]
445 pub const fn new(
446 target: String,
447 seq: u64,
448 sampled: NonZeroU32,
449 dropped: u64,
450 messages: Vec<TapMessage>,
451 ) -> Self {
452 Self {
453 v: RECORD_VERSION,
454 record_type: "tap.batch",
455 target,
456 seq,
457 sampled,
458 dropped,
459 messages,
460 }
461 }
462}
463
464/// The canonical bytes of a record: through a JSON value, so every key is in
465/// sorted order at every depth, as the schema normaliser writes them.
466///
467/// # Errors
468/// Returns [`ConsoleError::Encode`] naming the record when serialisation fails.
469pub fn canonical_bytes<Record: Serialize>(
470 record: &Record,
471 kind: &'static str,
472) -> Result<Vec<u8>, ConsoleError> {
473 serde_json::to_value(record)
474 .and_then(|value| serde_json::to_vec(&value))
475 .map_err(|source| ConsoleError::Encode {
476 record: kind,
477 source,
478 })
479}
480
481/// The `console.tap.<target>` record for one tap batch on `device`.
482///
483/// Every message is named, timed and attributed in the console's terms. A
484/// payload, which only a test grant can carry today, does not ride the
485/// record: the console's reader names no field for one.
486///
487/// # Errors
488/// Returns [`ConsoleError::Clock`] when a message's time passes u64
489/// milliseconds.
490pub fn tap_batch_record(
491 device: &str,
492 target: &str,
493 batch: &tap::TapBatch,
494 wall: &WallBase,
495) -> Result<TapBatch, ConsoleError> {
496 let messages = batch
497 .messages
498 .iter()
499 .map(|message| tap_message(device, message, wall))
500 .collect::<Result<Vec<_>, _>>()?;
501 Ok(TapBatch::new(
502 target.to_owned(),
503 batch.seq,
504 batch.sampled,
505 batch.dropped,
506 messages,
507 ))
508}
509
510fn tap_message(
511 device: &str,
512 message: &tap::TapMessage,
513 wall: &WallBase,
514) -> Result<TapMessage, ConsoleError> {
515 Ok(TapMessage {
516 id: match message.ident {
517 TapIdent::Message(id) => format!("{device}:{id}"),
518 TapIdent::Observation(observation) => format!("{device}:x{observation}"),
519 },
520 subject: message.subject.clone(),
521 size: message.size,
522 outcome: message.outcome.into(),
523 t: wall.unix_ms_at(message.seen_at)?,
524 from: match message.source {
525 TapSource::Connection(connection) => connection.to_string(),
526 TapSource::Server => message
527 .station
528 .as_deref()
529 .map_or_else(|| format!("liminal.{device}"), str::to_owned),
530 },
531 from_station: message.station.as_deref().map(str::to_owned),
532 to: device.to_owned(),
533 to_stations: message.to_stations.as_deref().map(|stations| {
534 stations
535 .iter()
536 .map(|reached| reached.station.to_string())
537 .collect()
538 }),
539 hops: message
540 .hops
541 .iter()
542 .map(|hop| Hop {
543 node: hop.node.to_string(),
544 at_ms: hop.at_ms,
545 })
546 .collect(),
547 reply_to: message.reply_to.clone(),
548 held: message.held,
549 diverted: None,
550 })
551}