nautechsystems/nautilus_trader · error · anyhow::Error

Failed to commit replacement transaction: {e}

Error message

Failed to commit replacement transaction: {e}

What it means

Wrapped sqlx error when transaction.commit() fails at the end of add_execution_replacement_hash (database.rs:3865-3869). All statements (lock, retire, upsert, status update, transition insert) already executed, so the durable outcome is ambiguous: the replacement may or may not have been persisted. The idempotency key 'replaced:{transaction_hash}' in the transition table makes a plain replay of the same call the safe resolution.

Source

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

            "
            INSERT INTO execution_transaction_transition (
                intent_id, transaction_hash_id, transition_key, from_status, to_status
            ) VALUES ($1, $2, $3, $4, 'replaced')
            ON CONFLICT (intent_id, transition_key) DO NOTHING
            ",
        )
        .bind(intent_id)
        .bind(row.id)
        .bind(format!("replaced:{transaction_hash}"))
        .bind(current_status)
        .execute(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to record replacement transition: {e}"))?;

        transaction
            .commit()
            .await
            .map_err(|e| anyhow::anyhow!("Failed to commit replacement transaction: {e}"))?;
        Ok(row)
    }

    /// Marks one order event as emitted after dispatch.
    ///
    /// # Errors
    ///
    /// Returns an error if the event kind is unknown, the intent is absent, the opposing
    /// terminal marker is already set, or persistence fails.
    pub async fn mark_execution_event_emitted(
        &self,
        intent_id: i64,
        event: &str,
    ) -> anyhow::Result<()> {
        let statement = match event {
            "acknowledgement" => {
                "UPDATE execution_intent SET acknowledgement_emitted = TRUE, updated_at = NOW() WHERE id = $1"
            }

View on GitHub (pinned to 2114cf6f76)

Solutions

  1. Retry add_execution_replacement_hash with identical arguments; ON CONFLICT clauses make the replay converge
  2. If repeated commits fail, verify connection stability (keepalives, PgBouncer pooling mode, Postgres uptime)
  3. Check the transition table afterward if you must know whether the first attempt committed: SELECT 1 FROM execution_transaction_transition WHERE intent_id = $1 AND transition_key = 'replaced:' || $2
  4. Keep the transaction short so the commit window is minimal
Defensive patterns

Strategy: retry

Type guard

fn is_commit_ambiguity(err: &anyhow::Error) -> bool {
    matches!(err.downcast_ref::<sqlx::Error>(), Some(sqlx::Error::Io(_)) | None)
}

Try / catch

match db.add_execution_replacement_hash(intent_id, chain_id, &hash).await {
    Ok(row) => Ok(row),
    Err(e) if is_commit_ambiguity(&e) => {
        // outcome unknown: replay; 'replaced:{hash}' transition key dedupes if it committed
        db.add_execution_replacement_hash(intent_id, chain_id, &hash).await
    }
    Err(e) => Err(e),
}

Prevention

When it happens

Trigger: Connection dying at commit; Postgres restart or failover; serialization failure surfacing at commit; pool returning a broken connection for the commit round-trip.

Common situations: Unstable network between the trading host and Postgres; failover events during bursts of replacement/reorg processing; connection reaper killing sessions held open by slow commit paths.

Related errors


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