You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Cassandra: a single-item write inside a transaction's PREPARE window is accepted undetected, and a deleted item is recreated by the transaction #578
On the Cassandra backend a single-item write that commits inside a transaction's PREPARE window is neither serialised nor detected, and an item deleted in that window is recreated by the transaction. Read from crates/storage-cassandra/src/data/transactions.rs at main744ea29 and measured against a cassandra:4.1 node on 2026-10-25.
Mechanism
Each PREPARE arm reads the row (fetch_item_for_transaction), evaluates the condition, then plants its marker with an LWT: UPDATE ... SET prepared_txn_id = ? ... IF prepared_txn_id = null on an existing row, INSERT ... IF NOT EXISTS with created_to_prepare = true on an absent one. Single-item writes are fenced out of the commit phase (occ_write's LWT is IF version = ? AND prepared_txn_id = null, and the single-item delete refuses when prepared_txn_id is set), but not out of the window between the transaction's read and its marker: a single-item write committing there leaves prepared_txn_id null, so the marker's own LWT applies and the transaction proceeds with a pre-image one write stale.
Measured: the marker statement UPDATE ... SET prepared_txn_id = ? WHERE pk = <absent> IF prepared_txn_id = null applies on a row that does not exist and creates it with item_data null. So when a single-item DeleteItem lands in the window, the transaction's Update arm finds a marker row with no item, computes its post-image from the bare key plus the actions, and commits it: the deleted item comes back. A transacted Delete in the same race applies its DELETE ... IF prepared_txn_id = ? against the recreated marker.
With #576 the stream records what the row held at commit time, so the record no longer claims a transition that did not happen; the write itself is still accepted and the resurrection still occurs.
Shapes
Single-item write in the PREPARE window
Transaction op
Result on main
PutItem A to B
Put or Delete
the transaction's condition was evaluated against A; the op applies
PutItem A to B
Update
post-image is f(B), condition evaluated against A
DeleteItem of A
Update
the row is recreated as key plus actions
DeleteItem of A
Delete
the marker row is created and then deleted; two deletes for one item
The service's TransactWriteItems serialises against concurrent single-item writes on the same item (a conflicting write cancels the transaction with TransactionConflict, or the transaction waits), and never recreates a deleted item.
Fix shape
Make the marker LWT carry the read's version: IF prepared_txn_id = null AND version = ? for an existing row (the single-item path already fences on version), and for an absent row keep INSERT ... IF NOT EXISTS, so a single-item write between the read and the marker fails the marker's LWT and the PREPARE phase re-reads or cancels with TransactionConflict. A storage test that interleaves a single-item write between a transaction's read and its marker (a hook or a split PREPARE in a test build) and asserts the transaction cancels or re-reads; a wire test with a tight loop of PutItem and TransactWriteItems on one key asserting every record's OldImage equals the previous record's NewImage.
Activity