astrid-runtime/astrid · error · anyhow::Error

list device revocations

Error message

list device revocations: {error}

What it means

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.

Solutions

  1. Restore backend health and retry startup/hydration
  2. Fix the backend error indicated in the inner message (auth, capability, connectivity)
  3. Confirm the device prefix and namespace configuration match the store contents
  4. Gate readiness on successful hydration so traffic only flows once all revocations are loaded

Example fix

// before
.map_err(|error| anyhow::anyhow!("list device revocations: {error}"))?;
// after: bounded retry then fail closed with readiness gate
let device_keys = with_backoff(5, ||
    store.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX)
).await
 .map_err(|error| anyhow::anyhow!("list device revocations: {error}"))?;
ready_flag.store(true, Ordering::Release); // only after full hydration
Defensive patterns

Strategy: retry

Validate before calling

// Confirm prefix listing works for both prefixes before hydration
pub async fn list_supported(store: &dyn KvStore) -> anyhow::Result<()> {
    store.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX).await.map(|_| ())
        .map_err(|e| anyhow::anyhow!("device prefix list unsupported: {e}"))
}

Try / catch

match load_from_store(&store).await {
    Ok((principals, devices)) => { *state.revocations.write() = (principals, devices); }
    Err(e) if e.to_string().contains("list device revocations") => {
        error!(%e, "device hydration failed; denying all device keys until retried");
        backoff_loop(|| load_from_store(&store)).await?;
    }
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: store.list_keys_with_prefix(REVOCATION_NAMESPACE, DEVICE_PREFIX) fails while load_from_store runs — backend unavailable, list unsupported, timeout, or permission denied.

Common situations: KV outage during gateway boot; store client missing list/prefix capability; namespace ACL misconfiguration; network partition between gateway and store.

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/ca5c778ab8e58b49. Report an issue: GitHub.

Appendix: source

Thrown at crates/astrid-gateway/src/revocations.rs:308

        },
        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)
            .await
            .map_err(|error| anyhow::anyhow!("read principal revocation {key:?}: {error}"))?
            .ok_or_else(|| {
                anyhow::anyhow!("principal revocation {key:?} disappeared during load")

View on GitHub (pinned to affd8760f4)