{"record":{"id":"9dd6f12cfae21e3c","repo":"clockworklabs/SpacetimeDB","slug":"todo-retry","errorCode":null,"errorMessage":"TODO: retry?","messagePattern":"TODO: retry\\?","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"critical","filePath":"crates/engine/src/relational_db.rs","lineNumber":1046,"sourceCode":"        let (_tx_offset, tx_metrics, reducer) = self.release_tx(tx);\n        self.report_read_tx_metrics(reducer, tx_metrics);\n        res\n    }\n\n    /// Perform the transactional logic for the `tx` according to the `res`\n    pub fn finish_tx<A, E>(&self, tx: MutTx, res: Result<A, E>) -> Result<A, E>\n    where\n        E: From<DBError>,\n    {\n        if res.is_err() {\n            let (_, tx_metrics, reducer) = self.rollback_mut_tx(tx);\n            self.report_mut_tx_metrics(reducer, tx_metrics, None);\n        } else {\n            match self.commit_tx(tx).map_err(E::from)? {\n                Some((_tx_offset, tx_data, tx_metrics, reducer)) => {\n                    self.report_mut_tx_metrics(reducer, tx_metrics, Some(tx_data));\n                }\n                None => panic!(\"TODO: retry?\"),\n            }\n        }\n\n        res\n    }\n\n    /// Roll back transaction `tx` if `res` is `Err`, otherwise return it\n    /// alongside the `Ok` value.\n    pub fn rollback_on_err<A, E>(&self, tx: MutTx, res: Result<A, E>) -> Result<(MutTx, A), E> {\n        match res {\n            Err(e) => {\n                let (_, tx_metrics, reducer) = self.rollback_mut_tx(tx);\n                self.report_mut_tx_metrics(reducer, tx_metrics, None);\n\n                Err(e)\n            }\n            Ok(a) => Ok((tx, a)),\n        }","sourceCodeStart":1028,"sourceCodeEnd":1064,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/3653d2ed498fddbd83b7062aa6bf8970e18635ec/crates/engine/src/relational_db.rs#L1028-L1064","documentation":"RelationalDB::finish_tx commits a MutTx on the Ok path; commit_tx returns Ok(None) when the transaction conflicted with a concurrent commit and must be retried (optimistic concurrency detected at commit time). The retry path was never implemented, so instead of retrying, the host panics with 'TODO: retry?'. You hit it when two transactions write the same rows and the loser detects the conflict only at commit; note the whole transaction is discarded, so callers cannot recover the work in-process.","triggerScenarios":"Concurrent transactions/reducers writing overlapping rows such that commit_tx cannot validate the read/write set and returns Ok(None) inside finish_tx.","commonSituations":"Hot rows or counters updated by many concurrent reducer calls; load tests with parallel writers; long-running transactions widening the conflict window.","solutions":["Reduce contention: shard the hot row (e.g. N counter rows aggregated in a view) or serialize updates through a single reducer path.","Retry the operation at the caller or module boundary with backoff, since the transaction is valid but lost the race.","Upgrade spacetimedb and check the changelog for a release that retries commit conflicts instead of panicking.","Keep write transactions short to shrink the conflict window."],"exampleFix":"// before: many clients concurrently update one hot row\n#[reducer]\nfn bump(ctx: &ReducerContext) { ctx.db.counter().update(|c| c.n += 1); }\n\n// after: shard the counter so concurrent commits rarely conflict\n#[reducer]\nfn bump(ctx: &ReducerContext) {\n    let shard = shard_for(ctx.sender) % SHARDS;\n    ctx.db.counter().shard(shard).update(|c| c.n += 1);\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":null,"tryCatchPattern":"// at the invocation boundary: retry the whole reducer on commit conflict\nlet mut attempt = 0;\nloop {\n    match std::panic::catch_unwind(AssertUnwindSafe(|| execute_reducer_call(call.clone()))) {\n        Ok(res) => break res,\n        Err(p) if is_commit_conflict_panic(&p) && attempt < 5 => {\n            attempt += 1;\n            std::thread::sleep(backoff(attempt)); // 10ms, 40ms, 160ms, ...\n        }\n        Err(p) => std::panic::resume_unwind(p),\n    }\n}\n\nfn is_commit_conflict_panic(p: &(dyn std::any::Any + Send)) -> bool {\n    let msg = p.downcast_ref::<String>().map(|s| s.as_str())\n        .or_else(|| p.downcast_ref::<&'static str>().copied());\n    msg.map_or(false, |m| m.contains(\"TODO: retry?\"))\n}","preventionTips":["Design reducers to avoid hot rows: shard counters and batch updates.","Keep transactions short to narrow the commit-conflict window.","Make reducers idempotent so client-side retries are safe.","Load-test concurrent writers before shipping; upgrade spacetimedb for commit-retry fixes."],"tags":["spacetimedb","transaction","commit-conflict","contention","panic"],"backgroundTag":"optimistic-concurrency-conflict","analyzedSha":"3653d2ed498fddbd83b7062aa6bf8970e18635ec","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}