nautechsystems/nautilus_trader · error · anyhow::Error

Failed to lock migrated intent: {e}

Error message

Failed to lock migrated intent: {e}

What it means

This error wraps any sqlx failure from `SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE` during the verification migration that reconstructs terminal transaction state. It fires when the intent row cannot be locked/read inside the migration transaction — most often because the row does not exist (query_scalar expects exactly one row) or the SELECT fails (connection, aborted transaction, or SQL error). The underlying database error is interpolated into the message as {e}.

Source

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

                        })?;
                        let gas_used = i64::try_from(record.gas_used.ok_or_else(|| {
                            anyhow::anyhow!("Terminal migration record has no gas usage")
                        })?)
                        .context("Migration gas usage exceeds PostgreSQL BIGINT")?;
                        let receipt_success = record.receipt_success.ok_or_else(|| {
                            anyhow::anyhow!("Terminal migration record has no receipt status")
                        })?;
                        anyhow::ensure!(
                            receipt_success == (status == TransactionStatus::Finalized),
                            "Migration receipt status conflicts with terminal status"
                        );
                        let current_status = sqlx::query_scalar::<_, String>(
                            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",
                        )
                        .bind(record.intent_id)
                        .fetch_one(&mut *transaction)
                        .await
                        .map_err(|e| anyhow::anyhow!("Failed to lock migrated intent: {e}"))?;
                        let hash_result = sqlx::query(
                            "
                            UPDATE execution_transaction_hash
                            SET status = $3, block_number = $4, block_hash = $5,
                                receipt_success = $6, gas_used = $7,
                                effective_gas_price = $8, updated_at = NOW()
                            WHERE intent_id = $1 AND transaction_hash = $2 AND current
                              AND payload_expected
                            ",
                        )
                        .bind(record.intent_id)
                        .bind(transaction_hash)
                        .bind(status.as_str())
                        .bind(block_number)
                        .bind(block_hash)
                        .bind(receipt_success)
                        .bind(gas_used)
                        .bind(record.effective_gas_price.as_deref())

View on GitHub (pinned to 18893faf8b)

Solutions

  1. Check that every intent_id in the migration evidence exists in execution_intent before running the migration (SELECT id FROM execution_intent WHERE id = ANY($1)).
  2. Run the migration from a single worker/instance only, so FOR UPDATE locks cannot deadlock.
  3. Inspect the interpolated {e} in the log: RowNotFound means a missing intent row — fix the evidence or restore the row; 'current transaction is aborted' means an earlier statement in this transaction failed.
  4. Verify the target database has the expected schema (\d execution_intent) and that you are connected to the intended database.
  5. If the cause was a transient connection error, re-run the migration; the transaction rollback leaves state consistent.

Example fix

// before: migration assumes all evidence intent_ids exist
migrate_records(records).await?;

// after: pre-validate evidence against the DB
let missing = sqlx::query_scalar::<_, String>(
    "SELECT id FROM unnest($1::text[]) AS t(id) WHERE id NOT IN (SELECT id FROM execution_intent)",
).bind(&intent_ids).fetch_all(&pool).await?;
anyhow::ensure!(missing.is_empty(), "evidence references missing intents: {missing:?}");
Defensive patterns

Strategy: validation

Validate before calling

let exists = sqlx::query_scalar::<_, i64>(
    "SELECT COUNT(*) FROM execution_intent WHERE id = $1",
).bind(&record.intent_id).fetch_one(&pool).await?;
if exists == 0 {
    anyhow::bail!("intent {} missing from execution_intent; fix evidence before migrating", record.intent_id);
}

Type guard

fn migration_record_has_existing_intent(record: &MigrationRecord, intent_ids: &HashSet<String>) -> bool {
    intent_ids.contains(&record.intent_id)
}

Try / catch

match migrate_records(records).await {
    Err(e) if e.to_string().contains("Failed to lock migrated intent") => {
        tracing::error!("migration lock failed (missing row or concurrent migration): {e:#}");
        // do not blindly retry: check evidence/DB first, then retry once
    }
    Err(e) => return Err(e),
    Ok(()) => {}
}

Prevention

When it happens

Trigger: Running the verification migration when (1) record.intent_id has no row in execution_intent (fetch_one on an empty result -> RowNotFound), (2) the migration transaction was already aborted by an earlier failed statement or hits a serialization/deadlock failure on the FOR UPDATE lock under concurrent access, (3) the DB connection drops mid-transaction, or (4) the execution_intent table is missing/renamed in the target database.

Common situations: Migrating evidence that references intents hard-deleted from the DB; running the migration concurrently from two workers causing deadlocks on the row lock; pointing the migration at a database with an older/missing schema; transient network blips to Postgres during a long migration batch.

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