Partial Writes
This document explains how Statehouse prevents partial writes from being visible.
The Problem
Partial writes occur when:
- Transaction has multiple operations
- Some operations succeed, others fail
- System would be in inconsistent state
Example: transferring money between accounts:
with client.begin_transaction() as tx:
tx.write(agent_id="bank", key="account:A", value={"balance": 50})
# Crash here would leave account B unchanged
tx.write(agent_id="bank", key="account:B", value={"balance": 150})
The Guarantee
Statehouse guarantees atomic transactions:
- All operations succeed together
- Or none of them take effect
This is the "no partial transactions visible" invariant.
How It Works
- Staging: Operations are staged in memory
- Commit: All operations written atomically
- Acknowledgment: Only after all durable
Begin → Write A → Write B → Write C → Commit
↓
[All staged in memory]
↓
[Atomic commit to storage]
↓
[All visible simultaneously]
Failure Scenarios
Crash Before Commit
with client.begin_transaction() as tx:
tx.write(agent_id="a", key="x", value={"v": 1})
tx.write(agent_id="a", key="y", value={"v": 2})
# CRASH - transaction lost
Result: Neither write visible. Correct behavior.
Crash During Commit
with client.begin_transaction() as tx:
tx.write(agent_id="a", key="x", value={"v": 1})
tx.write(agent_id="a", key="y", value={"v": 2})
tx.commit() # CRASH during this
Result: Either both visible or neither. Never partial.
Crash After Commit
with client.begin_transaction() as tx:
tx.write(agent_id="a", key="x", value={"v": 1})
tx.write(agent_id="a", key="y", value={"v": 2})
commit_ts = tx.commit() # Returns successfully
# CRASH here
Result: Both writes durable and visible after recovery.
Implementation
Atomicity is achieved through:
- RocksDB WriteBatch: Groups operations
- Single writer: No concurrent commits
- Fsync: Ensures durability
// Simplified internal logic
let batch = WriteBatch::new();
for op in transaction.operations {
batch.put(op.key, op.value);
}
db.write(batch)?; // Atomic
Verification
Verify atomicity in tests:
# Write multiple keys
with client.begin_transaction() as tx:
tx.write(agent_id="test", key="a", value={"v": 1})
tx.write(agent_id="test", key="b", value={"v": 2})
tx.write(agent_id="test", key="c", value={"v": 3})
# Read all or none
a = client.get_state(agent_id="test", key="a")
b = client.get_state(agent_id="test", key="b")
c = client.get_state(agent_id="test", key="c")
# All exist, or none exist
assert a.exists == b.exists == c.exists
Implications
Because of atomic transactions:
- Safe to update related keys together
- No need for application-level locking
- Simpler error handling
- Reliable state for agents