Skip to content

feat(ssi): complete and harden Serializable Snapshot Isolation#37

Open
gburd wants to merge 4 commits into
masterfrom
feat/ssi-hardening
Open

feat(ssi): complete and harden Serializable Snapshot Isolation#37
gburd wants to merge 4 commits into
masterfrom
feat/ssi-hardening

Conversation

@gburd

@gburd gburd commented Jul 23, 2026

Copy link
Copy Markdown
Collaborator

What

Completes the DB_TXN_SNAPSHOT_SAFE (SSI) feature and closes the correctness
gaps an external engineering review identified. Three logical commits:

  1. fix(ssi): si_ref init race + reject prepare()/2PC — moves the si_ref
    initialization under the region lock before the detail is published (was a
    published-before-initialized shared-memory field), and rejects
    DB_TXN_SNAPSHOT_SAFE + prepare() with EINVAL (SSI's commit-time pivot
    check could otherwise turn a prepared txn into a reachable __env_panic).
  2. fix(ssi): race-free commit-time pivot check + bounded SIREAD reclaim
    serializes the rw-conflict flag writes and the commit-time read on the
    txn-region mutex (they were unsynchronized under the default multi-partition
    lock manager, so a genuine pivot could commit); and adds a __txn_begin-
    driven bounded sweep so committed-reader markers no longer accumulate
    unbounded between checkpoints.
  3. feat(ssi): MVCC version-chain detection in mp_fget — implements
    Cahill's second rw-conflict detection mechanism (the one that was missing),
    catching conflicts where the writer commits before the read physically
    happens. Without it SSI ≈ plain snapshot isolation for those schedules.

Why

An external review found the shipped SSI flag was a partial implementation of a
correctness guarantee: only one of two detection mechanisms, an unbounded
resource-retention mode, a probable init race, and an unresolved 2PC panic —
with a single positive-path test. Every one of those is addressed here.

Testing

All via the Nix dev shell (Linux x86_64, clang, TCL 8.6), each new test proven
to isolate the behaviour it guards:

  • ssi003 — prepare() rejected, env survives (no panic).
  • ssi004 — write-skew forced onto 64 lock partitions ×50 (the cross-partition
    flag race); 50/50 prevented.
  • ssi005 — 400 committed readers, no checkpoint; peak lock objects stays at 7
    (not ~400), proving bounded reclaim.
  • ssi006 — write-skew only the version-chain mechanism can catch; verified
    to fail with mechanism (b) disabled and pass with it
    .
  • Existing ssi001/ssi002, plus txn001004/lock001/002/005/mut001
    regression: no regressions.
  • ASan+UBSan clean on the SSI path (caught and fixed a real
    SH_CHAIN_NEXTP-walks-off-end pointer bug during development).

Notes

  • Depends conceptually on build: track header dependencies in the Autoconf build #36 (header-dependency tracking) for safe incremental
    rebuilds after the src/dbinc/lock.h struct change, but does not require it
    to merge — a clean build works either way.
  • SSI remains labelled experimental on the honest remaining grounds:
    page-granularity conflict tracking can raise abort rates under contention, and
    the qualification test matrix (HA/replication, recovery mid-state, region
    exhaustion, measured abort-rate benchmarks) is still being built — that is the
    next PR (Tier 2).

gburd added 4 commits July 23, 2026 13:42
Two correctness fixes to the DB_TXN_SNAPSHOT_SAFE transaction path, plus the
honesty caveats the feature warranted.

1. si_ref was initialized *after* the transaction detail was published on the
   region's active-transaction list. TXN_DETAIL is region memory with no
   guaranteed zeroing, so a concurrent walker (lock-manager conflict flagging,
   checkpoint marker reclaim, stats) could read garbage in that window. Move
   the `td->si_ref = 0` initialization above the list insert, under
   TXN_SYSTEM_LOCK.

2. SSI is not yet compatible with two-phase commit: the pivot check that can
   abort an SSI transaction runs at commit time, but a prepared transaction
   must be guaranteed committable (upstream panics the environment if a
   prepared txn cannot commit). An SSI txn could acquire its second conflict
   edge after prepare, turning that panic into a reachable environment-wide
   crash for any XA user. Reject DB_TXN_SNAPSHOT_SAFE + prepare() with EINVAL
   until the conflict status can be frozen at prepare time.

README gains an Oracle non-affiliation disclaimer and a note that v5.3.29+ are
community fork releases (Oracle's final 5.3 was 5.3.28).

Test: ssi003 verifies prepare() is rejected, the txn aborts cleanly, a plain
txn can still prepare, and the environment survives (no panic).
…laim

Two entangled correctness fixes in the SSI lock/reclaim machinery.

Pivot-flag race: the rw-conflict flags (TXN_DTL_RCONF/TXN_DTL_WCONF) are set on
a transaction detail by writers in the lock manager and read by __txn_commit's
pivot check. Under the default multi-partition lock manager, the flag writes
hold only a per-partition mutex and the commit-time read took no lock at all,
so a writer in a different partition could set the second edge between the two
flag reads (or just after they pass), letting a genuine pivot commit -- exactly
in the high-contention schedules SSI exists for. Serialize both the flag writes
(__lock_get_internal) and the commit-time read (__txn_commit) on the txn-region
mutex. This is the only partition-independent, cross-process-correct choice;
the naive lock-region approach violates the lock/txn region ordering that
lock_deadlock.c and __txn_end rely on. Ordering mtx_partition/mtx_hash ->
txn-region is verified non-circular.

Bounded reclaim: committed readers' SIREAD markers were reclaimed only at
checkpoint (__lock_sicleanup), so with no checkpoints the marker count -- and
the committed-reader locker/detail structs it pins -- grew as a function of
total transactions ever run, an unbounded leak for a long-lived embedded
process. Add an approximate region counter (nsireaders) and a __txn_begin-
driven sweep that runs when live markers exceed half the allocated lock
objects, so retention is bounded by concurrent load, not history.

Tests: ssi004 forces the write-skew onto 64 lock partitions x50 iterations
(the cross-partition race condition); ssi005 runs 400 committed readers with no
checkpoint and asserts peak object usage stays bounded (observed: 7, not ~400).
Implements the second of Cahill's two rw-antidependency detection mechanisms,
the one that was missing. Mechanism (a), already present, uses the lock table:
a snapshot writer taking a WRITE lock sees a concurrent reader's SIREAD marker.
But once a writer commits it drops its locks, so a conflict where the writer
commits *before* the reader physically reads goes undetected by (a) -- and some
non-serializable schedules then commit. This is why SSI was labelled
incomplete.

Mechanism (b) closes that gap in the buffer pool. At the snapshot-read version-
chain walk in __memp_fget, when a DB_TXN_SNAPSHOT_SAFE reader is handed the
version visible at its read_lsn and newer committed versions exist on the chain
(SH_CHAIN_HASNEXT), each newer version was created by a concurrent writer W:
the reader R read a version W overwrote, an edge R --rw--> W. Record the edge
with the same flag rules as mechanism (a) and return DB_SNAPSHOT_CONFLICT if R
becomes the pivot of a dangerous structure -- aborting earlier than commit
time. The walk holds hp->mtx_hash; the pivot flags are read/written under
TXN_SYSTEM_LOCK (order mtx_hash -> txn-region, verified non-circular).

The version-chain walk must use SH_CHAIN_NEXT, not SH_CHAIN_NEXTP: the latter
does not return NULL at chain end (it computes elm + -1), which walks off into
freed memory. ASan+UBSan clean.

Test: ssi006 constructs a write-skew where writers overwrite items and commit
with no live SIREAD marker present, so mechanism (a) records nothing; snapshot
readers then cross-read the overwritten items and only the version-chain check
can catch it. Verified to fail with (b) disabled and pass with it.

With both mechanisms present, a race-free commit check, bounded reclaim, and
2PC rejection, the README and ROADMAP drop the "incomplete detection" caveat;
SSI remains "experimental" only on page-granularity abort rates and the
not-yet-built qualification matrix.
Extends the bounded SIREAD reclaim: it freed marker objects but never freed the
committed readers' locker entries, so a long-lived process running many
committed snapshot-safe *read-only* transactions exhausted the statically sized
locker region ("out of available locker entries") even while object usage
stayed tiny. Root causes, both fixed:

1. A read-only SSI reader never sets visible_lsn (that is set in __txn_end only
   when the txn created MVCC versions, i.e. wrote). __lock_siclean_obj's
   retention test treated a MAX_LSN visible_lsn as "not yet visible" and kept
   the marker forever. Order read-only markers by their read_lsn instead: drop
   them once the oldest active snapshot has advanced past the reader's snapshot
   (a committed reader is no longer abortable, so its marker only needs to
   outlive concurrent snapshots, which carry their own conflict state).

2. Nothing actually freed the DB_LOCKER_FREED lockers once their last marker was
   reclaimed. Add __lock_si_reap_lockers, called at the end of __lock_sicleanup
   (no object mutex held, so object->locker latch ordering holds), to reap them.

3. The reclaim trigger keyed only on object count, but reused-key workloads keep
   the object count tiny while lockers still accumulate one per committed
   reader. Also trigger when current lockers exceed half the locker cap.

Test: ssi007 runs 2000 committed readers on a 200-object/2000-locker region
(far more readers than either cap) and asserts no exhaustion; and separately
that a genuinely exhausted region returns a clean error with the environment
still usable (no panic/corruption). Correctness (ssi001/002/004/006) verified
across repeated runs; ASan+UBSan clean.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant