nautechsystems/nautilus_trader · error

Failed to lock execution intent for nonce assignment: {e}

Error message

Failed to lock execution intent for nonce assignment: {e}

What it means

In `assign_execution_intent_nonce_verified`, the code issues `SELECT ... FROM execution_intent WHERE id = $1 FOR UPDATE` inside a transaction to lock the intent row for a canonical nonce assignment. This error wraps any SQLx failure of that locking SELECT (connection loss, deadlock/lock timeout, syntax/schema mismatch, cancellation). The library throws it because nonce assignment must serialize on the intent row; if the lock query itself fails, the whole atomic operation is aborted and the caller gets this anyhow-wrapped error.

Source

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

            next_nonce == nonce,
            "Execution nonce {} does not match canonical nonce {next_nonce}",
            assignment.nonce
        );

        let (intent_chain_id, intent_wallet, intent_nonce, intent_status, intent_active) =
            sqlx::query_as::<_, (i32, String, Option<i64>, String, bool)>(
                "
            SELECT chain_id, wallet_address, nonce, status, active
            FROM execution_intent
            WHERE id = $1
            FOR UPDATE
            ",
            )
            .bind(assignment.intent_id)
            .fetch_optional(&mut *transaction)
            .await
            .map_err(|e| {
                anyhow::anyhow!("Failed to lock execution intent for nonce assignment: {e}")
            })?
            .ok_or_else(|| {
                anyhow::anyhow!("Execution intent {} was not found", assignment.intent_id)
            })?;
        anyhow::ensure!(
            intent_chain_id == chain_id
                && intent_wallet == assignment.wallet_address
                && intent_status == "prepared"
                && intent_active
                && intent_nonce.is_none_or(|assigned| assigned == nonce),
            "Execution intent {} cannot own canonical nonce {}",
            assignment.intent_id,
            assignment.nonce
        );

        for (index, decision) in assignment.decisions.iter().enumerate() {
            let height_start = decision
                .height_start

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check database connectivity and pool health (connection limits, TLS, idle timeouts) and retry the operation; the transaction was rolled back so it is safe to re-run.
  2. Verify migrations have been applied so the execution_intent table and columns exist.
  3. Reduce lock contention: keep the pre-lock work short, avoid other transactions that lock execution_intent rows in a different order, and tune deadlock_timeout/lock_timeout.
  4. Inspect the inner `{e}` SQLx error (it is preserved in the message) for the concrete cause, e.g. `40P01 deadlock_detected` or `57P01 admin shutdown`.

Example fix

// before: firing many assignments concurrently for the same intent/wallet
let handles: Vec<_> = intents.iter().map(|a| db.assign_execution_intent_nonce_verified(a)).collect();
// after: serialize assignments per (chain_id, wallet) or retry on transient DB errors
for a in intents {
    match db.assign_execution_intent_nonce_verified(a).await {
        Ok(()) => {}
        Err(e) if is_transient_db_error(&e) => backoff_retry(|| db.assign_execution_intent_nonce_verified(a)).await?,
        Err(e) => return Err(e),
    }
}
Defensive patterns

Strategy: retry

Validate before calling

let exists: Option<i64> = sqlx::query_scalar("SELECT 1 FROM execution_intent WHERE id = $1").bind(&assignment.intent_id).fetch_optional(pool).await?;
anyhow::ensure!(exists.is_some(), "intent {} missing before assignment", assignment.intent_id);

Try / catch

match db.assign_execution_intent_nonce_verified(&assignment).await {
    Ok(()) => {}
    Err(e) if e.to_string().contains("Failed to lock execution intent") && is_transient(&e) => backoff_retry(...).await?,
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: Calling `assign_execution_intent_nonce_verified` when: the Postgres connection drops mid-transaction (pool error, TLS failure); the SELECT ... FOR UPDATE deadlocks or exceeds lock_timeout because another transaction holds a conflicting lock on the same execution_intent row; the execution_intent table/columns are missing (unmigrated database); or the query is cancelled.

Common situations: Two workers assigning nonces for the same wallet concurrently causing lock contention or deadlock; database restart or failover during the call; running against a database without the execution_intent schema applied; long-running transactions hitting idle-in-transaction timeouts.

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 nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/92596a6c385181b7. Report an issue: GitHub.