block/buzz · error
classify mutation result: {e}
Error message
classify mutation result: {e} What it means
Raised when the driver gets Ok(false) from an atomic mutation (meaning the lease fence rejected the write) and then attempts to classify the outcome by re-reading the action row with `get_admin_action`. If that classification read itself fails (DB error), this wrapper obscures whether the mutation was already committed by another driver or the lease was lost, so the driver cannot safely proceed. It exists to avoid conflating a read failure with a LeaseLost verdict.
Source
Thrown at crates/buzz-relay/src/handlers/report_resolution.rs:787
)
.await
.map_err(|e| anyhow::anyhow!("delete failed: {e}"))
}
other => Err(anyhow::anyhow!("unexpected enforcement action: {other}")),
};
match raw? {
true => Ok(MutationOutcome::Committed),
false => {
// Reload to distinguish "step_marker already set by another driver"
// (AlreadyCommitted — safe to proceed to finalization) from "this
// driver's lease expired" (LeaseLost — must stop, recovery worker
// will take over after expiry).
let rec = state
.db
.get_admin_action(action_id)
.await
.map_err(|e| anyhow::anyhow!("classify mutation result: {e}"))?;
match rec {
Some(r) if r.step_marker.is_some() => Ok(MutationOutcome::AlreadyCommitted),
_ => Ok(MutationOutcome::LeaseLost),
}
}
}
}
/// Decode the report target hex into binary (public for the action recovery worker).
pub type TargetPair = (Option<Vec<u8>>, Option<Vec<u8>>);
/// Derive the enforcement target from a full report detail.
///
/// This is the single source of truth for "who/what does enforcement act on",
/// shared by the HTTP driver ([`resolve_report_with_enforcement`]) and the action
/// recovery worker (via [`derive_enforcement_target_pub`]). Because both paths
/// derive from the same immutable report row + stored event row — and the action
/// record persists no target columns of its own — a stranded action alwaysView on GitHub (pinned to eed74bde2f)
Solutions
- Inspect the inner `{e}` for the classification read failure and restore DB connectivity / pool capacity.
- Retry the whole action later — the recovery worker re-drives it after lease expiry; no partial state was written by this driver.
- Add retry-with-backoff on `get_admin_action` in the classification path for transient errors.
- Check for many concurrent drivers on the same action_id (lease contention) and reduce duplicate driving.
- Verify action_id is valid — an invalid/corrupt action_id could also make the read fail depending on the DB layer.
Example fix
// before: single read, any error aborts classification
let rec = state.db.get_admin_action(action_id).await
.map_err(|e| anyhow::anyhow!("classify mutation result: {e}"))?;
// after: small retry for transient read failures
let rec = retry_transient(3, || state.db.get_admin_action(action_id)).await
.map_err(|e| anyhow::anyhow!("classify mutation result: {e}"))?; Defensive patterns
Strategy: retry
Validate before calling
// ensure DB reachable before classification
state.db.ping().await.map_err(|e| anyhow::anyhow!("classification pre-check: db unreachable: {e}"))?; Type guard
fn is_transient_read_error(e: &anyhow::Error) -> bool {
let s = format!("{e:#}");
["timeout", "connection", "closed", "pool timed out"].iter().any(|k| s.contains(k))
} Try / catch
// classification read with bounded retries
let rec = loop {
match state.db.get_admin_action(action_id).await {
Ok(r) => break Ok(r),
Err(e) if attempts < 3 && is_transient_read_error(&e) => { attempts += 1; sleep(BACKOFF).await; }
Err(e) => break Err(anyhow::anyhow!("classify mutation result: {e}")),
}
}; Prevention
- Bound concurrent drivers per action_id so the Ok(false) fence path is rare.
- Retry transient reads with backoff instead of treating a classification hiccup as fatal.
- Remember no partial state was committed by this driver when classification fails — deferring to the recovery worker is always safe.
- Size the DB pool for the moderation burst profile.
When it happens
Trigger: execute_*_with_marker returned Ok(false) (fence rejected) AND the follow-up `state.db.get_admin_action(action_id)` errors — Postgres outage, pool exhaustion, or timeout occurring in the narrow window after a contested lease.
Common situations: Concurrent drivers (HTTP resolve path and recovery worker) racing on the same action while the DB is under load; connection pool maxed during a moderation burst; network blip between mutation and classification read.
Related errors
- ban failed: {e}
- timeout failed: {e}
- delete failed: {e}
- deletion request is not runnable, is blocked, or is leased b
- system message insert failed: {e}
AI-assisted analysis of block/buzz@eed74bde2f (2026-08-30).
Data as JSON: /api/errors/959da1bb08366d7e.
Report an issue: GitHub.