astrid-runtime/astrid · error · anyhow::Error
list principal revocations
Error message
list principal revocations: {error} What it means
load_from_store hydrates all revocation epochs into memory at startup by listing keys under PRINCIPAL_PREFIX in the revocation namespace. This error wraps any failure of store.list_keys_with_prefix for the principal prefix. It is thrown because hydration must fail closed: an incomplete load would silently drop revocations.
Solutions
- Ensure the KV backend is healthy and reachable, then restart/retry hydration
- Check the inner error for backend-specific causes (auth, timeout, unsupported operation)
- Verify REVOCATION_NAMESPACE and prefix configuration are correct
- Keep the gateway fail-closed (deny agents/devices) until hydration completes successfully
Example fix
// before: fail hard on any list error
.map_err(|error| anyhow::anyhow!("list principal revocations: {error}"))?;
// after: retry transient failures before aborting startup
let principal_keys = with_backoff(5, ||
store.list_keys_with_prefix(REVOCATION_NAMESPACE, PRINCIPAL_PREFIX)
).await
.map_err(|error| anyhow::anyhow!("list principal revocations: {error}"))?; Defensive patterns
Strategy: retry
Validate before calling
// Gate startup hydration on store health
pub async fn ready_for_hydration(store: &dyn KvStore) -> anyhow::Result<()> {
store.list_keys_with_prefix(REVOCATION_NAMESPACE, "__probe__").await
.map(|_| ())
.map_err(|e| anyhow::anyhow!("store list unavailable: {e}"))
} Try / catch
match load_from_store(&store).await {
Ok((principals, devices)) => { *state.revocations.write() = (principals, devices); }
Err(e) => {
error!(%e, "hydration failed; staying fail-closed and retrying");
backoff_loop(|| load_from_store(&store)).await?; // serve no traffic until success
}
} Prevention
- Block readiness until load_from_store succeeds so no traffic hits an unhydrated gateway
- Confirm the KV backend supports list_keys_with_prefix before adopting it
- Alert on startup list failures in the revocation namespace
- Validate namespace/prefix configuration in CI against a real store instance
When it happens
Trigger: Calling load_from_store (via migrate_legacy_file or the durable_epochs_are_monotonic_and_reloadable test path) when listing principal revocation keys fails — backend unreachable, list operation unsupported, timeout, or auth failure.
Common situations: Gateway startup against a down or misconfigured KV backend; backend that doesn't support prefix listing; permission errors on the namespace; version change in the store client breaking the list API.
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 device 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/d7940d49f509e55a.
Report an issue: GitHub.
Appendix: source
Thrown at crates/astrid-gateway/src/revocations.rs:304
Err(error) => {
publish_device_epoch(revoked_key_ids, key_id, u64::MAX);
return Err(error);
},
},
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)View on GitHub (pinned to affd8760f4)