refactor(swarm-queue-client): share the hive-status bucket's name and shape
The bucket has two ends in two crates: a hive writes its own key, the controller reads every key. `swarm-controller` declared the name as a private const with a doc comment arguing that "reader and writer must name the same bucket" — an argument the writer, in another crate, could not obey. The name is the mild half. Both ends do get-or-create, because either may come up first on a fresh swarm and neither can assume the other has run. Two `Config`s that drift means whichever end created the bucket wins and the other's `get_key_value` succeeds against a bucket it did not ask for: no error, no log, just a retention policy nobody chose. Sharing the constructor gives that race one outcome. Behind a default-off `kv` feature, so the crate's other consumer — the auth-callout responder, which speaks the connect and nothing else — still pulls neither `jetstream` nor `kv`. That was the actual reason the feature was excluded when this crate was extracted; the flag preserves it. The surface is deliberately narrow: one bucket's name and creation config, not a general KV facade.
This commit is contained in:
parent
6712cdb796
commit
22659234c4
6 changed files with 119 additions and 41 deletions
|
|
@ -112,6 +112,12 @@ pub fn chain(error: &dyn std::error::Error) -> String {
|
|||
rendered
|
||||
}
|
||||
|
||||
/// The hive-status KV bucket, shared by the hive that writes it and the
|
||||
/// controller that reads it. Behind the `kv` feature — see the module doc
|
||||
/// for why a bucket name and its config belong to neither end alone.
|
||||
#[cfg(feature = "kv")]
|
||||
pub mod status;
|
||||
|
||||
/// Only the one field this needs; authelia returns several.
|
||||
#[derive(serde::Deserialize)]
|
||||
struct TokenResponse {
|
||||
|
|
|
|||
64
swarm-queue-client/src/status.rs
Normal file
64
swarm-queue-client/src/status.rs
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
//! The hive-status KV bucket: its name, and the shape it is created with.
|
||||
//!
|
||||
//! Two processes touch this bucket from opposite ends — a hive writes its
|
||||
//! own key, the swarm controller reads every key — and they live in
|
||||
//! different crates. That is the whole reason this module exists rather
|
||||
//! than a `const` on each side: **the two ends must agree, and a literal
|
||||
//! repeated across crates is an agreement nothing checks.**
|
||||
//!
|
||||
//! The name is the obvious half. The sharper half is the *config*: both
|
||||
//! ends open the bucket with [`open_or_create`], because either may
|
||||
//! arrive first on a fresh swarm and neither can assume the other has
|
||||
//! run. If the two ends passed different `Config`s, whichever created it
|
||||
//! would win and the other's `get_key_value` would succeed against a
|
||||
//! bucket it did not ask for — no error, no log, just a retention policy
|
||||
//! nobody chose. Sharing the constructor makes the race have one outcome
|
||||
//! instead of two.
|
||||
//!
|
||||
//! Feature-gated (`kv`) so the crate's other consumer, the auth-callout
|
||||
//! responder, still pulls neither `jetstream` nor `kv`: it speaks the
|
||||
//! connect and nothing else.
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
/// The KV bucket hives publish their status snapshots into, one key per
|
||||
/// hive keyed by `hiveName`.
|
||||
///
|
||||
/// A constant and not an option: reader and writer must name the same
|
||||
/// bucket, and an option is a way for two deployments to disagree about
|
||||
/// which one that is. Nothing about a bucket name is site-specific.
|
||||
pub const BUCKET: &str = "hive-status";
|
||||
|
||||
/// Open the status bucket, creating it if nothing has yet.
|
||||
///
|
||||
/// `history: 1` is the shape: every consumer reads *the last thing each
|
||||
/// hive said*, and retaining more would be storage bought for a query
|
||||
/// nobody makes.
|
||||
///
|
||||
/// Creating rather than requiring a provisioning step is deliberate — the
|
||||
/// controller and the hives come up in no particular order, and a bucket
|
||||
/// that must pre-exist turns "the swarm was deployed in the wrong order"
|
||||
/// into a permanent, silent absence of data.
|
||||
pub async fn open_or_create(
|
||||
client: &async_nats::Client,
|
||||
) -> Result<async_nats::jetstream::kv::Store> {
|
||||
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,
|
||||
"status bucket not available, creating it"
|
||||
);
|
||||
js.create_key_value(async_nats::jetstream::kv::Config {
|
||||
bucket: BUCKET.to_owned(),
|
||||
description: "Last status snapshot offered by each hive".to_owned(),
|
||||
history: 1,
|
||||
..Default::default()
|
||||
})
|
||||
.await
|
||||
.with_context(|| format!("creating the {BUCKET} bucket"))
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue