{"record":{"id":"3c46fa4b083f650d","repo":"astrid-runtime/astrid","slug":"read-device-revocation-key-id-error","errorCode":null,"errorMessage":"read device revocation {key_id}: {error}","messagePattern":"read device revocation (.+?): (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-gateway/src/revocations.rs","lineNumber":195,"sourceCode":"/// CAS error, leaves a fence that startup hydration can restore. The function\n/// still returns an error after any CAS error so the HTTP caller withholds\n/// `204`, even when the fallback tombstone succeeded. If both writes fail,\n/// there is no durable fence; the caller may install a process-local maximum,\n/// but a later healthy empty KV cannot reconstruct it.\npub async fn record_device_max(\n    store: &dyn KvStore,\n    key_id: &str,\n    epoch: u64,\n) -> anyhow::Result<u64> {\n    if key_id.is_empty() || key_id.contains('/') {\n        anyhow::bail!(\"invalid device revocation key id\");\n    }\n    let key = format!(\"{DEVICE_PREFIX}{key_id}\");\n    loop {\n        let current = store\n            .get(REVOCATION_NAMESPACE, &key)\n            .await\n            .map_err(|error| anyhow::anyhow!(\"read device revocation {key_id}: {error}\"))?;\n        let current_epoch = current\n            .as_deref()\n            .map(|bytes| decode_epoch(bytes, &key))\n            .transpose()?;\n        let wanted = current_epoch.map_or(epoch, |current| current.max(epoch));\n        if current_epoch == Some(wanted) {\n            return Ok(wanted);\n        }\n        match store\n            .compare_and_swap(\n                REVOCATION_NAMESPACE,\n                &key,\n                current.as_deref(),\n                encode_epoch(wanted),\n            )\n            .await\n        {\n            Ok(true) => return Ok(wanted),","sourceCodeStart":177,"sourceCodeEnd":213,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/revocations.rs#L177-L213","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: single attempt\nlet current = store.get(REVOCATION_NAMESPACE, &key).await\n    .map_err(|error| anyhow::anyhow!(\"read device revocation {key_id}: {error}\"))?;\n// after: bounded retry for transient errors\nlet current = backoff_retry(3, || store.get(REVOCATION_NAMESPACE, &key)).await\n    .map_err(|error| anyhow::anyhow!(\"read device revocation {key_id}: {error}\"))?;","handlingStrategy":"retry","validationCode":"// Probe store read path before device revocation flows\npub async fn store_readable(store: &dyn KvStore) -> bool {\n    store.get(REVOCATION_NAMESPACE, \"__health_probe__\").await.is_ok()\n}","typeGuard":null,"tryCatchPattern":"match record_device_max(&store, key_id, epoch).await {\n    Ok(published) => Ok(published),\n    Err(e) if is_transient(&e) => backoff_retry(3, || record_device_max(&store, key_id, epoch)).await,\n    Err(e) => Err(e), // surface to caller; do not 2xx\n}","preventionTips":["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"],"tags":["kv-store","revocation","persistence"],"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"}