refactor(#4006): one wanted-state bucket per hive, so a watch can be scoped
A hive reads its own declaration today and that scopes cleanly: DIRECT.GET carries the key in the subject, so the grant can name it. A *watch* cannot be scoped that way — a consumer's filter travels in the request payload, so $JS.API.CONSUMER.CREATE.<stream> grants the whole stream. With every hive in one bucket, letting a hive watch its own declaration would let it read every other hive's. One bucket per hive (hive-wanted-<hive>) makes the stream a hive may hold exactly as wide as what it is allowed to see, which is what #4006's live-watch needs. That watch is a separate change; this only moves the boundary. mara's calls, both on #4006: one stream per hive rather than teaching the auth responder a hive roster, and a wildcard for the controller — "its okay if swarm controller can theoretically override hive". A bucket name is a single subject token with no prefix matching, so no wildcard narrower than * covers N per-hive buckets; the controller's grant is account-wide by consequence, and documented as chosen rather than left to look accidental. The reader arm of #4005's key-layout guard asserted the opposite of that ruling, so it is replaced rather than deleted: the hive arm survives as no_hive_may_write_another_role_s_agent_status (with a positive control), and the_readers_grant_is_deliberately_account_wide pins the decision and names the ruling, so the width reads as chosen to whoever finds it next. Two pre-existing negative assertions were silently defanged by the rename -- they matched hive-wanted.beta and $KV.hive-wanted.alpha, strings nothing produces any more, and kept passing. Both now match current names. swarm-controller resolves the store per hive per call instead of caching one in a OnceCell: there is no single handle that serves N buckets, and declarations change on operator action rather than per tick.
This commit is contained in:
parent
8c94e340b8
commit
78637ded0c
6 changed files with 111 additions and 66 deletions
|
|
@ -57,29 +57,31 @@ fn apply(current: Option<&[u8]>, agent: &str, state: AgentState) -> Result<(Hive
|
|||
/// Writes the wanted-state bucket, and reads it back.
|
||||
pub struct WantedWriter {
|
||||
client: async_nats::Client,
|
||||
store: tokio::sync::OnceCell<async_nats::jetstream::kv::Store>,
|
||||
}
|
||||
|
||||
impl WantedWriter {
|
||||
#[must_use]
|
||||
pub fn new(client: async_nats::Client) -> Self {
|
||||
Self {
|
||||
client,
|
||||
store: tokio::sync::OnceCell::new(),
|
||||
}
|
||||
Self { client }
|
||||
}
|
||||
|
||||
/// The bucket handle, created on first use if nothing has made it yet.
|
||||
/// One hive's bucket handle, created on first use if nothing has made it
|
||||
/// yet.
|
||||
///
|
||||
/// Creation lives in [`swarm_queue_client::wanted`] because a bucket is
|
||||
/// described identically by everyone who may create it. Only the
|
||||
/// controller creates this one; a hive opens it read-only.
|
||||
/// controller creates these; a hive opens its own read-only.
|
||||
///
|
||||
/// Resolved per call rather than cached: there is one bucket per hive, so
|
||||
/// a single cached handle cannot serve them, and the declarations this
|
||||
/// writes change on operator action rather than on a loop — the extra
|
||||
/// lookup is per *declaration*, not per tick. A cache here would be a map
|
||||
/// whose invalidation nobody needs yet.
|
||||
async fn store(
|
||||
&self,
|
||||
) -> std::result::Result<&async_nats::jetstream::kv::Store, swarm_queue_client::Error> {
|
||||
self.store
|
||||
.get_or_try_init(|| swarm_queue_client::wanted::open_or_create(&self.client))
|
||||
.await
|
||||
hive: &str,
|
||||
) -> std::result::Result<async_nats::jetstream::kv::Store, swarm_queue_client::Error> {
|
||||
swarm_queue_client::wanted::open_or_create(&self.client, hive).await
|
||||
}
|
||||
|
||||
/// The declaration currently published for `hive`, or `None`.
|
||||
|
|
@ -87,7 +89,7 @@ impl WantedWriter {
|
|||
// An unconnected client does not fail a JetStream request, it hangs
|
||||
// on it — see `swarm_queue_client::ensure_connected`.
|
||||
swarm_queue_client::ensure_connected(&self.client)?;
|
||||
let store = self.store().await?;
|
||||
let store = self.store(hive).await?;
|
||||
let Some(entry) = store
|
||||
.entry(hive)
|
||||
.await
|
||||
|
|
@ -110,7 +112,7 @@ impl WantedWriter {
|
|||
/// fact — the conflict has to be caught here.
|
||||
pub async fn set(&self, hive: &str, agent: &str, state: AgentState) -> Result<HiveWanted> {
|
||||
swarm_queue_client::ensure_connected(&self.client)?;
|
||||
let store = self.store().await?;
|
||||
let store = self.store(hive).await?;
|
||||
|
||||
for _ in 0..MAX_ATTEMPTS {
|
||||
let entry = store
|
||||
|
|
|
|||
Loading…
Reference in a new issue