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
24 changed files with 1848 additions and 40 deletions

View file

@ -21,6 +21,7 @@ pub mod notices;
pub mod notify_flows;
pub mod notify_router;
pub mod review_classifier;
pub mod router_api;
pub mod router_sink;
pub mod router_todo;
pub mod subagent_term;

View file

@ -0,0 +1,160 @@
//! What the notification router records and answers, for swarm-controller's
//! `GET /api/notify/deliveries` and `GET /api/notify/dry-run`.
//!
//! The router appends one [`Row`] to the [`LOG_STREAM`] stream for each
//! delivery it makes, and keeps the newest [`LOG_MAX_MSGS`]. The controller
//! asks it two things as NATS requests: the newest rows ([`DELIVERIES_SUBJECT`])
//! and, for a flows document, how many of the newest forge events reach each
//! node ([`DRY_RUN_SUBJECT`]), run without delivering anything.
//!
//! Three crates name these: the router records and answers, the controller
//! asks, and the auth-callout responder grants each its side. A router that
//! predates this answers neither, so the controller reports that.
use std::time::Duration;
use serde::{Deserialize, Serialize};
use crate::notify_flows::Document;
/// The stream holding the deliveries log.
pub const LOG_STREAM: &str = "NOTIFY_ROUTER_LOG";
/// The one subject [`LOG_STREAM`] captures. The router is its one publisher.
pub const LOG_SUBJECT: &str = "$SWARM.router.log";
/// How many rows the stream keeps; the oldest goes first.
pub const LOG_MAX_MSGS: i64 = 500;
/// The request for the newest [`Row`]s ([`DeliveriesRequest`]).
pub const DELIVERIES_SUBJECT: &str = "$SWARM.router.api.deliveries";
/// The request to run a document over the newest forge events
/// ([`DryRunRequest`]).
pub const DRY_RUN_SUBJECT: &str = "$SWARM.router.api.dry-run";
/// How many of the newest forge events a dry run covers.
pub const DRY_RUN_EVENTS: usize = 50;
/// The rows a request gets when it names no limit.
pub const DEFAULT_ROWS: usize = 100;
/// How long the controller waits for the router. The router's reply grant
/// lasts this long.
pub const REPLY_DEADLINE: Duration = Duration::from_secs(10);
/// What a delivery was.
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
#[serde(rename_all = "snake_case")]
pub enum Kind {
/// A todo op queued for an agent.
Todo,
/// A request to the controller for a review.
RequestReviewer,
}
/// One delivery the router made.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct Row {
/// Unix seconds.
pub at: u64,
pub flow: String,
pub node: String,
/// The forge event type, such as `issue_assign`.
pub event: String,
/// `owner/name` of the repository the event concerns.
pub repo: String,
/// The issue or pull request number.
pub number: u64,
pub kind: Kind,
/// The agent that got the todo, or the reviewer asked for.
pub target: String,
/// Whether it went through. A review the controller skipped went through.
pub ok: bool,
/// What happened: the op and key, the controller's answer, or the error.
pub detail: String,
}
/// Ask for the newest rows, newest first.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct DeliveriesRequest {
/// At most this many; [`DEFAULT_ROWS`] when absent. The log keeps
/// [`LOG_MAX_MSGS`], so no answer is longer.
#[serde(default)]
pub limit: Option<usize>,
}
/// The router's answer to a [`DeliveriesRequest`].
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct Deliveries {
/// Newest first.
pub rows: Vec<Row>,
}
/// Run `document` over the newest [`DRY_RUN_EVENTS`] forge events.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct DryRunRequest {
pub document: Document,
}
/// How many events reached one node.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct NodeHits {
pub node: String,
/// Events that reached the node. A `match` node counts those that passed
/// it, a `source` node those it starts from.
pub hits: u64,
}
/// The hits of one flow that would run.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct FlowHits {
pub flow: String,
/// Every node of the flow, those no event reached with `0`.
pub nodes: Vec<NodeHits>,
}
/// The router's answer to a [`DryRunRequest`] it ran.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[cfg_attr(feature = "schema", derive(utoipa::ToSchema))]
pub struct DryRun {
/// How many events were run, at most [`DRY_RUN_EVENTS`]: fewer when the
/// stream holds fewer.
pub events: u64,
/// The flows the document turns on: the built-ins it leaves on and its
/// custom flows that are enabled.
pub flows: Vec<FlowHits>,
}
/// The router's answer to a request it could not serve.
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct Refused {
pub error: String,
}
#[cfg(test)]
mod tests {
use super::*;
/// The log keeps the rows the deliveries view can ask for.
#[test]
fn a_request_never_asks_for_more_than_the_stream_keeps() {
assert!(i64::try_from(DEFAULT_ROWS).expect("small") <= LOG_MAX_MSGS);
}
#[test]
fn a_request_without_a_limit_decodes() {
let r: DeliveriesRequest = serde_json::from_str("{}").expect("empty request");
assert_eq!(r.limit, None);
}
#[test]
fn the_reply_deadline_leaves_the_controller_time_to_answer() {
assert!(REPLY_DEADLINE < Duration::from_secs(30));
}
}