Watch
0
0
Fork
You've already forked hyperhive
0

notify router: deliveries log, dry-run hit counts, api and page

This commit is contained in:
atlas 2026-10-24 22:13:07 +02:00
commit ed1fd053e7

View file

@ -0,0 +1,134 @@
//! The deliveries log: one row on the `NOTIFY_ROUTER_LOG` stream for each
//! delivery the router makes (`swarm_wire::router_api`), and reading the
//! newest rows back.
//!
//! The stream keeps the newest 500 rows. Appending is best effort: a log that
//! cannot be written is reported and never holds up a delivery.
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Context as _, Result};
use async_nats::jetstream::stream::{Config, DiscardPolicy, Stream};
use async_nats::jetstream::{self};
use futures_util::{StreamExt as _, stream};
use swarm_wire::router_api::{Kind, LOG_MAX_MSGS, LOG_STREAM, LOG_SUBJECT, Row};
/// How many messages are fetched at once.
const FETCH_AT_ONCE: usize = 16;
/// Open the stream, creating it if nothing has yet. The router is its one
/// writer and its one creator.
pub async fn open_or_create(js: &jetstream::Context) -> Result<Stream> {
match js.get_stream(LOG_STREAM).await {
Ok(stream) => Ok(stream),
Err(e) => {
tracing::info!(stream = LOG_STREAM, reason = %e, "notify router: log stream not available, creating it");
js.create_stream(Config {
name: LOG_STREAM.to_owned(),
description: Some("Notification router deliveries, newest 500".to_owned()),
subjects: vec![LOG_SUBJECT.to_owned()],
max_messages: LOG_MAX_MSGS,
discard: DiscardPolicy::Old,
..Default::default()
})
.await
.context("creating the log stream")
}
}
}
/// The event a delivery answers.
#[derive(Clone, Debug)]
pub struct Cause {
pub event: String,
pub repo: String,
pub number: u64,
}
impl Cause {
/// A row of `kind` made at `flow`'s `node` for `target`, which went
/// through when `result` is `Ok`.
pub fn row(
&self,
flow: &str,
node: &str,
kind: Kind,
target: &str,
result: Result<String, String>,
) -> Row {
let (ok, detail) = match result {
Ok(detail) => (true, detail),
Err(why) => (false, why),
};
Row {
at: SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_or(0, |d| d.as_secs()),
flow: flow.to_owned(),
node: node.to_owned(),
event: self.event.clone(),
repo: self.repo.clone(),
number: self.number,
kind,
target: target.to_owned(),
ok,
detail,
}
}
}
/// Append `row`; a failure is logged and dropped.
pub async fn append(js: &jetstream::Context, row: &Row) {
let body = serde_json::to_vec(row).expect("a row encodes");
let sent = async {
js.publish(LOG_SUBJECT, body.into())
.await
.context("publishing")?
.await
.context("waiting for the ack")
};
if let Err(e) = sent.await {
tracing::warn!(error = format!("{e:#}"), flow = %row.flow, "notify router: could not log a delivery");
}
}
/// The payloads of the newest `limit` messages of `stream`, newest first,
/// with their subjects. A sequence that no longer exists is skipped.
pub async fn newest(
js: &jetstream::Context,
stream: &str,
limit: usize,
) -> Result<Vec<(String, Vec<u8>)>> {
let stream = js
.get_stream(stream)
.await
.with_context(|| format!("opening {stream}"))?;
let state = &stream.cached_info().state;
let (first, last) = (state.first_sequence, state.last_sequence);
let count = u64::try_from(limit).unwrap_or(u64::MAX);
let from = first.max((last + 1).saturating_sub(count));
let seqs = (from..=last).rev();
let fetched: Vec<_> = stream::iter(seqs)
.map(|seq| {
let stream = &stream;
async move { stream.get_raw_message(seq).await }
})
.buffered(FETCH_AT_ONCE)
.collect()
.await;
Ok(fetched
.into_iter()
.filter_map(Result::ok)
.map(|m| (m.subject.to_string(), m.payload.to_vec()))
.collect())
}
/// The newest `limit` rows of the log, newest first. A row that does not
/// decode is skipped.
pub async fn recent(js: &jetstream::Context, limit: usize) -> Result<Vec<Row>> {
Ok(newest(js, LOG_STREAM, limit)
.await?
.into_iter()
.filter_map(|(_, body)| serde_json::from_slice(&body).ok())
.collect())
}