{"record":{"id":"d7940d49f509e55a","repo":"astrid-runtime/astrid","slug":"list-principal-revocations-error","errorCode":null,"errorMessage":"list principal revocations: {error}","messagePattern":"list principal revocations: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-gateway/src/revocations.rs","lineNumber":304,"sourceCode":"            Err(error) => {\n                publish_device_epoch(revoked_key_ids, key_id, u64::MAX);\n                return Err(error);\n            },\n        },\n        None => epoch,\n    };\n    Ok(publish_device_epoch(revoked_key_ids, key_id, durable_epoch))\n}\n\n/// Load all durable principal and device epochs from the fixed control\n/// namespace. Every key/value is bounded and validated before publication.\npub async fn load_from_store(\n    store: &dyn KvStore,\n) -> anyhow::Result<(HashMap<PrincipalId, u64>, HashMap<String, u64>)> {\n    let principal_keys = store\n        .list_keys_with_prefix(REVOCATION_NAMESPACE, PRINCIPAL_PREFIX)\n        .await\n        .map_err(|error| anyhow::anyhow!(\"list principal revocations: {error}\"))?;\n    let device_keys = store\n        .list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX)\n        .await\n        .map_err(|error| anyhow::anyhow!(\"list device revocations: {error}\"))?;\n    if principal_keys.len().saturating_add(device_keys.len()) > MAX_REVOCATION_ENTRIES {\n        anyhow::bail!(\"gateway revocation namespace exceeds entry cap\");\n    }\n    let mut principals = HashMap::with_capacity(principal_keys.len());\n    for key in principal_keys {\n        let alias = key\n            .strip_prefix(PRINCIPAL_PREFIX)\n            .filter(|alias| !alias.is_empty())\n            .ok_or_else(|| anyhow::anyhow!(\"invalid principal revocation key {key:?}\"))?;\n        let principal = PrincipalId::new(alias).map_err(|error| {\n            anyhow::anyhow!(\"invalid principal revocation key {key:?}: {error}\")\n        })?;\n        let value = store\n            .get(REVOCATION_NAMESPACE, &key)","sourceCodeStart":286,"sourceCodeEnd":322,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/revocations.rs#L286-L322","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: fail hard on any list error\n.map_err(|error| anyhow::anyhow!(\"list principal revocations: {error}\"))?;\n// after: retry transient failures before aborting startup\nlet principal_keys = with_backoff(5, ||\n    store.list_keys_with_prefix(REVOCATION_NAMESPACE, PRINCIPAL_PREFIX)\n).await\n .map_err(|error| anyhow::anyhow!(\"list principal revocations: {error}\"))?;","handlingStrategy":"retry","validationCode":"// Gate startup hydration on store health\npub async fn ready_for_hydration(store: &dyn KvStore) -> anyhow::Result<()> {\n    store.list_keys_with_prefix(REVOCATION_NAMESPACE, \"__probe__\").await\n        .map(|_| ())\n        .map_err(|e| anyhow::anyhow!(\"store list unavailable: {e}\"))\n}","typeGuard":null,"tryCatchPattern":"match load_from_store(&store).await {\n    Ok((principals, devices)) => { *state.revocations.write() = (principals, devices); }\n    Err(e) => {\n        error!(%e, \"hydration failed; staying fail-closed and retrying\");\n        backoff_loop(|| load_from_store(&store)).await?; // serve no traffic until success\n    }\n}","preventionTips":["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"],"tags":["kv-store","hydration","revocation","startup"],"backgroundTag":"database-query-failed","analyzedSha":"affd8760f44190dbdfbec23403f4c4b642c33112","analyzedAt":"2026-09-09T21:28:12.402Z","contentChangedAt":"2026-09-09T21:28:12.402Z","schemaVersion":2},"datasetVersion":"2026-09-17T15:17:12.973Z"}