{"record":{"id":"a50e314363500c85","repo":"astrid-runtime/astrid","slug":"read-principal-revocation-principal-error","errorCode":null,"errorMessage":"read principal revocation {principal}: {error}","messagePattern":"read principal revocation (.+?): (.+?)","errorType":"exception","errorClass":"anyhow::Error","httpStatus":null,"severity":"error","filePath":"crates/astrid-gateway/src/revocations.rs","lineNumber":129,"sourceCode":"\nfn encode_epoch(epoch: u64) -> Vec<u8> {\n    epoch.to_le_bytes().to_vec()\n}\n\n/// Record the maximum principal revocation epoch durably. The returned value\n/// is the epoch now authoritative in storage (which may be newer than the\n/// requested event when another writer won the CAS race).\npub async fn record_principal_max(\n    store: &dyn KvStore,\n    principal: &PrincipalId,\n    epoch: u64,\n) -> anyhow::Result<u64> {\n    let key = format!(\"{PRINCIPAL_PREFIX}{principal}\");\n    loop {\n        let current = store\n            .get(REVOCATION_NAMESPACE, &key)\n            .await\n            .map_err(|error| anyhow::anyhow!(\"read principal revocation {principal}: {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":111,"sourceCodeEnd":147,"githubUrl":"https://github.com/astrid-runtime/astrid/blob/affd8760f44190dbdfbec23403f4c4b642c33112/crates/astrid-gateway/src/revocations.rs#L111-L147","documentation":"record_principal_max reads the current revocation epoch for a principal from the KV store before a CAS-style monotonic update. This error wraps any failure of that read (store.get on REVOCATION_NAMESPACE). It is thrown because the function cannot safely compute max(current, wanted) without knowing the current durable epoch; on a read failure it fails closed rather than guessing.","triggerScenarios":"Calling record_principal_max (via migrate_legacy_file or spawn_watcher) when the underlying KvStore backend is unreachable, times out, or returns an internal error for a get() on the principal revocation key.","commonSituations":"Redis/etcd/KV backend down or restarting at gateway startup; network partition between gateway and store; wrong namespace/config so the backend rejects the get; TLS or auth misconfiguration on the KV client.","solutions":["Check connectivity and health of the configured KvStore backend and restart the gateway once it is reachable","Verify REVOCATION_NAMESPACE and store connection settings in the gateway configuration","Inspect the inner error in the message for backend-specific diagnostics (timeout, auth, connection refused) and fix that root cause","Retry record_principal_max; the CAS loop is designed to be retried once the store is healthy"],"exampleFix":"// before: propagate read error\n.map_err(|error| anyhow::anyhow!(\"read principal revocation {principal}: {error}\"))?\n// after: retry transient store failures before failing\nlet current = retry_transient(3, || store.get(REVOCATION_NAMESPACE, &key))\n    .await\n    .map_err(|error| anyhow::anyhow!(\"read principal revocation {principal}: {error}\"))?;","handlingStrategy":"retry","validationCode":"// Rust: check store health before recording revocations\npub async fn assert_store_healthy(store: &dyn KvStore) -> anyhow::Result<()> {\n    store.get(REVOCATION_NAMESPACE, \"__health_probe__\").await\n        .map(|_| ())\n        .map_err(|e| anyhow::anyhow!(\"revocation store unavailable: {e}\"))\n}","typeGuard":null,"tryCatchPattern":"match record_principal_max(&store, principal, epoch).await {\n    Ok(published) => info!(principal, published, \"revocation recorded\"),\n    Err(e) if is_transient(&e) => backoff_retry(3, || record_principal_max(&store, principal, epoch)).await?,\n    Err(e) => { error!(principal, %e, \"store read failed; fail closed locally\"); local_fence.lock().insert(principal.clone(), epoch); }\n}","preventionTips":["Add a startup/periodic health probe of the KvStore before serving revocation traffic","Deploy the gateway and its KV backend with liveness/readiness checks so revocations only run against a healthy store","Monitor the inner backend error strings (timeouts, auth, connection) and alert on them","Keep revocation keys warm/known; ensure the namespace exists before first use"],"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"}