clockworklabs/SpacetimeDB · critical
TODO: retry?
Error message
TODO: retry?
What it means
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.
Source
Thrown at crates/engine/src/relational_db.rs:1046
let (_tx_offset, tx_metrics, reducer) = self.release_tx(tx);
self.report_read_tx_metrics(reducer, tx_metrics);
res
}
/// Perform the transactional logic for the `tx` according to the `res`
pub fn finish_tx<A, E>(&self, tx: MutTx, res: Result<A, E>) -> Result<A, E>
where
E: From<DBError>,
{
if res.is_err() {
let (_, tx_metrics, reducer) = self.rollback_mut_tx(tx);
self.report_mut_tx_metrics(reducer, tx_metrics, None);
} else {
match self.commit_tx(tx).map_err(E::from)? {
Some((_tx_offset, tx_data, tx_metrics, reducer)) => {
self.report_mut_tx_metrics(reducer, tx_metrics, Some(tx_data));
}
None => panic!("TODO: retry?"),
}
}
res
}
/// Roll back transaction `tx` if `res` is `Err`, otherwise return it
/// alongside the `Ok` value.
pub fn rollback_on_err<A, E>(&self, tx: MutTx, res: Result<A, E>) -> Result<(MutTx, A), E> {
match res {
Err(e) => {
let (_, tx_metrics, reducer) = self.rollback_mut_tx(tx);
self.report_mut_tx_metrics(reducer, tx_metrics, None);
Err(e)
}
Ok(a) => Ok((tx, a)),
}View on GitHub (pinned to 3653d2ed49)
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.
Example fix
// before: many clients concurrently update one hot row
#[reducer]
fn bump(ctx: &ReducerContext) { ctx.db.counter().update(|c| c.n += 1); }
// after: shard the counter so concurrent commits rarely conflict
#[reducer]
fn bump(ctx: &ReducerContext) {
let shard = shard_for(ctx.sender) % SHARDS;
ctx.db.counter().shard(shard).update(|c| c.n += 1);
} Defensive patterns
Strategy: retry
Try / catch
// at the invocation boundary: retry the whole reducer on commit conflict
let mut attempt = 0;
loop {
match std::panic::catch_unwind(AssertUnwindSafe(|| execute_reducer_call(call.clone()))) {
Ok(res) => break res,
Err(p) if is_commit_conflict_panic(&p) && attempt < 5 => {
attempt += 1;
std::thread::sleep(backoff(attempt)); // 10ms, 40ms, 160ms, ...
}
Err(p) => std::panic::resume_unwind(p),
}
}
fn is_commit_conflict_panic(p: &(dyn std::any::Any + Send)) -> bool {
let msg = p.downcast_ref::<String>().map(|s| s.as_str())
.or_else(|| p.downcast_ref::<&'static str>().copied());
msg.map_or(false, |m| m.contains("TODO: retry?"))
} Prevention
- 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.
When it happens
Trigger: Concurrent transactions/reducers writing overlapping rows such that commit_tx cannot validate the read/write set and returns Ok(None) inside finish_tx.
Common situations: Hot rows or counters updated by many concurrent reducer calls; load tests with parallel writers; long-running transactions widening the conflict window.
Related errors
- cannot deserialize refs without a typespace
- length didn't fit in `u32`
- Deletion for non-existent table {table_id:?}... huh?
- begin mutable transaction while one is already active
- insert without active mutable transaction
AI-assisted analysis of clockworklabs/SpacetimeDB@3653d2ed49 (2026-08-20).
Data as JSON: /api/errors/9dd6f12cfae21e3c.
Report an issue: GitHub.