swarm-queue-client: the wanted-state bucket, opposite in direction to status
The controller declares each hive's wanted agent set; the hive reads its own key. Deliberately not a mirror of `status`, which the module documents as a table: authored by the controller rather than the hive, DECLARED rather than observed, and — the row that decides how it must be built — unrecoverable if the store is lost, where status regenerates because every hive republishes what it is. Two open functions rather than one shared `open_or_create`. `status` shares its constructor because either end may legitimately arrive first on a fresh swarm; here the writer is single and known, so the hive gets a read-only open returning `Option` and holds no grant to create the bucket. Its absence is the ordinary pre-publication state, not an error a hive could fix. Absence is also not a deletion order, which the module says at the place an implementer will meet it: swarm-side lifecycle does not yet cover agents that predate it, so a hive finding no key has learned nothing about what it runs — converging to an empty set would tear those down. No consumer yet. This is the half that is invariant under the scope semantics and cadence still being decided.
This commit is contained in:
parent
a73cc83ee6
commit
1e80e52f3c
2 changed files with 87 additions and 0 deletions
|
|
@ -164,6 +164,13 @@ pub fn chain(error: &dyn std::error::Error) -> String {
|
|||
/// which is the disagreement this module exists to prevent.
|
||||
pub mod status;
|
||||
|
||||
/// The bucket the controller declares each hive's wanted agent set in — the
|
||||
/// opposite direction from [`status`], and deliberately not a mirror of it.
|
||||
/// Status is *observed* and republishes itself; this is *declared* and nothing
|
||||
/// recreates it, which is why the module documents its own durability and its
|
||||
/// "absence is not a deletion order" rule rather than inheriting either.
|
||||
pub mod wanted;
|
||||
|
||||
/// The subject the swarm controller publishes on when the hive-wide knowledge
|
||||
/// repository has changed. One writer, many readers — every hive subscribes.
|
||||
///
|
||||
|
|
|
|||
80
swarm-queue-client/src/wanted.rs
Normal file
80
swarm-queue-client/src/wanted.rs
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
//! The hive-wanted KV bucket: the agent set the controller declares for each
|
||||
//! hive, keyed by `hiveName`.
|
||||
//!
|
||||
//! Sibling of [`crate::status`] and deliberately **not** a mirror of it: that
|
||||
//! one is **observed** — each hive republishes what it is, so its store losing
|
||||
//! everything "degrades to honesty" (`swarm-nats.nix`). This one is
|
||||
//! **declared**, and nothing regenerates it; lose it and a reconcile loop has
|
||||
//! nothing to converge to, which is the failure such a loop exists to remove.
|
||||
//!
|
||||
//! # Absence is not a deletion order
|
||||
//!
|
||||
//! A hive that finds no key for itself — or no bucket at all — has learned
|
||||
//! nothing about the agents it is running, not that it should have none.
|
||||
//! Swarm-side lifecycle does not yet cover agents that predate it, so callers
|
||||
//! converge the agents a value NAMES and leave the rest alone.
|
||||
//!
|
||||
//! # Only the controller creates it
|
||||
//!
|
||||
//! Unlike [`crate::status::open_or_create`], where either end may legitimately
|
||||
//! arrive first, the writer here is single and known. A hive opens read-only
|
||||
//! and treats absence as the case above, so "not published yet" stays quiet
|
||||
//! rather than looking like an error a hive could fix.
|
||||
|
||||
#[cfg(feature = "kv")]
|
||||
use crate::Error;
|
||||
|
||||
/// The KV bucket the controller publishes per-hive wanted state into, one key
|
||||
/// per hive keyed by `hiveName`.
|
||||
///
|
||||
/// A constant and not an option, for the reason [`crate::status::BUCKET`]
|
||||
/// gives: writer and reader must name the same bucket, and an option is a way
|
||||
/// for two deployments to disagree about which one that is.
|
||||
pub const BUCKET: &str = "hive-wanted";
|
||||
|
||||
/// Open the wanted-state bucket for writing, creating it if nothing has yet.
|
||||
///
|
||||
/// **Controller-side only.** `history: 1` because a hive converges to the
|
||||
/// current declaration and never asks what the previous one was — that
|
||||
/// question is answered by the controller's own records, not by replaying a
|
||||
/// bucket.
|
||||
#[cfg(feature = "kv")]
|
||||
pub async fn open_or_create(
|
||||
client: &async_nats::Client,
|
||||
) -> Result<async_nats::jetstream::kv::Store, Error> {
|
||||
let js = async_nats::jetstream::new(client.clone());
|
||||
match js.get_key_value(BUCKET).await {
|
||||
Ok(store) => Ok(store),
|
||||
Err(e) => {
|
||||
tracing::info!(
|
||||
bucket = BUCKET,
|
||||
reason = %e,
|
||||
"wanted-state bucket not available, creating it"
|
||||
);
|
||||
js.create_key_value(async_nats::jetstream::kv::Config {
|
||||
bucket: BUCKET.to_owned(),
|
||||
description: "Agent set the swarm controller declares for each hive".to_owned(),
|
||||
history: 1,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.map_err(|source| Error::CreateBucket {
|
||||
bucket: BUCKET,
|
||||
source,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the wanted-state bucket for reading, or `None` when it does not exist.
|
||||
///
|
||||
/// **Hive-side.** `None` is the ordinary pre-publication state, not a failure:
|
||||
/// a hive holds no grant to create this bucket and must not treat its absence
|
||||
/// as a reason to converge to an empty agent set — see the module docs.
|
||||
#[cfg(feature = "kv")]
|
||||
pub async fn open_read_only(
|
||||
client: &async_nats::Client,
|
||||
) -> Option<async_nats::jetstream::kv::Store> {
|
||||
let js = async_nats::jetstream::new(client.clone());
|
||||
js.get_key_value(BUCKET).await.ok()
|
||||
}
|
||||
Loading…
Reference in a new issue