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

  1. Ensure the KV backend is healthy and reachable, then restart/retry hydration
  2. Check the inner error for backend-specific causes (auth, timeout, unsupported operation)
  3. Verify REVOCATION_NAMESPACE and prefix configuration are correct
  4. 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

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


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)