swarm-wire: take notices's wire types, fmt the moved imports
This commit is contained in:
parent
e111807f59
commit
fc72bea151
12 changed files with 72 additions and 86 deletions
|
|
@ -33,9 +33,9 @@ use serde::Serialize;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::time::{Interval, MissedTickBehavior};
|
use tokio::time::{Interval, MissedTickBehavior};
|
||||||
|
|
||||||
use swarm_wire::wanted::AgentState;
|
|
||||||
use swarm_wire::agent_inference::Effective;
|
use swarm_wire::agent_inference::Effective;
|
||||||
use swarm_wire::agent_login::Login;
|
use swarm_wire::agent_login::Login;
|
||||||
|
use swarm_wire::wanted::AgentState;
|
||||||
|
|
||||||
use crate::events::{Bus, BusEvent, LiveEvent, TurnState};
|
use crate::events::{Bus, BusEvent, LiveEvent, TurnState};
|
||||||
use crate::login::LoginState;
|
use crate::login::LoginState;
|
||||||
|
|
@ -337,8 +337,8 @@ mod tests {
|
||||||
use crate::events::{BusEvent, LiveEvent, TurnState};
|
use crate::events::{BusEvent, LiveEvent, TurnState};
|
||||||
use crate::login::LoginState;
|
use crate::login::LoginState;
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use swarm_wire::wanted::AgentState;
|
|
||||||
use swarm_wire::agent_inference::{Effective, Source};
|
use swarm_wire::agent_inference::{Effective, Source};
|
||||||
|
use swarm_wire::wanted::AgentState;
|
||||||
use tokio::sync::broadcast;
|
use tokio::sync::broadcast;
|
||||||
use tokio::time::{Duration, Instant};
|
use tokio::time::{Duration, Instant};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
//! a status snapshot has a current value a late reader can always ask
|
//! a status snapshot has a current value a late reader can always ask
|
||||||
//! for; a lifecycle notice ("container crashed at 04:12") does not — miss
|
//! for; a lifecycle notice ("container crashed at 04:12") does not — miss
|
||||||
//! it and there is nothing left to read later that says it happened. See
|
//! it and there is nothing left to read later that says it happened. See
|
||||||
//! [`swarm_queue_client::notices`] for the stream this publishes into.
|
//! [`swarm_queue_client::notices`] for opening the stream this publishes into.
|
||||||
//!
|
//!
|
||||||
//! **Best-effort, never fatal to the caller.** A hive with no queue
|
//! **Best-effort, never fatal to the caller.** A hive with no queue
|
||||||
//! coordinates is a silent no-op (the ordinary case). A hive that cannot
|
//! coordinates is a silent no-op (the ordinary case). A hive that cannot
|
||||||
|
|
@ -129,7 +129,7 @@ async fn publish(
|
||||||
.context("serialising the notice")?;
|
.context("serialising the notice")?;
|
||||||
|
|
||||||
let js = async_nats::jetstream::new(client.clone());
|
let js = async_nats::jetstream::new(client.clone());
|
||||||
js.publish(swarm_queue_client::notices::subject(hive), payload.into())
|
js.publish(swarm_wire::notices::subject(hive), payload.into())
|
||||||
.await
|
.await
|
||||||
.context("publishing the notice")?
|
.context("publishing the notice")?
|
||||||
.await
|
.await
|
||||||
|
|
|
||||||
|
|
@ -33,8 +33,8 @@ use std::collections::BTreeSet;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{Context, Result, bail};
|
use anyhow::{Context, Result, bail};
|
||||||
use swarm_wire::wanted::{AgentState, HiveWanted};
|
|
||||||
use swarm_secret_client::{SecretStore, mtls, queue};
|
use swarm_secret_client::{SecretStore, mtls, queue};
|
||||||
|
use swarm_wire::wanted::{AgentState, HiveWanted};
|
||||||
use x509_cert::der::DecodePem as _;
|
use x509_cert::der::DecodePem as _;
|
||||||
|
|
||||||
use crate::wanted::WantedWriter;
|
use crate::wanted::WantedWriter;
|
||||||
|
|
|
||||||
|
|
@ -141,10 +141,9 @@ impl AgentStatusReader {
|
||||||
else {
|
else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
let payload = match serde_json::from_slice::<
|
let payload = match serde_json::from_slice::<swarm_wire::agent_status::AgentStatus>(
|
||||||
swarm_wire::agent_status::AgentStatus,
|
&entry.value,
|
||||||
>(&entry.value)
|
) {
|
||||||
{
|
|
||||||
Ok(status) => serde_json::to_value(status).ok(),
|
Ok(status) => serde_json::to_value(status).ok(),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(
|
tracing::warn!(
|
||||||
|
|
|
||||||
|
|
@ -78,9 +78,7 @@ pub(crate) struct AgentDeclaration {
|
||||||
state: String,
|
state: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn render(
|
pub(crate) fn render(declaration: &swarm_wire::wanted::HiveWanted) -> Vec<AgentDeclaration> {
|
||||||
declaration: &swarm_wire::wanted::HiveWanted,
|
|
||||||
) -> Vec<AgentDeclaration> {
|
|
||||||
declaration
|
declaration
|
||||||
.agents
|
.agents
|
||||||
.iter()
|
.iter()
|
||||||
|
|
@ -867,9 +865,7 @@ async fn placements_elsewhere<E, Fut>(
|
||||||
read_declared: impl FnOnce() -> Fut,
|
read_declared: impl FnOnce() -> Fut,
|
||||||
) -> Result<Vec<String>, E>
|
) -> Result<Vec<String>, E>
|
||||||
where
|
where
|
||||||
Fut: std::future::Future<
|
Fut: std::future::Future<Output = Result<Vec<(String, swarm_wire::wanted::HiveWanted)>, E>>,
|
||||||
Output = Result<Vec<(String, swarm_wire::wanted::HiveWanted)>, E>,
|
|
||||||
>,
|
|
||||||
{
|
{
|
||||||
let queued = snapshot_queued();
|
let queued = snapshot_queued();
|
||||||
let declared = read_declared().await?;
|
let declared = read_declared().await?;
|
||||||
|
|
@ -886,8 +882,7 @@ async fn declarations_elsewhere(
|
||||||
state: &AppState,
|
state: &AppState,
|
||||||
agent: &str,
|
agent: &str,
|
||||||
hive: &str,
|
hive: &str,
|
||||||
) -> Result<Vec<(String, swarm_wire::wanted::HiveWanted)>, problem_details::ProblemDetails>
|
) -> Result<Vec<(String, swarm_wire::wanted::HiveWanted)>, problem_details::ProblemDetails> {
|
||||||
{
|
|
||||||
read_declarations(state, state.hives.iter().filter(|h| h.name != hive))
|
read_declarations(state, state.hives.iter().filter(|h| h.name != hive))
|
||||||
.await
|
.await
|
||||||
.map_err(|(other, e)| {
|
.map_err(|(other, e)| {
|
||||||
|
|
@ -1709,10 +1704,9 @@ mod tests {
|
||||||
state: swarm_wire::wanted::AgentState,
|
state: swarm_wire::wanted::AgentState,
|
||||||
) -> (String, swarm_wire::wanted::HiveWanted) {
|
) -> (String, swarm_wire::wanted::HiveWanted) {
|
||||||
let mut declaration = swarm_wire::wanted::HiveWanted::default();
|
let mut declaration = swarm_wire::wanted::HiveWanted::default();
|
||||||
declaration.agents.insert(
|
declaration
|
||||||
agent.to_owned(),
|
.agents
|
||||||
swarm_wire::wanted::AgentWanted { state },
|
.insert(agent.to_owned(), swarm_wire::wanted::AgentWanted { state });
|
||||||
);
|
|
||||||
(hive.to_owned(), declaration)
|
(hive.to_owned(), declaration)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2010,8 +2004,7 @@ mod tests {
|
||||||
/// declaration before it is written, as it refuses creation.
|
/// declaration before it is written, as it refuses creation.
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn set_agent_state_refuses_when_the_wanted_state_is_unreadable() {
|
async fn set_agent_state_refuses_when_the_wanted_state_is_unreadable() {
|
||||||
let err =
|
let err = set_on_disconnected_pr1ma("atlas", swarm_wire::wanted::AgentState::Up).await;
|
||||||
set_on_disconnected_pr1ma("atlas", swarm_wire::wanted::AgentState::Up).await;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.status,
|
err.status,
|
||||||
Some(axum::http::StatusCode::SERVICE_UNAVAILABLE),
|
Some(axum::http::StatusCode::SERVICE_UNAVAILABLE),
|
||||||
|
|
@ -2029,8 +2022,7 @@ mod tests {
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn set_agent_state_does_not_check_a_destroy() {
|
async fn set_agent_state_does_not_check_a_destroy() {
|
||||||
let err =
|
let err =
|
||||||
set_on_disconnected_pr1ma("admin", swarm_wire::wanted::AgentState::Destroyed)
|
set_on_disconnected_pr1ma("admin", swarm_wire::wanted::AgentState::Destroyed).await;
|
||||||
.await;
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
err.status,
|
err.status,
|
||||||
Some(axum::http::StatusCode::SERVICE_UNAVAILABLE),
|
Some(axum::http::StatusCode::SERVICE_UNAVAILABLE),
|
||||||
|
|
|
||||||
|
|
@ -23,12 +23,11 @@ serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
# The jti digest: base32hex(sha256(claims)) over every JWT this crate signs.
|
# The jti digest: base32hex(sha256(claims)) over every JWT this crate signs.
|
||||||
sha2.workspace = true
|
sha2.workspace = true
|
||||||
# For `status::BUCKET` and `notices::STREAM` - the subjects a hive may
|
# For `status::BUCKET`: the subjects a hive may publish to are derived from
|
||||||
# publish to are derived from these names, and every end that touches them
|
# the name, and every end that touches it must agree on the same one.
|
||||||
# must agree on the same one. Deliberately WITHOUT the `kv` feature: this
|
# Deliberately WITHOUT the `kv` feature: this crate derives subject strings,
|
||||||
# crate derives subject strings, it never opens the bucket. `notices`
|
# it never opens the bucket.
|
||||||
# is name-only too (no `jetstream`/`kv` surface), same reason.
|
swarm-queue-client.workspace = true
|
||||||
swarm-queue-client = { workspace = true, features = ["notices"] }
|
|
||||||
# The store reads: the callout keys, its own OIDC secret, and the agent
|
# The store reads: the callout keys, its own OIDC secret, and the agent
|
||||||
# credentials `swarm-agent-verify` checks a presented token against.
|
# credentials `swarm-agent-verify` checks a presented token against.
|
||||||
swarm-secret-client.workspace = true
|
swarm-secret-client.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -477,10 +477,10 @@ impl Policy {
|
||||||
fn notices_subjects() -> [String; 2] {
|
fn notices_subjects() -> [String; 2] {
|
||||||
// Unlike `Self::stream()` above, no `KV_` prefix: the notices
|
// Unlike `Self::stream()` above, no `KV_` prefix: the notices
|
||||||
// stream is a plain `JetStream` stream, not a KV bucket, so its
|
// stream is a plain `JetStream` stream, not a KV bucket, so its
|
||||||
// NATS stream name *is* `swarm_queue_client::notices::STREAM`
|
// NATS stream name *is* `swarm_wire::notices::STREAM`
|
||||||
// verbatim — the `KV_` prefix is `create_key_value`'s own
|
// verbatim — the `KV_` prefix is `create_key_value`'s own
|
||||||
// convention, not something every stream carries.
|
// convention, not something every stream carries.
|
||||||
let stream = swarm_queue_client::notices::STREAM;
|
let stream = swarm_wire::notices::STREAM;
|
||||||
[
|
[
|
||||||
format!("$JS.API.STREAM.INFO.{stream}"),
|
format!("$JS.API.STREAM.INFO.{stream}"),
|
||||||
format!("$JS.API.STREAM.CREATE.{stream}"),
|
format!("$JS.API.STREAM.CREATE.{stream}"),
|
||||||
|
|
@ -514,7 +514,7 @@ impl Policy {
|
||||||
// Named from the defining crate so a rename cannot leave the grant
|
// Named from the defining crate so a rename cannot leave the grant
|
||||||
// pointing at a subject nobody publishes to. Not `extra_hive_subjects`:
|
// pointing at a subject nobody publishes to. Not `extra_hive_subjects`:
|
||||||
// that is for streams this crate does not know about.
|
// that is for streams this crate does not know about.
|
||||||
subjects.push(swarm_queue_client::notices::subject(hive));
|
subjects.push(swarm_wire::notices::subject(hive));
|
||||||
// Reading the wanted-state bucket, which is **this hive's own**.
|
// Reading the wanted-state bucket, which is **this hive's own**.
|
||||||
//
|
//
|
||||||
// One bucket per hive rather than one keyed by hive, so that a *watch*
|
// One bucket per hive rather than one keyed by hive, so that a *watch*
|
||||||
|
|
|
||||||
|
|
@ -11,8 +11,8 @@
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use swarm_wire::agent_limits::AgentCaps;
|
|
||||||
use swarm_wire::agent_inference::AgentInference;
|
use swarm_wire::agent_inference::AgentInference;
|
||||||
|
use swarm_wire::agent_limits::AgentCaps;
|
||||||
|
|
||||||
/// The KV bucket archetype sources are published into, keyed by source name.
|
/// The KV bucket archetype sources are published into, keyed by source name.
|
||||||
pub const BUCKET: &str = "agent-archetypes";
|
pub const BUCKET: &str = "agent-archetypes";
|
||||||
|
|
|
||||||
|
|
@ -469,9 +469,9 @@ pub struct AgentInput {
|
||||||
pub body: String,
|
pub body: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The hive-notices stream, shared by the hive that publishes and
|
/// Opening the hive-notices stream. The name and subject are
|
||||||
/// whatever eventually consumes it. Behind the `notices` feature, same
|
/// `swarm_wire::notices`. Behind the `notices` feature, same reason `status`
|
||||||
/// reason `status` is behind `kv` — see the module doc.
|
/// is behind `kv`.
|
||||||
#[cfg(feature = "notices")]
|
#[cfg(feature = "notices")]
|
||||||
pub mod notices;
|
pub mod notices;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,54 +1,12 @@
|
||||||
//! The hive-notices stream: its name, subject shape, and how a hive
|
//! Opening the hive-notices stream a hive publishes lifecycle notices into. The
|
||||||
//! opens it to publish.
|
//! stream name, subject shape and why it is a stream rather than a bucket are in
|
||||||
|
//! `swarm_wire::notices`.
|
||||||
//!
|
//!
|
||||||
//! Same reason [`crate::status`] exists rather than a bare `const` on
|
//! Feature-gated (`notices`) so the crate's other consumers do not compile
|
||||||
//! whichever side happens to need one first: a hive that publishes and a
|
|
||||||
//! swarm-level reader that eventually consumes live in different crates,
|
|
||||||
//! and a literal name repeated across both is an agreement nothing
|
|
||||||
//! checks.
|
|
||||||
//!
|
|
||||||
//! **This is a stream, not a bucket, and that is a real design choice —
|
|
||||||
//! not the same shape as [`crate::status`] wearing a different name.**
|
|
||||||
//! [`crate::status`]'s hive-status snapshot has a current value: a
|
|
||||||
//! reconnecting reader can always ask "what does this hive say *now*"
|
|
||||||
//! and get the true answer, so a KV bucket (last value per key) is the
|
|
||||||
//! right shape. A lifecycle notice ("container crashed at 04:12") has no
|
|
||||||
//! such steady state — miss the message and there is nothing left to
|
|
||||||
//! read later that would tell you it happened. That needs durable
|
|
||||||
//! delivery (a JetStream stream a consumer acks against), which is what
|
|
||||||
//! this module opens instead.
|
|
||||||
//!
|
|
||||||
//! Feature-gated (`notices`) for the same reason [`crate::status`] is
|
|
||||||
//! gated behind `kv`: the crate's other consumers (the auth-callout
|
|
||||||
//! responder, a hive that only publishes status) should not compile
|
|
||||||
//! against a stream shape they never touch.
|
//! against a stream shape they never touch.
|
||||||
|
|
||||||
use crate::Error;
|
use crate::Error;
|
||||||
|
use swarm_wire::notices::STREAM;
|
||||||
/// The stream a hive publishes lifecycle notices into.
|
|
||||||
///
|
|
||||||
/// A constant and not an option, matching [`crate::status::BUCKET`]:
|
|
||||||
/// reader and writer must name the same stream, and letting either side
|
|
||||||
/// pick its own name is how two deployments end up disagreeing about
|
|
||||||
/// which stream a notice actually landed in.
|
|
||||||
pub const STREAM: &str = "hive-notices";
|
|
||||||
|
|
||||||
/// Build the subject a given hive's notices publish to.
|
|
||||||
///
|
|
||||||
/// Every hive's notices land under `STREAM`, one subject per hive:
|
|
||||||
/// `hive-notices.<hiveName>`.
|
|
||||||
///
|
|
||||||
/// Not one subject per notice *kind* — a consumer that wants a specific
|
|
||||||
/// hive's notices subscribes to `subject(hive)`; one that wants the whole
|
|
||||||
/// swarm's subscribes to `{STREAM}.>`. The kind travels inside the message
|
|
||||||
/// payload instead, so adding a new notice kind is never a subject-design
|
|
||||||
/// change. Deliberately reuses `STREAM` rather than a second `PREFIX`
|
|
||||||
/// constant with the same value — one name for one fact, same reasoning
|
|
||||||
/// the module doc above gives for a shared `const` over a repeated literal.
|
|
||||||
#[must_use]
|
|
||||||
pub fn subject(hive: &str) -> String {
|
|
||||||
format!("{STREAM}.{hive}")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Open the notices stream, creating it if nothing has yet.
|
/// Open the notices stream, creating it if nothing has yet.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -8,10 +8,11 @@
|
||||||
pub mod agent_icon;
|
pub mod agent_icon;
|
||||||
pub mod agent_inference;
|
pub mod agent_inference;
|
||||||
pub mod agent_limits;
|
pub mod agent_limits;
|
||||||
pub mod agent_schedules;
|
|
||||||
pub mod agent_login;
|
pub mod agent_login;
|
||||||
|
pub mod agent_schedules;
|
||||||
pub mod agent_status;
|
pub mod agent_status;
|
||||||
pub mod builder;
|
pub mod builder;
|
||||||
pub mod claude_login;
|
pub mod claude_login;
|
||||||
|
pub mod notices;
|
||||||
pub mod review_classifier;
|
pub mod review_classifier;
|
||||||
pub mod wanted;
|
pub mod wanted;
|
||||||
|
|
|
||||||
37
swarm-wire/src/notices.rs
Normal file
37
swarm-wire/src/notices.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
//! The hive-notices stream: its name and subject shape.
|
||||||
|
//!
|
||||||
|
//! A hive that publishes and a swarm-level reader that consumes live in
|
||||||
|
//! different crates, and a literal name repeated across both is an agreement
|
||||||
|
//! nothing checks.
|
||||||
|
//!
|
||||||
|
//! **This is a stream, not a bucket.** The hive-status snapshot has a current
|
||||||
|
//! value, so a KV bucket (last value per key) is the right shape. A lifecycle
|
||||||
|
//! notice ("container crashed at 04:12") has no such steady state: miss the
|
||||||
|
//! message and nothing left to read later says it happened. That needs durable
|
||||||
|
//! delivery (a `JetStream` stream a consumer acks against), which
|
||||||
|
//! `swarm_queue_client::notices::open_or_create` opens.
|
||||||
|
|
||||||
|
/// The stream a hive publishes lifecycle notices into.
|
||||||
|
///
|
||||||
|
/// A constant and not an option, like every bucket name here:
|
||||||
|
/// reader and writer must name the same stream, and letting either side
|
||||||
|
/// pick its own name is how two deployments end up disagreeing about
|
||||||
|
/// which stream a notice actually landed in.
|
||||||
|
pub const STREAM: &str = "hive-notices";
|
||||||
|
|
||||||
|
/// Build the subject a given hive's notices publish to.
|
||||||
|
///
|
||||||
|
/// Every hive's notices land under `STREAM`, one subject per hive:
|
||||||
|
/// `hive-notices.<hiveName>`.
|
||||||
|
///
|
||||||
|
/// Not one subject per notice *kind* — a consumer that wants a specific
|
||||||
|
/// hive's notices subscribes to `subject(hive)`; one that wants the whole
|
||||||
|
/// swarm's subscribes to `{STREAM}.>`. The kind travels inside the message
|
||||||
|
/// payload instead, so adding a new notice kind is never a subject-design
|
||||||
|
/// change. Deliberately reuses `STREAM` rather than a second `PREFIX`
|
||||||
|
/// constant with the same value — one name for one fact, same reasoning
|
||||||
|
/// the module doc above gives for a shared `const` over a repeated literal.
|
||||||
|
#[must_use]
|
||||||
|
pub fn subject(hive: &str) -> String {
|
||||||
|
format!("{STREAM}.{hive}")
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue