From 327397132811623c6f2d14ac35612a6febcce558 Mon Sep 17 00:00:00 2001 From: atlas Date: Sat, 15 Aug 2026 23:14:06 +0200 Subject: [PATCH] refactor(swarm-queue-client): typed errors for the bucket and the guard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Finishes the anyhow removal for the parts this branch adds: the status bucket's open-or-create and the connected-client precondition. Two variants, one of them behind the `kv` feature because the error type it wraps does not exist without it — the error enum respects the same gate the module does. NotConnected is deliberately distinct from Connect: one is a connect that was attempted and refused, the other is a request made before any connection exists. The first is a deployment problem and the second is a caller-ordering one, which is the whole reason a caller wants an enum rather than a string. The controller's `store` now returns the queue client's error rather than an anyhow one: `OnceCell::get_or_try_init` takes its error type from the closure, so widening there would mean converting inside the closure for no gain. `view` `?`s it and anyhow converts at that boundary — the library keeps a typed error, the binary keeps anyhow, and no call site pays for the split. --- swarm-controller/src/status.rs | 11 ++++++++++- swarm-queue-client/src/lib.rs | 19 +++++++++++++++++-- swarm-queue-client/src/status.rs | 9 ++++++--- 3 files changed, 33 insertions(+), 6 deletions(-) diff --git a/swarm-controller/src/status.rs b/swarm-controller/src/status.rs index 3b1cd1b1..eaff6038 100644 --- a/swarm-controller/src/status.rs +++ b/swarm-controller/src/status.rs @@ -155,7 +155,16 @@ impl StatusReader { /// /// Whichever side arrives first creates it, and both sides ask for the /// same shape, so this is a race with one outcome. - async fn store(&self) -> Result<&async_nats::jetstream::kv::Store> { + /// + /// Returns the queue client's own error rather than an `anyhow::Error`: + /// `OnceCell::get_or_try_init` takes its error type from the closure, + /// so widening here would mean converting *inside* the closure for no + /// gain. `view` below `?`s it and anyhow converts there — which is the + /// whole point of the library keeping a typed error while the binary + /// keeps anyhow. + 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::status::open_or_create(&self.client)) .await diff --git a/swarm-queue-client/src/lib.rs b/swarm-queue-client/src/lib.rs index d9e00fb7..d6d0ce62 100644 --- a/swarm-queue-client/src/lib.rs +++ b/swarm-queue-client/src/lib.rs @@ -84,6 +84,21 @@ pub enum Error { #[source] source: async_nats::ConnectError, }, + + /// Distinct from [`Error::Connect`] on purpose: that one is a connect + /// that was attempted and refused, this one is a request made before + /// any connection exists. The second is a caller-ordering problem and + /// the first is a deployment one. + #[error("not connected to the swarm queue (client state: {0:?})")] + NotConnected(async_nats::connection::State), + + #[cfg(feature = "kv")] + #[error("creating the {bucket} bucket")] + CreateBucket { + bucket: &'static str, + #[source] + source: async_nats::jetstream::context::CreateKeyValueError, + }, } /// Render an error and its source chain on one line. @@ -242,10 +257,10 @@ async fn mint_token(http: &reqwest::Client, cfg: &QueueConfig) -> Result Result<()> { +pub fn ensure_connected(client: &async_nats::Client) -> Result<(), Error> { let state = client.connection_state(); if state != async_nats::connection::State::Connected { - bail!("not connected to the swarm queue (client state: {state:?})"); + return Err(Error::NotConnected(state)); } Ok(()) } diff --git a/swarm-queue-client/src/status.rs b/swarm-queue-client/src/status.rs index 0c528cf0..0a38d7d7 100644 --- a/swarm-queue-client/src/status.rs +++ b/swarm-queue-client/src/status.rs @@ -19,7 +19,7 @@ //! responder, still pulls neither `jetstream` nor `kv`: it speaks the //! connect and nothing else. -use anyhow::{Context, Result}; +use crate::Error; /// The KV bucket hives publish their status snapshots into, one key per /// hive keyed by `hiveName`. @@ -41,7 +41,7 @@ pub const BUCKET: &str = "hive-status"; /// into a permanent, silent absence of data. pub async fn open_or_create( client: &async_nats::Client, -) -> Result { +) -> Result { let js = async_nats::jetstream::new(client.clone()); match js.get_key_value(BUCKET).await { Ok(store) => Ok(store), @@ -58,7 +58,10 @@ pub async fn open_or_create( ..Default::default() }) .await - .with_context(|| format!("creating the {BUCKET} bucket")) + .map_err(|source| Error::CreateBucket { + bucket: BUCKET, + source, + }) } } }