{"record":{"id":"9d88a466ecf4c97e","repo":"nautechsystems/nautilus_trader","slug":"execution-intent-intent-id-is-current-status","errorCode":null,"errorMessage":"Execution intent {intent_id} is {current_status}, not recoverable before signing","messagePattern":"Execution intent (.+?) is (.+?), not recoverable before signing","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"crates/adapters/blockchain/src/cache/database.rs","lineNumber":6522,"sourceCode":"\n    /// Releases an intent when no broadcast attempt can have occurred.\n    ///\n    /// # Errors\n    ///\n    /// Returns an error if the intent advanced to broadcast or persistence fails.\n    pub async fn mark_execution_intent_recoverable(&self, intent_id: i64) -> anyhow::Result<()> {\n        let mut transaction = self.pool.begin().await.map_err(|e| {\n            anyhow::anyhow!(\"Failed to start recoverable execution transition: {e}\")\n        })?;\n        let current_status = sqlx::query_scalar::<_, String>(\n            \"SELECT status FROM execution_intent WHERE id = $1 FOR UPDATE\",\n        )\n        .bind(intent_id)\n        .fetch_optional(&mut *transaction)\n        .await\n        .map_err(|e| anyhow::anyhow!(\"Failed to lock recoverable execution intent: {e}\"))?\n        .ok_or_else(|| anyhow::anyhow!(\"Execution intent {intent_id} was not found\"))?;\n        anyhow::ensure!(\n            current_status == \"prepared\",\n            \"Execution intent {intent_id} is {current_status}, not recoverable before signing\"\n        );\n        let result = sqlx::query(\n            \"\n            UPDATE execution_intent\n            SET status = 'recoverable', active = FALSE, updated_at = NOW()\n            WHERE id = $1 AND status = 'prepared'\n            \",\n        )\n        .bind(intent_id)\n        .execute(&mut *transaction)\n        .await\n        .map_err(|e| anyhow::anyhow!(\"Failed to mark execution intent recoverable: {e}\"))?;\n        anyhow::ensure!(\n            result.rows_affected() == 1,\n            \"Execution intent {intent_id} is not recoverable from preparation\"\n        );","sourceCodeStart":6504,"sourceCodeEnd":6540,"githubUrl":"https://github.com/nautechsystems/nautilus_trader/blob/18893faf8b356be3320add8de2f861b0b647cf06/crates/adapters/blockchain/src/cache/database.rs#L6504-L6540","documentation":"After locking the execution_intent row, the code runs anyhow::ensure!(current_status == \"prepared\", ...) before transitioning it to 'recoverable'. Recovery is only legal from the 'prepared' state (before signing); any other status — signed, broadcast, confirmed, replaced, already recoverable — trips this error. It prevents resurrecting or mutating intents whose execution already advanced.","triggerScenarios":"Calling mark_execution_intent_recoverable(intent_id) when the row's status column is anything other than 'prepared', e.g. the intent was already signed or broadcast, or was already marked recoverable.","commonSituations":"Two recovery workers race and one wins (second sees 'recoverable'); a retry fires after the intent already progressed to broadcast; an operator re-runs a recovery script against completed intents; a caller misidentifies which intents are still pending signing.","solutions":["Query the current status first (SELECT status FROM execution_intent WHERE id = $1) and only call the recoverable API for rows in 'prepared'.","Treat the error as benign in idempotent recovery loops: catch it, log the current status, and skip — the intent already advanced.","If the intent is stuck in an unexpected status, use the transition audit table (execution_transaction_transition) to determine the actual progress before acting.","Ensure concurrent workers serialize recovery work (e.g. distributed lock or claim column) so only one caller attempts the transition."],"exampleFix":"// before\ndb.mark_execution_intent_recoverable(intent_id).await?; // panics-safe but errors on signed intents\n// after\nlet status: String = sqlx::query_scalar(\"SELECT status FROM execution_intent WHERE id = $1\")\n    .bind(intent_id)\n    .fetch_one(&db.pool)\n    .await?;\nif status == \"prepared\" {\n    db.mark_execution_intent_recoverable(intent_id).await?;\n} else {\n    tracing::info!(%intent_id, %status, \"intent already advanced; skipping recovery\");\n}","handlingStrategy":"validation","validationCode":"async fn can_recover(pool: &sqlx::PgPool, intent_id: i64) -> anyhow::Result<bool> {\n    let status: Option<String> = sqlx::query_scalar(\"SELECT status FROM execution_intent WHERE id = $1\")\n        .bind(intent_id)\n        .fetch_optional(pool)\n        .await?;\n    Ok(status.as_deref() == Some(\"prepared\"))\n}","typeGuard":null,"tryCatchPattern":"// Treat non-'prepared' statuses as expected in idempotent recovery\nif let Err(e) = db.mark_execution_intent_recoverable(id).await {\n    let msg = e.to_string();\n    if msg.contains(\"not recoverable before signing\") {\n        tracing::info!(%id, \"intent already advanced; skipping\");\n    } else {\n        return Err(e);\n    }\n}","preventionTips":["Filter recovery candidates to status = 'prepared' before calling the API.","Make recovery loops idempotent: skip, don't fail, on already-advanced intents.","Serialize concurrent recovery workers to avoid double-transition races.","Consult execution_transaction_transition to confirm intent progress before acting."],"tags":["database","invalid-state-transition","concurrency"],"backgroundTag":"invalid-state-transition","analyzedSha":"18893faf8b356be3320add8de2f861b0b647cf06","analyzedAt":"2026-09-08T20:49:34.690Z","contentChangedAt":"2026-09-08T20:49:34.690Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}