Skip to main content

liminal_server/console/records/
tap_records.rs

1//! Tap records preserve channel facts and add terminal call facts.
2
3use super::RECORD_VERSION;
4use crate::console::{ConsoleError, wall::WallBase};
5use crate::tap::{self, TapIdent, TapOutcome, TapSource};
6use serde::Serialize;
7use std::num::NonZeroU32;
8
9/// What became of a tapped message, in the console's words.
10///
11/// Channel delivery and terminal call correlation name their outcomes here.
12#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize)]
13#[serde(rename_all = "lowercase")]
14pub enum Outcome {
15    /// Taken by at least one subscriber.
16    Delivered,
17    /// The correlated call reply arrived.
18    Answered,
19    /// No reply arrived before the call deadline.
20    Unanswered,
21    /// Admitted, and no subscriber matched.
22    Nobody,
23    /// Admitted; subscribers matched and none took it.
24    Unaccepted,
25    /// Refused at admission, by the schema, or by the library.
26    Refused,
27    /// The store could not say whether the append landed.
28    Unresolved,
29}
30
31impl From<TapOutcome> for Outcome {
32    fn from(outcome: TapOutcome) -> Self {
33        match outcome {
34            TapOutcome::Answered => Self::Answered,
35            TapOutcome::Unanswered => Self::Unanswered,
36            TapOutcome::Delivered => Self::Delivered,
37            TapOutcome::NoSubscriber => Self::Nobody,
38            TapOutcome::NoneAccepted => Self::Unaccepted,
39            TapOutcome::Unresolved => Self::Unresolved,
40            TapOutcome::Refused => Self::Refused,
41        }
42    }
43}
44
45/// One point a message crossed.
46#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
47pub struct Hop {
48    /// The device id.
49    pub node: String,
50    /// Milliseconds after the message's `t`.
51    pub at_ms: u64,
52}
53
54/// The facts a tap carries about one message. Payloads never ride here.
55#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
56pub struct TapMessage {
57    /// `<device>:<message id>` when admitted, `<device>:x<observation>`
58    /// otherwise.
59    pub id: String,
60    /// The subject (channel name).
61    pub subject: String,
62    /// Payload size in bytes.
63    pub size: u64,
64    /// What became of it.
65    pub outcome: Outcome,
66    /// Wall time it was first seen, in milliseconds since the epoch.
67    pub t: u64,
68    /// The publishing connection's id, or `liminal.<device>` for the server.
69    pub from: String,
70    /// The station that published it: the registered worker identity of the
71    /// publishing connection. Absent when that connection is not a registered
72    /// worker, and for the server's own records.
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub from_station: Option<String>,
75    /// This device's id.
76    pub to: String,
77    /// The stations that received it: each registered worker whose
78    /// subscription's inbox on this device queued the message, once, in
79    /// delivery order. A subscriber whose inbox refused it is not here; nor is
80    /// one that is no registered worker (it is counted in the outcome, never
81    /// named), nor a receiver on another device. `[]` when the fan-out was
82    /// recorded and reached no registered worker here; absent when it was not
83    /// recorded (a tap went live while the publish ran, or the tap could not
84    /// lend it a buffer), so a reader shows its receivers as not known rather
85    /// than as none.
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub to_stations: Option<Vec<String>>,
88    /// Where it went, in order.
89    pub hops: Vec<Hop>,
90    /// The id of the message it answers (its causal parent), when it has one.
91    #[serde(skip_serializing_if = "Option::is_none")]
92    pub reply_to: Option<String>,
93    /// Held by a rule. Rules do not exist yet, so always false.
94    pub held: bool,
95    /// Where a rule diverted it. Rules do not exist yet, so always absent.
96    #[serde(skip_serializing_if = "Option::is_none")]
97    pub diverted: Option<String>,
98    /// Measured round trip and named refusal of a call.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub fields: Option<tap::message::CallDetails>,
101}
102
103/// One `console.tap.<target>` record: one batch, every message in it admitted
104/// at the one divisor `sampled`.
105#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
106pub struct TapBatch {
107    v: u64,
108    #[serde(rename = "type")]
109    record_type: &'static str,
110    /// The node or stream the tap is on.
111    pub target: String,
112    /// One more than this tap's previous batch, from 1.
113    pub seq: u64,
114    /// N in "sampled 1 in N" for every message in this batch.
115    pub sampled: NonZeroU32,
116    /// Messages evicted from the tap's ring since its previous batch.
117    pub dropped: u64,
118    /// The messages, oldest first.
119    pub messages: Vec<TapMessage>,
120}
121
122impl TapBatch {
123    /// A batch for `target`.
124    #[must_use]
125    pub const fn new(
126        target: String,
127        seq: u64,
128        sampled: NonZeroU32,
129        dropped: u64,
130        messages: Vec<TapMessage>,
131    ) -> Self {
132        Self {
133            v: RECORD_VERSION,
134            record_type: "tap.batch",
135            target,
136            seq,
137            sampled,
138            dropped,
139            messages,
140        }
141    }
142}
143
144/// The `console.tap.<target>` record for one tap batch on `device`.
145///
146/// Every message is named, timed and attributed in the console's terms. A
147/// payload, which only a test grant can carry today, does not ride the
148/// record: the console's reader names no field for one.
149///
150/// # Errors
151/// Returns [`ConsoleError::Clock`] when a message's time passes u64
152/// milliseconds.
153pub fn tap_batch_record(
154    device: &str,
155    target: &str,
156    batch: &tap::TapBatch,
157    wall: &WallBase,
158) -> Result<TapBatch, ConsoleError> {
159    let messages = batch
160        .messages
161        .iter()
162        .map(|message| tap_message(device, message, wall))
163        .collect::<Result<Vec<_>, _>>()?;
164    Ok(TapBatch::new(
165        target.to_owned(),
166        batch.seq,
167        batch.sampled,
168        batch.dropped,
169        messages,
170    ))
171}
172
173fn tap_message(
174    device: &str,
175    message: &tap::TapMessage,
176    wall: &WallBase,
177) -> Result<TapMessage, ConsoleError> {
178    Ok(TapMessage {
179        id: match message.ident {
180            TapIdent::Call {
181                connection,
182                operation,
183            } => format!("{device}:call:{connection}:{operation}"),
184            TapIdent::Message(id) => format!("{device}:{id}"),
185            TapIdent::Observation(observation) => format!("{device}:x{observation}"),
186        },
187        subject: message.subject.clone(),
188        size: message.size,
189        outcome: message.outcome.into(),
190        t: wall.unix_ms_at(message.seen_at)?,
191        from: match message.source {
192            TapSource::Connection(connection) => connection.to_string(),
193            TapSource::Server => message
194                .station
195                .as_deref()
196                .map_or_else(|| format!("liminal.{device}"), str::to_owned),
197        },
198        from_station: message.station.as_deref().map(str::to_owned),
199        to: device.to_owned(),
200        to_stations: message.to_stations.as_deref().map(|stations| {
201            stations
202                .iter()
203                .map(|reached| reached.station.to_string())
204                .collect()
205        }),
206        hops: message
207            .hops
208            .iter()
209            .map(|hop| Hop {
210                node: hop.node.to_string(),
211                at_ms: hop.at_ms,
212            })
213            .collect(),
214        reply_to: message.reply_to.clone(),
215        held: message.held,
216        diverted: None,
217        fields: message.call.clone(),
218    })
219}