astrid-runtime/astrid · error · anyhow::Error
read device revocation
Error message
read device revocation {key_id}: {error} What it means
record_device_max reads the current revocation epoch for a device key ID from the KV store before performing its monotonic CAS update. This error wraps any failure of that get() call. It is thrown because the function must know the current durable epoch to compute the max; without it the function refuses to proceed rather than risk weakening an existing revocation.
Solutions
- Verify the KvStore backend is healthy and reachable, then retry apply_device_revocation
- Check the inner error text for the backend-specific root cause (timeout, connection, auth) and fix that
- Confirm the store namespace/prefix configuration matches the deployment
- If revoking in response to a key compromise, enforce a process-local fence while the store is unavailable
Example fix
// before: single attempt
let current = store.get(REVOCATION_NAMESPACE, &key).await
.map_err(|error| anyhow::anyhow!("read device revocation {key_id}: {error}"))?;
// after: bounded retry for transient errors
let current = backoff_retry(3, || store.get(REVOCATION_NAMESPACE, &key)).await
.map_err(|error| anyhow::anyhow!("read device revocation {key_id}: {error}"))?; Defensive patterns
Strategy: retry
Validate before calling
// Probe store read path before device revocation flows
pub async fn store_readable(store: &dyn KvStore) -> bool {
store.get(REVOCATION_NAMESPACE, "__health_probe__").await.is_ok()
} Try / catch
match record_device_max(&store, key_id, epoch).await {
Ok(published) => Ok(published),
Err(e) if is_transient(&e) => backoff_retry(3, || record_device_max(&store, key_id, epoch)).await,
Err(e) => Err(e), // surface to caller; do not 2xx
} Prevention
- Verify store connectivity and credentials before accepting revoke requests
- Alert on store get() failures in the revocation namespace
- Pin and test the store client version used by the gateway
- Fail closed in-process for the key while the store read path is down
When it happens
Trigger: Calling record_device_max (via apply_device_revocation) when store.get on the DEVICE_PREFIX key in REVOCATION_NAMESPACE fails due to backend unavailability, timeout, or an internal store error.
Common situations: KV backend outage or restart; network partition to the store; store authentication/authorization failure for the gateway's client; misconfigured namespace or endpoint in gateway config.
Understand the failure class
Background: Database query failed: Internal Server Error 500s wrapping SQL, Prisma, and connection failures — what to check first — this error's family across 16 libraries.
Related errors
- read principal revocation
- list device revocations
- list principal revocations
- write device revocation
- write device revocation
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/3c46fa4b083f650d.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-gateway/src/revocations.rs:195
/// CAS error, leaves a fence that startup hydration can restore. The function
/// still returns an error after any CAS error so the HTTP caller withholds
/// `204`, even when the fallback tombstone succeeded. If both writes fail,
/// there is no durable fence; the caller may install a process-local maximum,
/// but a later healthy empty KV cannot reconstruct it.
pub async fn record_device_max(
store: &dyn KvStore,
key_id: &str,
epoch: u64,
) -> anyhow::Result<u64> {
if key_id.is_empty() || key_id.contains('/') {
anyhow::bail!("invalid device revocation key id");
}
let key = format!("{DEVICE_PREFIX}{key_id}");
loop {
let current = store
.get(REVOCATION_NAMESPACE, &key)
.await
.map_err(|error| anyhow::anyhow!("read device revocation {key_id}: {error}"))?;
let current_epoch = current
.as_deref()
.map(|bytes| decode_epoch(bytes, &key))
.transpose()?;
let wanted = current_epoch.map_or(epoch, |current| current.max(epoch));
if current_epoch == Some(wanted) {
return Ok(wanted);
}
match store
.compare_and_swap(
REVOCATION_NAMESPACE,
&key,
current.as_deref(),
encode_epoch(wanted),
)
.await
{
Ok(true) => return Ok(wanted),View on GitHub (pinned to affd8760f4)