astrid-runtime/astrid · error · anyhow::Error
list device revocations
Error message
list device revocations: {error} What it means
load_from_store lists device revocation keys under DEVICE_PREFIX in the revocation namespace to rebuild the in-memory epoch map. This error wraps failure of that list_keys_with_prefix call. It is thrown so hydration aborts (fail closed) rather than starting the gateway with an incomplete set of device revocations.
Solutions
- Restore backend health and retry startup/hydration
- Fix the backend error indicated in the inner message (auth, capability, connectivity)
- Confirm the device prefix and namespace configuration match the store contents
- Gate readiness on successful hydration so traffic only flows once all revocations are loaded
Example fix
// before
.map_err(|error| anyhow::anyhow!("list device revocations: {error}"))?;
// after: bounded retry then fail closed with readiness gate
let device_keys = with_backoff(5, ||
store.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX)
).await
.map_err(|error| anyhow::anyhow!("list device revocations: {error}"))?;
ready_flag.store(true, Ordering::Release); // only after full hydration Defensive patterns
Strategy: retry
Validate before calling
// Confirm prefix listing works for both prefixes before hydration
pub async fn list_supported(store: &dyn KvStore) -> anyhow::Result<()> {
store.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX).await.map(|_| ())
.map_err(|e| anyhow::anyhow!("device prefix list unsupported: {e}"))
} Try / catch
match load_from_store(&store).await {
Ok((principals, devices)) => { *state.revocations.write() = (principals, devices); }
Err(e) if e.to_string().contains("list device revocations") => {
error!(%e, "device hydration failed; denying all device keys until retried");
backoff_loop(|| load_from_store(&store)).await?;
}
Err(e) => return Err(e),
} Prevention
- Gate service readiness on complete hydration (both principal and device maps loaded)
- Test the store client's prefix-list capability at deploy time
- Fix namespace ACLs so the gateway identity can list the revocation namespace
- Add dashboards for hydration duration/failures at startup
When it happens
Trigger: store.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX) fails while load_from_store runs — backend unavailable, list unsupported, timeout, or permission denied.
Common situations: KV outage during gateway boot; store client missing list/prefix capability; namespace ACL misconfiguration; network partition between gateway and store.
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
- list principal revocations
- device revocation map poisoned during startup hydration
- read device revocation
- read principal revocation
- revocation map poisoned during startup hydration
AI-assisted analysis of astrid-runtime/astrid@affd8760f4 (2026-09-09).
Data as JSON: /api/errors/ca5c778ab8e58b49.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-gateway/src/revocations.rs:308
},
None => epoch,
};
Ok(publish_device_epoch(revoked_key_ids, key_id, durable_epoch))
}
/// Load all durable principal and device epochs from the fixed control
/// namespace. Every key/value is bounded and validated before publication.
pub async fn load_from_store(
store: &dyn KvStore,
) -> anyhow::Result<(HashMap<PrincipalId, u64>, HashMap<String, u64>)> {
let principal_keys = store
.list_keys_with_prefix(REVOCATION_NAMESPACE, PRINCIPAL_PREFIX)
.await
.map_err(|error| anyhow::anyhow!("list principal revocations: {error}"))?;
let device_keys = store
.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX)
.await
.map_err(|error| anyhow::anyhow!("list device revocations: {error}"))?;
if principal_keys.len().saturating_add(device_keys.len()) > MAX_REVOCATION_ENTRIES {
anyhow::bail!("gateway revocation namespace exceeds entry cap");
}
let mut principals = HashMap::with_capacity(principal_keys.len());
for key in principal_keys {
let alias = key
.strip_prefix(PRINCIPAL_PREFIX)
.filter(|alias| !alias.is_empty())
.ok_or_else(|| anyhow::anyhow!("invalid principal revocation key {key:?}"))?;
let principal = PrincipalId::new(alias).map_err(|error| {
anyhow::anyhow!("invalid principal revocation key {key:?}: {error}")
})?;
let value = store
.get(REVOCATION_NAMESPACE, &key)
.await
.map_err(|error| anyhow::anyhow!("read principal revocation {key:?}: {error}"))?
.ok_or_else(|| {
anyhow::anyhow!("principal revocation {key:?} disappeared during load")View on GitHub (pinned to affd8760f4)