Loading learning content...
When the Thomas Write Rule tells us to "ignore" an obsolete write, what does that actually mean? This seemingly simple instruction has subtle implications that affect transaction semantics, system behavior, and even recovery mechanisms.
Consider this: if a transaction T executes a sequence of operations read(X), write(Y), write(Z) and the write to Y is ignored as obsolete, what happens to T? Does T see its own write to Y? Does T's subsequent logic depend on Y being written? What if T later reads Y—does it see its ignored value or the value written by the superseding transaction?
These questions reveal that "ignoring" a write is more nuanced than simply skipping an instruction. In this page, we'll dissect the precise semantics of ignored writes and their implications for transaction processing.
By the end of this page, you will understand the exact semantics of ignored writes, how transaction-local state is affected, the read-own-write problem, implementation strategies for handling ignored writes, and the interaction with transaction logging and recovery.
When the Thomas Write Rule specifies that a write should be ignored, it means:
Definition (Ignored Write):
An ignored write is a write operation that:
Is not applied to the database — The data item retains its current value (from the superseding transaction's write)
Does not update W-TS — The write timestamp of the data item remains unchanged
Does not generate a log record — No redo/undo log entry is created for this write
Is treated as completed from the transaction's perspective — The transaction proceeds as if the write succeeded
Is invisible to subsequent reads by other transactions — Other transactions see the superseding transaction's value
The key insight: An ignored write is semantically equivalent to a write that executed but was immediately overwritten, except without the overhead of actually performing the write and update operations.
Think of an ignored write as a 'phantom write'—it appears in the transaction's logical execution but leaves no trace in the database. The transaction believes it wrote the value, but the value was never actually stored because it would have been immediately superseded.
Formal Semantics:
Let S be a schedule containing transaction T_i with write(X, v_i), where this write is ignored due to the Thomas Write Rule (because T_j with TS(T_j) > TS(T_i) has already written X).
The execution semantics are:
BEFORE ignore:
X.value = v_j (from T_j's write)
W-TS(X) = TS(T_j)
AFTER ignore:
X.value = v_j (unchanged)
W-TS(X) = TS(T_j) (unchanged)
T_i.status = ACTIVE (T_i continues normally)
Compare this to the ABORT case:
BEFORE abort:
X.value = v_j
W-TS(X) = TS(T_j)
AFTER abort:
X.value = v_j (unchanged)
W-TS(X) = TS(T_j) (unchanged)
T_i.status = ABORTED (T_i must restart)
T_i.work = DISCARDED (all T_i's work is lost)
The critical difference: with ignore, T_i continues with its other operations; with abort, T_i loses all its work.
One of the most subtle issues with ignored writes is the read-own-write problem. What happens if a transaction attempts to read a data item that it previously wrote, but that write was ignored?
The Scenario:
Transaction T₁ (timestamp 10):
write(X, 100) // IGNORED (W-TS(X) = 20 from T₂)
... other work ...
y = read(X) // What value does T₁ see?
if (y != 100) {
// Application logic error!
}
This is a genuine problem. If T₁ reads its own ignored write, what should happen?
Two Schools of Thought:
The Standard Solution: Transaction-Local Write Buffer
Most implementations use Option B with a transaction-local write buffer (also called a "private workspace" or "write set"):
When T₁ writes X = 100 and the write is ignored:
When T₁ later reads X:
At commit time:
This approach provides read-own-write consistency while still benefiting from the Thomas Write Rule's abort reduction.
The write buffer approach is used by most systems regardless of the Thomas Write Rule. It provides snapshot-like behavior for transaction-local reads. The Thomas Write Rule simply means that some entries in the write buffer will never be applied to the database.
Let's trace through the execution flow of a transaction that has some writes ignored, to understand the full impact on transaction processing.
Example Transaction:
Transaction T₁ (timestamp 10):
BEGIN
read(A) // A = 500
write(A, 600) // Successfully written, W-TS(A) = 10
read(B) // B = 200
write(B, 300) // IGNORED! W-TS(B) = 20 from T₂
write(C, 400) // Successfully written, W-TS(C) = 10
COMMIT
Execution Trace:
| Step | Operation | Database State | T₁ Write Buffer | Notes |
|---|---|---|---|---|
| 1 | BEGIN T₁ | A=500, B=200, C=0 | {} | Transaction starts |
| 2 | read(A) | A=500, B=200, C=0 | {} | Sees A=500, R-TS(A)=10 |
| 3 | write(A,600) | A=600, B=200, C=0 | {A→600} | W-TS(A)=10, success |
| 4 | read(B) | A=600, B=200, C=0 | {A→600} | Sees B=200, R-TS(B)=10 |
| 5 | write(B,300) | A=600, B=200, C=0 | {A→600, B→300} | IGNORED! W-TS(B)=20 > 10 |
| 6 | write(C,400) | A=600, B=200, C=400 | {A→600, B→300, C→400} | W-TS(C)=10, success |
| 7 | COMMIT | A=600, B=200, C=400 | discarded | B's entry was ignored |
Key Observations:
T₁ completes successfully — No abort occurred despite the conflict on B
T₁'s writes to A and C persisted — The non-conflicting writes are not affected
B retains T₂'s value — The ignored write has no effect on the final state
T₁'s write buffer held B→300 — If T₁ had read B again, it would have seen 300
No partial state visible — Other transactions never see an inconsistent state
What if T₁ had read B after the ignored write?
write(B, 300) // IGNORED
x = read(B) // T₁ sees 300 from write buffer, NOT 200 from database
This maintains the illusion that T₁'s write succeeded, even though it will be discarded at commit.
From T₁'s perspective, the write to B succeeded. T₁ can read its own write and continue with logic that depends on that value. Only at commit time is the ignored write silently discarded—and since the final database state is correct (view serializable), no harm is done.
The Thomas Write Rule has important implications for transaction logging and recovery. Let's examine how ignored writes interact with the logging subsystem.
Standard Write-Ahead Logging (WAL):
In a typical WAL implementation, before a write is applied to the database:
What Happens with Ignored Writes?
When a write is ignored due to the Thomas Write Rule:
This is a performance benefit: less logging overhead for large transactions with many ignored writes.
If the system crashes while T₁ is active (after the ignored write but before commit), the recovery system won't have any record of T₁'s attempted write to B. This is correct—the write was never applied, so there's nothing to undo. But it means debugging crashed transactions can be tricky if you're trying to understand what writes were attempted.
Comparison: Logged vs Non-Logged Ignored Writes
Some systems choose to log ignored writes for debugging/auditing purposes:
Approach 1: Don't Log Ignored Writes
- Pros: Better performance, simpler recovery
- Cons: No audit trail of attempted writes
Approach 2: Log Ignored Writes with IGNORED Flag
- Log record: (T₁, B, IGNORED, 300)
- Pros: Complete audit trail
- Cons: Additional logging overhead
- Recovery: Ignored log records are skipped during redo/undo
Approach 3: Log to Separate Debug Stream
- Primary log: No record for ignored writes
- Debug log: Records ignored writes for analysis
- Pros: Production performance + debugging capability
- Cons: Two log systems to maintain
Recovery Behavior:
During crash recovery:
The recovery is simpler precisely because ignored writes left no trace that needs to be managed.
While an individual ignored write is simple to handle, complex transactions may have cascading effects that require careful consideration.
Scenario 1: Conditional Logic Depending on Written Value
Transaction T₁ (timestamp 10):
old_balance = read(BALANCE) // 1000
new_balance = old_balance + 100
write(BALANCE, new_balance) // IGNORED (T₂ already wrote)
// T₁'s read-own-write sees 1100 from buffer
if (read(BALANCE) > 1000) {
write(VIP_STATUS, true) // This executes!
}
T₁ proceeds based on its own view (1100), but the actual balance is whatever T₂ wrote. The VIP_STATUS update may or may not be correct depending on application semantics.
Is this a problem?
Not necessarily. From view serializability perspective:
The schedule is view serializable, but the application semantics may be violated if VIP_STATUS should reflect the final balance.
Scenario 2: Foreign Key Relationships
T₁ (timestamp 10):
write(ORDERS.customer_id, 123) // IGNORED
write(ORDER_DETAILS.order_id, 456)
T₂ (timestamp 20):
write(ORDERS.customer_id, 789) // Already written, supersedes T₁
If ORDERS and ORDER_DETAILS have referential integrity constraints:
This is typically not a problem because:
But application logic might expect ORDER_DETAILS to reference orders with customer_id 123.
The Thomas Write Rule provides database-level correctness (view serializability), but application-level correctness depends on the application's semantics. If an application requires that related writes appear atomically, it should use mechanisms like UPDATE with row-level locking rather than relying on timestamp ordering.
Scenario 3: Multiple Ignored Writes
T₁ (timestamp 10):
write(X, 1) // IGNORED (T₂ wrote with ts=20)
write(Y, 2) // IGNORED (T₃ wrote with ts=30)
write(Z, 3) // SUCCESS
Multiple ignored writes in the same transaction are handled independently. Each ignored write:
T₁ may execute considerable logic based on its view of X and Y, but only its write to Z persists. This is correct from serializability, but applications should be aware of this behavior.
Let's examine several implementation patterns for handling ignored writes in production systems.
Pattern 1: Deferred Write with Validation at Commit
In this pattern, all writes are deferred to commit time:
Write Phase (during transaction):
1. Record write intent in private buffer
2. No immediate timestamp check
3. Transaction continues
Commit Phase:
1. For each write in buffer:
a. Check TS(T) vs R-TS(X) and W-TS(X)
b. If conflict with read: ABORT
c. If outdated: IGNORE
d. Otherwise: APPLY
2. Commit transaction
Pros:
Cons:
Pattern 2: Immediate Write with Rollback
In this pattern, writes are applied immediately:
Write Phase (during transaction):
1. Check TS(T) vs R-TS(X)
- If conflict: ABORT immediately
2. Check TS(T) vs W-TS(X)
- If outdated: Mark as ignored, continue
3. Otherwise: Apply write to database
Commit Phase:
1. Validate no conflicts arose during execution
2. Commit transaction
Abort Phase:
1. Rollback all applied writes (ignored writes have nothing to rollback)
Pros:
Cons:
Pattern 3: Hybrid with Write Intent Locks
Some systems combine timestamp ordering with lightweight intent locks:
Write Phase:
1. Acquire write intent lock on X (non-blocking notification)
2. Check timestamps:
- If TS(T) < R-TS(X): Release intent, ABORT
- If TS(T) < W-TS(X): Release intent, IGNORE
- Otherwise: Keep intent lock, defer actual write
Commit Phase:
1. Convert intent locks to real writes
2. Apply deferred writes
3. Release all locks
4. Commit
This pattern helps coordinate between transactions without full locking overhead, while still detecting ignored writes early.
Let's examine edge cases where the semantics of ignored writes can be tricky.
Edge Case 1: Ignored Write Followed by Read by Another Transaction
T₁ (ts=10): write(X, 100) [IGNORED, not in database]
T₃ (ts=15): read(X) [Sees value from T₂, not T₁]
T₂ (ts=20): write(X, 200) [Already happened, W-TS(X)=20]
T₃ reads X with R-TS(X) being updated to 15. But wait—we said T₁'s write was ignored because no read occurred between T₁ and T₂. Now T₃ has read!
Resolution: The timing matters. When T₁ attempted to write:
T₁'s write was correctly identified as outdated at that moment. T₃'s read happened after T₁'s attempted write, so it doesn't affect T₁'s decision. T₃ correctly reads T₂'s value (200).
Outdatedness is determined at the moment of the write attempt. Future reads by other transactions don't retroactively un-outdated a write. This is critical for correctness—once a write is ignored, it stays ignored.
Edge Case 2: Multiple Transactions Ignoring Same Write
T₄ (ts=40): write(X, 400) [Already happened, W-TS(X)=40]
T₁ (ts=10): write(X, 100) [IGNORED]
T₂ (ts=20): write(X, 200) [IGNORED]
T₃ (ts=30): write(X, 300) [IGNORED]
Three transactions have their writes to X ignored. All continue execution. Only T₄'s value persists.
Is this correct?
Yes! In serial schedule T₁ → T₂ → T₃ → T₄:
Ignoring T₁, T₂, T₃'s writes produces the same final value. The schedule is view serializable.
Edge Case 3: Same Transaction Writes Same Item Twice
T₁ (ts=10):
write(X, 100) // SUCCESS, W-TS(X)=10
... other work ...
write(X, 150) // What happens?
When a transaction writes the same item twice:
Now, what if T₂ (ts=20) intervenes?
T₁ (ts=10):
write(X, 100) // SUCCESS, W-TS(X)=10
T₂ (ts=20):
write(X, 500) // SUCCESS, W-TS(X)=20
T₁ (ts=10):
write(X, 150) // IGNORED! W-TS(X)=20 > 10
T₁'s second write is ignored. T₁'s write buffer shows X→150, but the database has 500.
We've comprehensively explored the semantics and implications of ignoring obsolete writes. Let's consolidate the key takeaways:
What's Next:
In the next page, we'll analyze the performance improvements achieved by the Thomas Write Rule. We'll examine quantitative metrics, benchmark comparisons with basic timestamp ordering, and identify workload characteristics that maximize the benefits of ignoring obsolete writes.
You now understand the precise semantics of ignoring obsolete writes—how the mechanism works, its interaction with transaction execution, logging, and recovery, and the edge cases that require careful handling. This knowledge is essential for implementing and reasoning about timestamp-based concurrency control.