nautechsystems/nautilus_trader · error · anyhow::Error

Failed to assign nonce {nonce} to execution intent: {e}

Error message

Failed to assign nonce {nonce} to execution intent: {e}

What it means

Thrown when the guarded UPDATE in assign_execution_intent_nonce fails at the database level (the zero-rows case is a separate error). Because it is a single UPDATE executed on the pool, failures are connection loss, statement timeout, or row-lock contention with the FOR UPDATE readers in record_execution_status and add_execution_transaction_hash.

Source

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

        intent_id: i64,
        nonce: u64,
    ) -> anyhow::Result<()> {
        let nonce_db = i64::try_from(nonce)
            .with_context(|| format!("Execution nonce {nonce} exceeds PostgreSQL BIGINT"))?;
        let result = sqlx::query(
            "
            UPDATE execution_intent
            SET nonce = $2, updated_at = NOW()
            WHERE id = $1
              AND status = 'prepared'
              AND (nonce IS NULL OR nonce = $2)
            ",
        )
        .bind(intent_id)
        .bind(nonce_db)
        .execute(&self.pool)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to assign nonce {nonce} to execution intent: {e}"))?;
        anyhow::ensure!(
            result.rows_affected() == 1,
            "Execution intent {intent_id} is not prepared for nonce {nonce}"
        );
        Ok(())
    }

    /// Releases an intent when no broadcast attempt can have occurred.
    ///
    /// # Errors
    ///
    /// Returns an error if the intent advanced to broadcast or persistence fails.
    pub async fn mark_execution_intent_recoverable(&self, intent_id: i64) -> anyhow::Result<()> {
        let mut transaction = self.pool.begin().await.map_err(|e| {
            anyhow::anyhow!("Failed to start recoverable execution transition: {e}")
        })?;
        let current_status = sqlx::query_scalar::<_, String>(
            "SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE",

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Retry the assignment with the same nonce - it is idempotent while status is 'prepared' and nonce is NULL or equal
  2. Assign the nonce before enabling watchers on the intent to avoid row-lock races
  3. Raise statement_timeout if lock waits dominate the wrapped {e}
Defensive patterns

Strategy: retry

Try / catch

for attempt in 0..3 {
    match db.assign_execution_intent_nonce(intent_id, nonce).await {
        Ok(()) => break,
        Err(e) if e.downcast_ref::<sqlx::Error>().is_some_and(|se| matches!(se, sqlx::Error::Io(_) | sqlx::Error::ConnectionClosed(_) | sqlx::Error::Database(_))) => {
            tokio::time::sleep(Duration::from_millis(50u64 << attempt)).await;
        }
        Err(e) => return Err(e),
    }
}

Prevention

When it happens

Trigger: A concurrent status transition holding the intent row lock while the nonce is assigned; connection drop mid-UPDATE; aggressive statement_timeout.

Common situations: The signer racing the receipt watcher on the same intent; busy nodes with tight timeouts.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21). Data as JSON: /api/errors/b5ba6cdf092f0ef8. Report an issue: GitHub.