{"record":{"id":"ca5c778ab8e58b49","repo":"astrid-runtime/astrid","slug":"list-device-revocations-error","errorCode":null,"errorMessage":"list device revocations: {error}","messagePattern":"list device revocations: (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-gateway/src/revocations.rs","lineNumber":308,"sourceCode":"        },\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)\n            .await\n            .map_err(|error| anyhow::anyhow!(\"read principal revocation {key:?}: {error}\"))?\n            .ok_or_else(|| {\n                anyhow::anyhow!(\"principal revocation {key:?} disappeared during load\")","sourceCodeStart":290,"sourceCodeEnd":326,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/revocations.rs#L290-L326","documentation":"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.","triggerScenarios":"store.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX) fails while load_from_store runs — backend unavailable, list unsupported, timeout, or permission denied.","commonSituations":"KV outage during gateway boot; store client missing list/prefix capability; namespace ACL misconfiguration; network partition between gateway and store.","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"],"exampleFix":"// before\n.map_err(|error| anyhow::anyhow!(\"list device revocations: {error}\"))?;\n// after: bounded retry then fail closed with readiness gate\nlet device_keys = with_backoff(5, ||\n    store.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX)\n).await\n .map_err(|error| anyhow::anyhow!(\"list device revocations: {error}\"))?;\nready_flag.store(true, Ordering::Release); // only after full hydration","handlingStrategy":"retry","validationCode":"// Confirm prefix listing works for both prefixes before hydration\npub async fn list_supported(store: &dyn KvStore) -> anyhow::Result<()> {\n    store.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX).await.map(|_| ())\n        .map_err(|e| anyhow::anyhow!(\"device prefix list unsupported: {e}\"))\n}","typeGuard":null,"tryCatchPattern":"match load_from_store(&store).await {\n    Ok((principals, devices)) => { *state.revocations.write() = (principals, devices); }\n    Err(e) if e.to_string().contains(\"list device revocations\") => {\n        error!(%e, \"device hydration failed; denying all device keys until retried\");\n        backoff_loop(|| load_from_store(&store)).await?;\n    }\n    Err(e) => return Err(e),\n}","preventionTips":["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"],"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"}