-
Type:
Task
-
Resolution: Unresolved
-
Priority:
Major - P3
-
None
-
Affects Version/s: None
-
Component/s: Layered Tables
-
None
-
Storage Engines - Transactions
-
376.695
-
None
-
None
Problem
The layered cursor decides whether its read context changed by comparing the session's WT_GEN_HAS_SNAPSHOT generation (plus the read timestamp and the transaction's modification count) in __clayered_update_state. If nothing changed, it keeps the iteration flag and trusts the alternate constituent's parked position instead of repositioning it.
This assumes two snapshots with the same generation see the same transactions. That is not guaranteed:
- A commit makes its updates visible in _txn_release and only then calls _wt_gen_next(session, WT_GEN_HAS_SNAPSHOT, NULL).
- Prepare (since
WT-18967) bumps the generation right after __txn_remove_from_global_table, with the same gap. - A reader records its generation in _wt_session_gen_enter before building its snapshot in _txn_get_snapshot.
A reader can therefore record generation G and build a snapshot after the updates became visible but before the bump. That snapshot sees the new updates but carries the same generation as an earlier snapshot that did not.
| Committer | Reader |
|---|---|
| __wt_session_gen_enter records G | |
| _txn_release / _txn_remove_from_global_table: updates visible | |
| builds snapshot, sees the updates | |
| __wt_gen_next: G becomes G+1 |
Impact
A positioned follower walk whose snapshot changes (an autocommit step after a read-only commit, or a new transaction) inside that window can keep trusting a parked alternate past a key that was just committed or prepared. It can return a stale value, or data instead of WT_PREPARE_CONFLICT. This is the same bug fixed for prepare in WT-18717 (see test_layered_cursor31), narrowed to a race of a few instructions.
Why simple reorderings don't fix it
- Moving the bump before making updates visible does not close the race: a slow reader can record G before the bump and build its snapshot after the release. It also breaks the snapshot fast path in __wt_txn_get_snapshot, which relies on "an unchanged generation means nothing was published since my snapshot": a reader could record G+1, build a snapshot without the commit, and then keep reusing it after the commit returns, so a read-committed reader would miss a committed write until a later bump.
- Moving the bump inside visibility_rwlock is irrelevant: that lock is only taken for logged commits and only coordinates with the checkpoint start record, never with snapshot builders.
The fast path needs publish-then-bump with the generation read before building; the layered cursor needs bump-then-publish with the generation read after building. One bump cannot provide both.
A deterministic test will need a test hook or timing stress between making updates visible and the generation bump.