{"record":{"id":"6ef2bd96aa87dcb6","repo":"clockworklabs/SpacetimeDB","slug":"invalid-transaction-offset-expected","errorCode":null,"errorMessage":"invalid transaction offset {}, expected {}","messagePattern":"invalid transaction offset (.+?), expected (.+?)","errorType":"exception","errorClass":"io::Error","httpStatus":null,"severity":"error","filePath":"crates/commitlog/src/segment.rs","lineNumber":127,"sourceCode":"    pub(crate) min_tx_offset: u64,\n    pub(crate) bytes_written: u64,\n\n    pub(crate) offset_index_head: Option<OffsetIndexWriter>,\n}\n\nimpl<W: io::Write> Writer<W> {\n    pub fn commit<T: Into<Transaction<U>>, U: Encode>(\n        &mut self,\n        transactions: impl IntoIterator<Item = T>,\n    ) -> io::Result<Option<Committed>> {\n        for tx in transactions {\n            let tx = tx.into();\n            let expected_offset = self.commit.min_tx_offset + self.commit.n as u64;\n            if tx.offset != expected_offset {\n                self.commit.n = 0;\n                self.commit.records.clear();\n\n                return Err(io::Error::new(\n                    io::ErrorKind::InvalidInput,\n                    format!(\"invalid transaction offset {}, expected {}\", tx.offset, expected_offset),\n                ));\n            }\n            assert!(\n                self.commit.n < u16::MAX,\n                \"maximum number of transactions in a single commit exceeded\"\n            );\n            self.commit.n += 1;\n            tx.txdata.encode_record(&mut self.commit.records);\n        }\n\n        if self.commit.n == 0 {\n            return Ok(None);\n        }\n\n        let checksum = self\n            .commit","sourceCodeStart":109,"sourceCodeEnd":145,"githubUrl":"https://github.com/clockworklabs/SpacetimeDB/blob/3653d2ed498fddbd83b7062aa6bf8970e18635ec/crates/commitlog/src/segment.rs#L109-L145","documentation":"segment::Writer::commit enforces contiguity inside a commit batch: each transaction's offset must equal the batch's min_tx_offset plus its index within the batch. On a gap, duplicate, or out-of-order offset the pending commit buffer is reset and io::ErrorKind::InvalidInput is returned naming the bad and expected offsets.","triggerScenarios":"Passing transactions whose offsets skip values (e.g. 1,2,4); retrying a batch whose first transaction already committed (duplicate offset); building the batch from a source that filtered some transactions out, leaving holes; off-by-one when assigning offsets.","commonSituations":"Generating offsets from user-supplied sequence numbers without normalizing; retry logic that replays a partially committed batch; consuming from an upstream system with its own gaps.","solutions":["Query the commitlog for the last committed transaction range and start assigning offsets from its end","Filter and renumber transactions to a contiguous range before calling commit","On retry of a partially applied batch, resume from the next uncommitted offset instead of replaying from the start","Assert offsets in tests with contiguous generators (0..n) rather than hand-written literals"],"exampleFix":"// before\nwriter.commit([tx(3), tx(4), tx(6)])?; // gap at 5\n\n// after\nlet next = log.last_tx_offset()? + 1;\nlet txs = [tx(3), tx(4), tx(6)]\n    .into_iter()\n    .enumerate()\n    .map(|(i, mut tx)| { tx.offset = next + i as u64; tx });\nwriter.commit(txs)?;","handlingStrategy":"validation","validationCode":"fn contiguous_from(next: u64, txs: Vec<Tx>) -> io::Result<Vec<Tx>> {\n    for (i, tx) in txs.iter().enumerate() {\n        let expected = next + i as u64;\n        if tx.offset != expected {\n            return Err(io::Error::new(\n                io::ErrorKind::InvalidInput,\n                format!(\"gap before batch: have {} expected {expected}\", tx.offset),\n            ));\n        }\n    }\n    Ok(txs)\n}","typeGuard":"fn is_tx_offset_mismatch(err: &io::Error) -> bool {\n    err.kind() == io::ErrorKind::InvalidInput && err.to_string().starts_with(\"invalid transaction offset\")\n}","tryCatchPattern":"match writer.commit(batch) {\n    Ok(committed) => Ok(committed),\n    Err(e) if is_tx_offset_mismatch(&e) => {\n        // writer reset its batch; rebase offsets onto the log's next offset and retry once\n        let next = last_committed_offset()?;\n        writer.commit(renumber_from(next, batch))\n    }\n    Err(e) => Err(e),\n}","preventionTips":["Assign offsets from the commitlog's last committed range end, never from caller input","Make retries resumable: remember the next uncommitted offset, do not replay from batch start","Add a unit assertion that batch offsets are strictly consecutive before calling commit"],"tags":["rust","commitlog","transaction-offset","contiguity","invalid-input"],"backgroundTag":"sequence-number-gap","analyzedSha":"3653d2ed498fddbd83b7062aa6bf8970e18635ec","analyzedAt":"2026-08-20T06:08:37.179Z","contentChangedAt":"2026-08-20T06:08:37.179Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}