nautechsystems/nautilus_trader · error

Execution intent {intent_id} is not recoverable from prepara

Error message

Execution intent {intent_id} is not recoverable from preparation

What it means

The conditional UPDATE (`WHERE id = $1 AND status = 'prepared'`) affected 0 rows even though the earlier SELECT FOR UPDATE read the row, so anyhow::ensure! raises "... is not recoverable from preparation". Since the row is FOR UPDATE-locked inside the same transaction, status should not have changed; 0 rows usually signals a logic/data anomaly rather than a race.

Source

Thrown at crates/adapters/blockchain/src/cache/database.rs:6537

        .await
        .map_err(|e| anyhow::anyhow!("Failed to lock recoverable execution intent: {e}"))?
        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
        anyhow::ensure!(
            current_status == "prepared",
            "Execution intent {intent_id} is {current_status}, not recoverable before signing"
        );
        let result = sqlx::query(
            "
            UPDATE execution_intent
            SET status = 'recoverable', active = FALSE, updated_at = NOW()
            WHERE id = $1 AND status = 'prepared'
            ",
        )
        .bind(intent_id)
        .execute(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to mark execution intent recoverable: {e}"))?;
        anyhow::ensure!(
            result.rows_affected() == 1,
            "Execution intent {intent_id} is not recoverable from preparation"
        );
        sqlx::query(
            "
            INSERT INTO execution_transaction_transition (
                intent_id, transition_key, from_status, to_status
            ) VALUES ($1, 'recoverable', $2, 'recoverable')
            ON CONFLICT (intent_id, transition_key) DO NOTHING
            ",
        )
        .bind(intent_id)
        .bind(current_status)
        .execute(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to record recoverable transition: {e}"))?;
        transaction
            .commit()

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Inspect the row directly: SELECT id, status FROM execution_intent WHERE id = $1; confirm the status value matches 'prepared' byte-for-byte (casing, whitespace).
  2. Check for triggers or row-level security policies on execution_intent that could alter visibility or the status between SELECT and UPDATE.
  3. Verify no schema drift: the code expects a plain lowercase 'prepared' string in the status column.
  4. If it persists, treat as an internal invariant violation and file a bug with the transition audit log from execution_transaction_transition.
Defensive patterns

Strategy: try-catch

Validate before calling

let status: String = sqlx::query_scalar("SELECT status FROM execution_intent WHERE id = $1")
    .bind(intent_id)
    .fetch_one(pool)
    .await?;
assert_eq!(status, "prepared", "unexpected status encoding: {status:?}");

Try / catch

// Should be impossible under normal operation; log loudly and halt recovery
match db.mark_execution_intent_recoverable(id).await {
    Err(e) if e.to_string().contains("not recoverable from preparation") => {
        tracing::error!(%id, "invariant violation: locked row did not update");
        // escalate / page, inspect triggers and schema
    }
    other => other?,
}

Prevention

When it happens

Trigger: Calling mark_execution_intent_recoverable on an intent whose UPDATE matched no row — in practice only possible via data corruption, a modified/inconsistent schema (e.g. status column collation or case mismatch), triggers altering the row, or reading through a different snapshot/replica setup than the UPDATE writes to.

Common situations: A database trigger or middleware rewrote the status during the transaction; schema drift (status stored with different casing such as 'PREPARED'); someone patched the UPDATE predicate; exotic replication routing where the lock and update hit divergent views.

Understand the failure class

Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/e9c9bbcfa4d89848. Report an issue: GitHub.