notify router: deliveries log, dry-run hit counts, api and page
This commit is contained in:
parent
6dd9d0086e
commit
ed1fd053e7
1 changed files with 1848 additions and 40 deletions
134
swarm-notify-router/src/log.rs
Normal file
134
swarm-notify-router/src/log.rs
Normal 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())
|
||||
}
|
||||
Loading…
Reference in a new issue