diff --git a/.gitignore b/.gitignore index 63e90c6c8..6d86c509d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ build_unix/** compile_commands.json +test/tcl/tclIndex diff --git a/dist/api_flags b/dist/api_flags index 9d606670e..77a02e018 100644 --- a/dist/api_flags +++ b/dist/api_flags @@ -275,6 +275,7 @@ DbEnv.txn_begin DB_TXN_FAMILY # Cursors and child txns are # independent but lock-compatible DB_TXN_SNAPSHOT # Snapshot isolation + DB_TXN_SNAPSHOT_SAFE # Serializable snapshot isolation (SSI) DB_TXN_SYNC # Always sync log on commit DB_TXN_WAIT # Always wait for locks in this txn DB_TXN_WRITE_NOSYNC # Write the log but don't sync diff --git a/docs/ssi/M2-partition-design.md b/docs/ssi/M2-partition-design.md new file mode 100644 index 000000000..30e384597 --- /dev/null +++ b/docs/ssi/M2-partition-design.md @@ -0,0 +1,88 @@ +# M2 design note — porting SIREAD lock GC to 5.3.x partitioned lock regions + +Scope: how Cahill's 2008 SIREAD-lock bookkeeping (built on BDB 4.6.21's single +global lock table) maps onto 5.3.x, whose lock region is **partitioned**. This +is the one area most likely to be subtly wrong, so we agree on it before coding. + +## What the prototype assumed (4.6.21) + +- One global object hash table; GC walked `for i in object_t_size { for obj in obj_tab[i] }`. +- `OBJECT_LOCK_NDX(lt, ndx)` — 2 args; one region mutex effectively serialized object access. +- It added its own `__txn_oldest_reader`. +- `__lock_siclean_obj` could call `__lock_freelocker` **while holding the object mutex**. + +## What actually exists in 5.3.x (verified) + +- `obj_tab` is still a flat array of `object_t_size` hash buckets. Partitioning + is a *locking* concern: `LOCK_PART(reg, ndx) = ndx % part_t_size` maps a + bucket to a partition mutex. +- `OBJECT_LOCK_NDX(lt, reg, ndx)` / `OBJECT_UNLOCK(lt, reg, ndx)` — **3 args**; + they lock/unlock only the partition owning `ndx`. +- `LOCK_SYSTEM_LOCK(lt, reg)` locks the single region mutex **only when + `part_t_size == 1`**; with multiple partitions it is a no-op and callers must + take partition mutexes individually. +- `__txn_oldest_reader(ENV *, DB_LSN *)` **already exists** (`txn_region.c`, + used for MVCC buffer freezing). It takes `TXN_SYSTEM_LOCK` internally and + scans `region->active_txn` for the smallest `read_lsn`. **Reuse it; do not + reimplement.** +- Locker freeing is `__lock_freelocker_int(lt, region, sh_locker, reallyfree)` + (the public `__lock_freelocker(lt, sh_locker)` wraps it) and runs under + `LOCK_LOCKERS` (`mtx_lockers`). +- Internal API is `ENV *env`, not `DB_ENV *dbenv`. + +## The critical decision: lock ordering + +Master's nesting is **system → partition**, and locker frees happen separately +under `mtx_lockers`. The prototype freed a locker *while holding the object +partition mutex* (partition → lockers). Reproducing that introduces a +partition→`mtx_lockers` ordering that master does not otherwise use — a +lock-order-inversion / deadlock risk. + +**Decision:** do **not** free lockers while holding a partition mutex. Instead, +within `__lock_siclean_obj`, only (a) detach the SIREAD lock and (b) decrement +`sh_locker->nlocks`; collect any locker that reaches `nlocks == 0 && FREED` +onto a small local victim list. After the partition mutex is released, free the +victims under `LOCK_LOCKERS`. This preserves master's existing order +(partition released before lockers) and keeps GC partition-local. + +## GC algorithm (5.3.x form) + +`__txn_oldest_reader` is computed **once up front**, before taking any +partition mutex (it takes `TXN_SYSTEM_LOCK`, so taking it under a partition +mutex would itself be a new ordering). Then: + +``` +__lock_sicleanup(env): + if (__txn_oldest_reader(env, &old_lsn)) return ret; # no partition held + for ndx in 0 .. object_t_size-1: + OBJECT_LOCK_NDX(lt, region, ndx) + __lock_siclean_obj(env, &obj_tab[ndx]'s objects, &old_lsn, &victims) + OBJECT_UNLOCK(lt, region, ndx) + free victims under LOCK_LOCKERS +``` + +`__lock_siclean_obj` keeps a SIREAD lock if its owner is still `TXN_RUNNING`, +or if its read/commit LSNs are still newer than `old_lsn` (the live-snapshot +window); otherwise it unlinks the lock from `obj->sireaders` via +`__lock_put_internal(... DB_LOCK_FREE | DB_LOCK_DOALL)` and adjusts counts. + +## Counting / stats + +SIREAD locks occupy real `struct __db_lock`s, so they must be reflected in +`part_array[part].part_stat.st_nlocks` on alloc/free exactly like normal locks +(the prototype's single-region counter becomes the per-partition counter). + +## Acquisition path (gated) + +`DB_LOCK_SNAPSHOT_SAFE` is read into a local `safe_si` and cleared at the top of +`__lock_get_internal`. Only when `safe_si` is set do we (1) place SIREAD locks on +`obj->sireaders` and (2), on a `DB_LOCK_WRITE` acquire, scan `obj->sireaders` to +record rw-antidependencies (M3/M4). With `safe_si` unset the path is byte-for-byte +the existing behavior — SSI is strictly opt-in. + +## Open items to confirm during coding + +- Whether any `__lock_put_internal` path needs a `DB_LOCK_FREE` variant that + skips holder/waiter promotion for SIREAD locks (prototype used `DB_LOCK_FREE`). +- Recovery/`__lock_getlocker` must initialize `td_off` for every locker that can + take SIREAD locks (M2 part 2 wires this in `__lock_getlocker`). diff --git a/docs/ssi/M4-commit-lifecycle.md b/docs/ssi/M4-commit-lifecycle.md new file mode 100644 index 000000000..5e5840b36 --- /dev/null +++ b/docs/ssi/M4-commit-lifecycle.md @@ -0,0 +1,53 @@ +# M4 design note — faithful (A) SIREAD lifecycle across commit + +Chosen approach (A): SIREAD markers persist past the reading transaction's +commit, so conflicts against an already-committed reader are still detected +(full Cahill SSI). This requires the reader's `TXN_DETAIL` and `DB_LOCKER` to +outlive its commit, because each SIREAD marker dereferences them through +`LOCK_OWNER` (status / read_lsn / visible_lsn). + +## The reference-counting model + +Master already extends `TXN_DETAIL` lifetime past commit for MVCC: a committed +snapshot txn with `mvcc_ref > 0` is parked on `region->mvcc_txn` (flag +`TXN_DTL_SNAPSHOT`) and freed by the mpool when its last page is evicted. + +We add a **parallel reference**: `TXN_DETAIL.si_ref` counts outstanding SIREAD +markers naming this detail. The detail is freed only when **both** `mvcc_ref` +and `si_ref` reach zero. Symmetrically, the `DB_LOCKER` is kept (flag +`DB_LOCKER_FREED`) until its last SIREAD marker is gone. + +## Lifecycle + +1. **Acquire (reader):** granting a SIREAD marker increments the owner's + `td->si_ref` (the marker names the reader's own detail). +2. **Commit (`__lock_sicommit`, before `PUT_ALL`):** detach the txn's SIREAD + markers from the locker `heldby` list so normal release does not free them; + they stay on each object's `sireaders` list. If any remain, mark the locker + `DB_LOCKER_FREED`. +3. **`__txn_end`:** if `si_ref > 0`, retain `td` (do not `__env_alloc_free` it); + it is already on no active list, so it simply stays allocated, pinned by the + markers. (`mvcc_ref` retention is unchanged and composes with this.) +4. **`__lock_freelocker_int`:** if `DB_LOCKER_FREED` and the locker still has + SIREAD markers, defer — return without freeing. +5. **GC (`__lock_sicleanup` / `__lock_siclean_obj`):** for a committed reader + whose snapshot is older than `__txn_oldest_reader`, remove the marker from + `sireaders`, free the lock struct (no `UNLINK` — it is already off `heldby`), + `td->si_ref--`, and locker `nlocks--`. When a locker's markers reach zero and + it is `DB_LOCKER_FREED`, free it (deferred to after the partition mutex is + released). When `td->si_ref == 0 && mvcc_ref == 0`, free the detail via a + txn-region helper (cross-subsystem free, mirroring how mpool frees MVCC + details today). + +## Lock ordering (unchanged from M2 note) + +`__txn_oldest_reader` (takes the txn system lock) is computed before any +partition mutex. Lockers and details are freed after the partition mutex is +released. No partition→txn-system or partition→mtx_lockers nesting is +introduced. + +## Why this is the highest-risk area for M5 + +Two subsystems (lock GC and mpool eviction) can each hold the last reference to +a `TXN_DETAIL`. The M5 campaign must stress concurrent commit + eviction + +SIREAD GC to prove there is no use-after-free or double-free of details/lockers. diff --git a/lang/tcl/tcl_txn.c b/lang/tcl/tcl_txn.c index 564babde6..258a12b72 100644 --- a/lang/tcl/tcl_txn.c +++ b/lang/tcl/tcl_txn.c @@ -149,6 +149,7 @@ tcl_Txn(interp, objc, objv, dbenv, envip) "-nowait", "-parent", "-snapshot", + "-snapshot_safe", "-sync", "-wrnosync", NULL @@ -167,6 +168,7 @@ tcl_Txn(interp, objc, objv, dbenv, envip) TXNNOWAIT, TXNPARENT, TXNSNAPSHOT, + TXNSNAPSHOTSAFE, TXNSYNC, TXNWRNOSYNC }; @@ -262,6 +264,9 @@ get_timeout: if (i >= objc) { case TXNSNAPSHOT: flag |= DB_TXN_SNAPSHOT; break; + case TXNSNAPSHOTSAFE: + flag |= DB_TXN_SNAPSHOT_SAFE; + break; case TXNSYNC: flag |= DB_TXN_SYNC; break; diff --git a/src/common/db_err.c b/src/common/db_err.c index 9025bf967..e21fa1c09 100644 --- a/src/common/db_err.c +++ b/src/common/db_err.c @@ -356,6 +356,12 @@ db_strerror(error) case DB_SECONDARY_BAD: return (DB_STR("0088", "DB_SECONDARY_BAD: Secondary index inconsistent with primary")); + case DB_SNAPSHOT_CONFLICT: + return (DB_STR("4573", + "DB_SNAPSHOT_CONFLICT: Serializable snapshot update conflict")); + case DB_SNAPSHOT_UNSAFE: + return (DB_STR("4574", + "DB_SNAPSHOT_UNSAFE: Potential serializable snapshot anomaly")); case DB_TIMEOUT: return (DB_STR("0089", "DB_TIMEOUT: Operation timed out")); case DB_VERIFY_BAD: diff --git a/src/db/db_meta.c b/src/db/db_meta.c index e47a38e1d..f89c10665 100644 --- a/src/db/db_meta.c +++ b/src/db/db_meta.c @@ -1162,8 +1162,6 @@ __db_lget(dbc, action, pgno, mode, lkflags, lockp) * calling __db_lget to acquire the lock. */ if (CDB_LOCKING(env) || !LOCKING_ON(env) || - (MULTIVERSION(dbp) && mode == DB_LOCK_READ && - dbc->txn != NULL && F_ISSET(dbc->txn, TXN_SNAPSHOT)) || F_ISSET(dbc, DBC_DONTLOCK) || (F_ISSET(dbc, DBC_RECOVER) && (action != LCK_ROLLBACK || IS_REP_CLIENT(env))) || (action != LCK_ALWAYS && F_ISSET(dbc, DBC_OPD))) { @@ -1171,6 +1169,24 @@ __db_lget(dbc, action, pgno, mode, lkflags, lockp) return (0); } + /* + * SSI: under multiversion (snapshot) isolation a plain snapshot read + * takes no lock (unchanged behavior). A snapshot-safe (SSI) read + * instead acquires a SIREAD marker, and SSI writes are flagged so the + * lock manager can track read/write antidependencies. + */ + if (MULTIVERSION(dbp) && mode == DB_LOCK_READ && + txn != NULL && F_ISSET(txn, TXN_SNAPSHOT)) { + if (!F_ISSET(txn, TXN_SNAPSHOT_SAFE)) { + LOCK_INIT(*lockp); + return (0); + } + lkflags |= DB_LOCK_SNAPSHOT_SAFE; + mode = DB_LOCK_SIREAD; + } else if (MULTIVERSION(dbp) && txn != NULL && + F_ISSET(txn, TXN_SNAPSHOT_SAFE)) + lkflags |= DB_LOCK_SNAPSHOT_SAFE; + /* * If the transaction enclosing this cursor has DB_LOCK_NOWAIT set, * pass that along to the lock call. diff --git a/src/dbinc/db.in b/src/dbinc/db.in index 92ac822a9..6ec48dbe7 100644 --- a/src/dbinc/db.in +++ b/src/dbinc/db.in @@ -316,7 +316,8 @@ typedef enum { DB_LOCK_IREAD=5, /* Intent to share/read. */ DB_LOCK_IWR=6, /* Intent to read and write. */ DB_LOCK_READ_UNCOMMITTED=7, /* Degree 1 isolation. */ - DB_LOCK_WWRITE=8 /* Was Written. */ + DB_LOCK_WWRITE=8, /* Was Written. */ + DB_LOCK_SIREAD=9 /* Snapshot isolation read (SSI). */ } db_lockmode_t; /* @@ -957,6 +958,7 @@ struct __db_txn { #define TXN_SYNC 0x10000 /* Write and sync on prepare/commit. */ #define TXN_WRITE_NOSYNC 0x20000 /* Write only on prepare/commit. */ #define TXN_BULK 0x40000 /* Enable bulk loading optimization. */ +#define TXN_SNAPSHOT_SAFE 0x80000 /* Serializable snapshot isolation (SSI). */ u_int32_t flags; }; @@ -1398,6 +1400,8 @@ typedef enum { #define DB_TIMEOUT (-30971)/* Timed out on read consistency. */ #define DB_VERIFY_BAD (-30970)/* Verify failed; bad format. */ #define DB_VERSION_MISMATCH (-30969)/* Environment version mismatch. */ +#define DB_SNAPSHOT_CONFLICT (-30968)/* SSI: conflicting snapshot update. */ +#define DB_SNAPSHOT_UNSAFE (-30967)/* SSI: potential snapshot anomaly. */ /* DB (private) error return codes. */ #define DB_ALREADY_ABORTED (-30899) diff --git a/src/dbinc/lock.h b/src/dbinc/lock.h index c3186f75d..d6133e800 100644 --- a/src/dbinc/lock.h +++ b/src/dbinc/lock.h @@ -119,6 +119,7 @@ typedef struct __db_lockobj { /* SHARED */ SH_TAILQ_ENTRY dd_links; /* Links for dd list. */ SH_TAILQ_HEAD(__waitl) waiters; /* List of waiting locks. */ SH_TAILQ_HEAD(__holdl) holders; /* List of held locks. */ + SH_TAILQ_HEAD(__sil) sireaders; /* List of SSI snapshot readers. */ /* Declare room in the object to hold * typical DB lock structures so that * we do not have to allocate them from @@ -136,6 +137,8 @@ struct __db_locker { /* SHARED */ db_threadid_t tid; /* Thread owning locker ID */ db_mutex_t mtx_locker; /* Mutex to block on. */ + roff_t td_off; /* SSI: TXN_DETAIL offset of locker. */ + u_int32_t dd_id; /* Deadlock detector id. */ u_int32_t nlocks; /* Number of locks held. */ @@ -163,6 +166,7 @@ struct __db_locker { /* SHARED */ #define DB_LOCKER_TIMEOUT 0x0004 /* Has timeout set. */ #define DB_LOCKER_FAMILY_LOCKER 0x0008 /* Part of a family of lockers. */ #define DB_LOCKER_HANDLE_LOCKER 0x0010 /* Not associated with a thread. */ +#define DB_LOCKER_FREED 0x0020 /* SSI: freed, kept for SIREAD locks. */ u_int32_t flags; }; @@ -235,6 +239,25 @@ struct __db_lock { /* SHARED */ db_status_t status; /* Status of this lock. */ }; +/* + * Serializable Snapshot Isolation (SSI) helpers. + * + * A locker is linked to its transaction detail via td_off so the lock layer + * can read MVCC snapshot LSNs (read_lsn / visible_lsn) when reasoning about + * rw-antidependencies recorded by SIREAD locks. + */ +#define LOCKER_TD(env, lockerp) \ + ((TXN_DETAIL *)R_ADDR(&(env)->tx_handle->reginfo, (lockerp)->td_off)) + +#define LOCK_HOLDER(env, lp) \ + ((DB_LOCKER *)R_ADDR(&(env)->lk_handle->reginfo, (lp)->holder)) + +#define LOCK_OWNER(env, lp) LOCKER_TD(env, LOCK_HOLDER(env, lp)) + +#define LOCK_READLSN(env, lp) (LOCK_OWNER(env, lp)->read_lsn) + +#define LOCK_COMMITLSN(env, lp) (LOCK_OWNER(env, lp)->visible_lsn) + /* * Flag values for __lock_put_internal: * DB_LOCK_DOALL: Unlock all references in this lock (instead of only 1). @@ -246,6 +269,7 @@ struct __db_lock { /* SHARED */ * we pass some of those around. */ #define DB_LOCK_DOALL 0x010000 +#define DB_LOCK_SNAPSHOT_SAFE 0x020000 /* SSI: snapshot-safe acquire. */ #define DB_LOCK_FREE 0x040000 #define DB_LOCK_NOPROMOTE 0x080000 #define DB_LOCK_UNLINK 0x100000 diff --git a/src/dbinc/txn.h b/src/dbinc/txn.h index 9b047277a..4ca626f4e 100644 --- a/src/dbinc/txn.h +++ b/src/dbinc/txn.h @@ -93,6 +93,9 @@ typedef struct __txn_detail { db_mutex_t mvcc_mtx; /* Version mutex. */ u_int32_t mvcc_ref; /* Number of buffers created by this transaction still in cache. */ + u_int32_t si_ref; /* SSI: outstanding SIREAD markers that + reference this detail (keeps it alive + past commit, parallels mvcc_ref). */ u_int32_t priority; /* Deadlock resolution priority. */ @@ -107,6 +110,8 @@ typedef struct __txn_detail { #define TXN_DTL_INMEMORY 0x04 /* uses in memory logs */ #define TXN_DTL_SNAPSHOT 0x08 /* On the list of snapshot txns. */ #define TXN_DTL_NOWAIT 0x10 /* Don't block on locks. */ +#define TXN_DTL_WCONF 0x20 /* SSI: write end of an rw-conflict. */ +#define TXN_DTL_RCONF 0x40 /* SSI: read end of an rw-conflict. */ u_int32_t flags; SH_TAILQ_ENTRY links; /* active/free/snapshot list */ diff --git a/src/dbinc_auto/api_flags.in b/src/dbinc_auto/api_flags.in index 9727ede2c..43b9185c6 100644 --- a/src/dbinc_auto/api_flags.in +++ b/src/dbinc_auto/api_flags.in @@ -195,8 +195,9 @@ #define DB_TXN_NOT_DURABLE 0x00000004 #define DB_TXN_NOWAIT 0x00000002 #define DB_TXN_SNAPSHOT 0x00000004 +#define DB_TXN_SNAPSHOT_SAFE 0x00000080 #define DB_TXN_SYNC 0x00000008 -#define DB_TXN_WAIT 0x00000080 +#define DB_TXN_WAIT 0x00000100 #define DB_TXN_WRITE_NOSYNC 0x00000020 #define DB_UNREF 0x00020000 #define DB_UPGRADE 0x00000001 diff --git a/src/dbinc_auto/lock_ext.h b/src/dbinc_auto/lock_ext.h index d5981e180..31a499f36 100644 --- a/src/dbinc_auto/lock_ext.h +++ b/src/dbinc_auto/lock_ext.h @@ -71,6 +71,8 @@ int __lock_inherit_timeout __P((ENV *, DB_LOCKER *, DB_LOCKER *)); u_int32_t __lock_ohash __P((const DBT *)); u_int32_t __lock_lhash __P((DB_LOCKOBJ *)); int __lock_nomem __P((ENV *, const char *)); +int __lock_sicleanup __P((ENV *)); +int __lock_sicommit __P((ENV *, DB_LOCKER *, int)); #if defined(__cplusplus) } diff --git a/src/lock/lock.c b/src/lock/lock.c index e2a2eddbd..73400d92d 100644 --- a/src/lock/lock.c +++ b/src/lock/lock.c @@ -11,6 +11,7 @@ #include "db_int.h" #include "dbinc/lock.h" #include "dbinc/log.h" +#include "dbinc/txn.h" static int __lock_allocobj __P((DB_LOCKTAB *, u_int32_t)); static int __lock_alloclock __P((DB_LOCKTAB *, u_int32_t)); @@ -30,6 +31,7 @@ static int __lock_remove_waiter __P((DB_LOCKTAB *, static int __lock_trade __P((ENV *, DB_LOCK *, DB_LOCKER *)); static int __lock_vec_api __P((ENV *, u_int32_t, u_int32_t, DB_LOCKREQ *, int, DB_LOCKREQ **)); +static int __lock_siclean_obj __P((ENV *, DB_LOCKOBJ *, DB_LSN *)); static const char __db_lock_invalid[] = "%s: Lock is no longer valid"; static const char __db_locker_invalid[] = "Locker is not valid"; @@ -89,6 +91,172 @@ __lock_vec_api(env, lid, flags, list, nlist, elistp) return (ret); } +/* + * __lock_siclean_obj -- + * Garbage-collect SSI snapshot-read (SIREAD) locks on one object whose + * owning transactions have committed and whose snapshots are no longer + * visible to any active reader (old_lsnp == oldest active read LSN). + * The caller must hold the object's partition mutex. + */ +static int +__lock_siclean_obj(env, obj, old_lsnp) + ENV *env; + DB_LOCKOBJ *obj; + DB_LSN *old_lsnp; +{ + DB_LOCKTAB *lt; + DB_LOCKER *sh_locker; + struct __db_lock *lp, *next_lock; + int ret; + + lt = env->lk_handle; + ret = 0; + + for (lp = SH_TAILQ_FIRST(&obj->sireaders, __db_lock); + lp != NULL; lp = next_lock) { + next_lock = SH_TAILQ_NEXT(lp, links, __db_lock); + + /* Keep readers whose transaction is still running. */ + if (LOCK_OWNER(env, lp)->status == TXN_RUNNING) + continue; + + /* + * Keep the marker while its snapshot is still within the + * window of the oldest active reader. + */ + if (!(IS_MAX_LSN(LOCK_COMMITLSN(env, lp)) && + LOG_COMPARE(&LOCK_READLSN(env, lp), old_lsnp) > 0) && + LOG_COMPARE(&LOCK_COMMITLSN(env, lp), old_lsnp) > 0) + continue; + + SH_TAILQ_REMOVE(&obj->sireaders, lp, links, __db_lock); + sh_locker = LOCK_HOLDER(env, lp); + /* + * Committed readers' markers were already detached from the + * locker's heldby list by __lock_sicommit, so free WITHOUT + * DB_LOCK_UNLINK and account for the marker by hand. The owner + * detail and the (DB_LOCKER_FREED) locker are reclaimed by + * __lock_sicleanup once their last marker is gone. + */ + if (sh_locker->td_off != INVALID_ROFF) + LOCKER_TD(env, sh_locker)->si_ref--; + if (sh_locker->nlocks > 0) + sh_locker->nlocks--; + if ((ret = __lock_freelock(lt, lp, sh_locker, + DB_LOCK_FREE)) != 0) + break; + } + + return (ret); +} + +/* + * __lock_sicleanup -- + * Sweep all objects and garbage-collect SIREAD locks that are no longer + * needed for serializable-snapshot conflict detection. The oldest active + * read LSN is computed once up front (before any partition mutex is held, + * since __txn_oldest_reader takes the txn system lock). + * + * PUBLIC: int __lock_sicleanup __P((ENV *)); + */ +int +__lock_sicleanup(env) + ENV *env; +{ + DB_LOCKTAB *lt; + DB_LOCKREGION *region; + DB_LOCKOBJ *obj, *next_obj; + DB_LSN old_lsn; + u_int32_t i; + int ret; + + if (!LOCKING_ON(env)) + return (0); + + lt = env->lk_handle; + region = lt->reginfo.primary; + + if ((ret = __txn_oldest_reader(env, &old_lsn)) != 0) + return (ret); + + for (i = 0; i < region->object_t_size; i++) { + OBJECT_LOCK_NDX(lt, region, i); + for (obj = SH_TAILQ_FIRST(<->obj_tab[i], __db_lockobj); + obj != NULL; obj = next_obj) { + next_obj = SH_TAILQ_NEXT(obj, links, __db_lockobj); + if ((ret = + __lock_siclean_obj(env, obj, &old_lsn)) != 0) { + OBJECT_UNLOCK(lt, region, i); + return (ret); + } + } + OBJECT_UNLOCK(lt, region, i); + } + + return (0); +} + +/* + * __lock_sicommit -- + * Handle a transaction's SIREAD markers at txn end. On commit the markers + * persist on their objects' sireaders lists (detached from the locker so + * normal lock release leaves them in place); the locker is flagged + * DB_LOCKER_FREED so it survives until GC reclaims the last marker. On + * abort the markers are released immediately (an aborted reader leaves no + * footprint). + * + * PUBLIC: int __lock_sicommit __P((ENV *, DB_LOCKER *, int)); + */ +int +__lock_sicommit(env, sh_locker, is_commit) + ENV *env; + DB_LOCKER *sh_locker; + int is_commit; +{ + DB_LOCKTAB *lt; + DB_LOCKREGION *region; + DB_LOCKOBJ *obj; + struct __db_lock *lp, *next_lock; + int detached, ret; + + if (!LOCKING_ON(env) || sh_locker == NULL) + return (0); + + lt = env->lk_handle; + region = lt->reginfo.primary; + detached = ret = 0; + + LOCK_SYSTEM_LOCK(lt, region); + for (lp = SH_LIST_FIRST(&sh_locker->heldby, __db_lock); + lp != NULL; lp = next_lock) { + next_lock = SH_LIST_NEXT(lp, locker_links, __db_lock); + if (lp->mode != DB_LOCK_SIREAD) + continue; + /* Detach from the locker so normal release leaves it alone. */ + SH_LIST_REMOVE(lp, locker_links, __db_lock); + if (is_commit) { + detached++; + continue; + } + /* Abort: drop the marker entirely. */ + obj = (DB_LOCKOBJ *)SH_OFF_TO_PTR(lp, lp->obj, DB_LOCKOBJ); + OBJECT_LOCK_NDX(lt, region, obj->indx); + SH_TAILQ_REMOVE(&obj->sireaders, lp, links, __db_lock); + if (sh_locker->td_off != INVALID_ROFF) + LOCKER_TD(env, sh_locker)->si_ref--; + sh_locker->nlocks--; + ret = __lock_freelock(lt, lp, sh_locker, DB_LOCK_FREE); + OBJECT_UNLOCK(lt, region, obj->indx); + if (ret != 0) + break; + } + if (is_commit && detached > 0) + F_SET(sh_locker, DB_LOCKER_FREED); + LOCK_SYSTEM_UNLOCK(lt, region); + + return (ret); +} + /* * __lock_vec -- * ENV->lock_vec. @@ -513,13 +681,13 @@ __lock_get_internal(lt, sh_locker, flags, obj, lock_mode, timeout, lock) db_timeout_t timeout; DB_LOCK *lock; { - struct __db_lock *newl, *lp; + struct __db_lock *newl, *lp, *sireadlp, *next_lock; ENV *env; DB_LOCKOBJ *sh_obj; DB_LOCKREGION *region; DB_THREAD_INFO *ip; u_int32_t ndx, part_id; - int did_abort, ihold, grant_dirty, no_dd, ret, t_ret; + int did_abort, ihold, grant_dirty, no_dd, ret, rwconf, safe_si, t_ret; roff_t holder, sh_off; /* @@ -552,6 +720,12 @@ __lock_get_internal(lt, sh_locker, flags, obj, lock_mode, timeout, lock) newl = NULL; sh_obj = NULL; + /* SSI: snapshot-safe bookkeeping is strictly opt-in. */ + safe_si = LF_ISSET(DB_LOCK_SNAPSHOT_SAFE) ? 1 : 0; + LF_CLR(DB_LOCK_SNAPSHOT_SAFE); + if (safe_si) + DB_ASSERT(env, sh_locker->td_off != INVALID_ROFF); + /* Check that the lock mode is valid. */ if (lock_mode >= (db_lockmode_t)region->nmodes) { __db_errx(env, DB_STR_A("2037", @@ -625,6 +799,7 @@ again: if (obj == NULL) { */ ihold = 0; grant_dirty = 0; + rwconf = 0; holder = 0; /* @@ -667,6 +842,15 @@ again: if (obj == NULL) { lock->gen = lp->gen; lock->mode = lp->mode; goto done; + } else if (safe_si && lp->mode == DB_LOCK_WRITE && + lock_mode == DB_LOCK_SIREAD) { + /* + * SSI: this locker already holds WRITE on the + * object, so a snapshot-read marker is + * redundant -- grant trivially. + */ + LOCK_INIT(*lock); + goto done; } else { ihold = 1; } @@ -682,6 +866,79 @@ again: if (obj == NULL) { } } + /* + * SSI: when a snapshot-safe writer (or reader) acquires a lock and the + * object has snapshot readers, record rw-antidependencies. A reader + * R that read a version now being written by W creates an edge + * R --rw--> W. A transaction that is both the read end and the write + * end of such edges (a "pivot") may produce a serializability anomaly + * and is aborted with DB_SNAPSHOT_UNSAFE. + */ + if (safe_si && lp == NULL && + (lock_mode == DB_LOCK_WRITE || lock_mode == DB_LOCK_SIREAD)) { + for (sireadlp = SH_TAILQ_FIRST(&sh_obj->sireaders, __db_lock); + sireadlp != NULL; sireadlp = next_lock) { + next_lock = SH_TAILQ_NEXT(sireadlp, links, __db_lock); + if (lock_mode == DB_LOCK_WRITE && + sh_off == sireadlp->holder) { + /* + * Upgrading our own SIREAD to WRITE: drop the + * SIREAD marker to avoid self-conflicts. + */ + SH_TAILQ_REMOVE(&sh_obj->sireaders, + sireadlp, links, __db_lock); + if ((ret = __lock_freelock(lt, sireadlp, + LOCK_HOLDER(env, sireadlp), + DB_LOCK_UNLINK | DB_LOCK_FREE)) != 0) + goto err; + } else if (lock_mode == DB_LOCK_WRITE && + sh_off != sireadlp->holder && + (LOCK_OWNER(env, sireadlp)->status == TXN_RUNNING || + LOG_COMPARE(&LOCK_COMMITLSN(env, sireadlp), + &LOCKER_TD(env, sh_locker)->read_lsn) > 0)) { + if (F_ISSET(LOCK_OWNER(env, sireadlp), + TXN_DTL_WCONF) && + LOCK_OWNER(env, sireadlp)->status == + TXN_COMMITTED) { + ret = DB_SNAPSHOT_UNSAFE; + goto err; + } + rwconf = 1; + /* + * Set the incoming-conflict flag on our txn, + * unless the reader will itself abort. + */ + if (LOCK_OWNER(env, sireadlp)->status == + TXN_COMMITTED || + !F_ISSET(LOCK_OWNER(env, sireadlp), + TXN_DTL_WCONF)) { + if (F_ISSET(LOCKER_TD(env, sh_locker), + TXN_DTL_RCONF)) { + ret = DB_SNAPSHOT_UNSAFE; + goto err; + } + F_SET(LOCKER_TD(env, sh_locker), + TXN_DTL_WCONF); + } + F_SET(LOCK_OWNER(env, sireadlp), TXN_DTL_RCONF); + } else if (lock_mode == DB_LOCK_SIREAD && + sh_off == sireadlp->holder) { + /* Already hold a SIREAD marker here. */ + sireadlp->refcount++; + lock->off = R_OFFSET(<->reginfo, sireadlp); + lock->gen = sireadlp->gen; + lock->mode = sireadlp->mode; + goto done; + } + } + /* + * Note: obsolete SIREAD markers are reclaimed by + * __lock_sicleanup (run without a partition mutex held), not + * here -- computing the oldest reader needs the txn system + * lock, which must not be taken under a partition mutex. + */ + } + #ifdef DIAGNOSTIC if (LF_ISSET(DB_LOCK_CHECK)) { ret = ENOENT; @@ -860,7 +1117,11 @@ upgrade: lp = R_ADDR(<->reginfo, lock->off); switch (action) { case GRANT: newl->status = DB_LSTAT_HELD; - SH_TAILQ_INSERT_TAIL(&sh_obj->holders, newl, links); + if (safe_si && lock_mode == DB_LOCK_SIREAD) { + SH_TAILQ_INSERT_TAIL(&sh_obj->sireaders, newl, links); + LOCKER_TD(env, sh_locker)->si_ref++; + } else + SH_TAILQ_INSERT_TAIL(&sh_obj->holders, newl, links); break; case UPGRADE: DB_ASSERT(env, lock_mode == DB_LOCK_WAIT); @@ -1552,6 +1813,7 @@ retry: SH_TAILQ_FOREACH(sh_obj, <->obj_tab[ndx], links, __db_lockobj) { sh_obj->indx = ndx; SH_TAILQ_INIT(&sh_obj->waiters); SH_TAILQ_INIT(&sh_obj->holders); + SH_TAILQ_INIT(&sh_obj->sireaders); /* SSI snapshot readers. */ sh_obj->lockobj.size = obj->size; sh_obj->lockobj.off = (roff_t)SH_PTR_TO_OFF(&sh_obj->lockobj, p); diff --git a/src/lock/lock_id.c b/src/lock/lock_id.c index f97c8a437..89357708c 100644 --- a/src/lock/lock_id.c +++ b/src/lock/lock_id.c @@ -370,6 +370,7 @@ __lock_getlocker_int(lt, locker, create, retp) env->dbenv, &sh_locker->pid, &sh_locker->tid); sh_locker->mtx_locker = mutex; sh_locker->dd_id = 0; + sh_locker->td_off = INVALID_ROFF; /* SSI: set by txn layer. */ sh_locker->master_locker = INVALID_ROFF; sh_locker->parent_locker = INVALID_ROFF; SH_LIST_INIT(&sh_locker->child_locker); @@ -489,6 +490,15 @@ __lock_freelocker_int(lt, region, sh_locker, reallyfree) return (EINVAL); } + /* + * SSI: a committed snapshot-safe reader is kept alive (DB_LOCKER_FREED) + * while its persisted SIREAD markers still reference it; defer the real + * free to __lock_sicleanup, which reclaims the markers and then the + * locker once the oldest reader has advanced past its snapshot. + */ + if (F_ISSET(sh_locker, DB_LOCKER_FREED) && sh_locker->nlocks != 0) + return (0); + /* If this is part of a family, we must fix up its links. */ if (sh_locker->master_locker != INVALID_ROFF) { SH_LIST_REMOVE(sh_locker, child_link, __db_locker); diff --git a/src/lock/lock_region.c b/src/lock/lock_region.c index 9a35d6e3e..bd5a7a170 100644 --- a/src/lock/lock_region.c +++ b/src/lock/lock_region.c @@ -17,18 +17,19 @@ static int __lock_region_init __P((ENV *, DB_LOCKTAB *)); * The conflict arrays are set up such that the row is the lock you are * holding and the column is the lock that is desired. */ -#define DB_LOCK_RIW_N 9 +#define DB_LOCK_RIW_N 10 static const u_int8_t db_riw_conflicts[] = { -/* N R W WT IW IR RIW DR WW */ -/* N */ 0, 0, 0, 0, 0, 0, 0, 0, 0, -/* R */ 0, 0, 1, 0, 1, 0, 1, 0, 1, -/* W */ 0, 1, 1, 1, 1, 1, 1, 1, 1, -/* WT */ 0, 0, 0, 0, 0, 0, 0, 0, 0, -/* IW */ 0, 1, 1, 0, 0, 0, 0, 1, 1, -/* IR */ 0, 0, 1, 0, 0, 0, 0, 0, 1, -/* RIW */ 0, 1, 1, 0, 0, 0, 0, 1, 1, -/* DR */ 0, 0, 1, 0, 1, 0, 1, 0, 0, -/* WW */ 0, 1, 1, 0, 1, 1, 1, 0, 1 +/* N R W WT IW IR RIW DR WW SI */ +/* N */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +/* R */ 0, 0, 1, 0, 1, 0, 1, 0, 1, 0, +/* W */ 0, 1, 1, 1, 1, 1, 1, 1, 1, 0, +/* WT */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, +/* IW */ 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, +/* IR */ 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, +/* RIW */ 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, +/* DR */ 0, 0, 1, 0, 1, 0, 1, 0, 0, 0, +/* WW */ 0, 1, 1, 0, 1, 1, 1, 0, 1, 0, +/* SI */ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; /* diff --git a/src/txn/txn.c b/src/txn/txn.c index d709d35a4..e95ffab3b 100644 --- a/src/txn/txn.c +++ b/src/txn/txn.c @@ -108,6 +108,7 @@ __txn_begin_pp(dbenv, parent, txnpp, flags) DB_IGNORE_LEASE |DB_READ_COMMITTED | DB_READ_UNCOMMITTED | DB_TXN_FAMILY | DB_TXN_NOSYNC | DB_TXN_SNAPSHOT | DB_TXN_SYNC | DB_TXN_WAIT | DB_TXN_WRITE_NOSYNC | DB_TXN_NOWAIT | + DB_TXN_SNAPSHOT_SAFE | DB_TXN_BULK)) != 0) return (ret); if ((ret = __db_fcchk(env, "txn_begin", flags, @@ -226,7 +227,8 @@ __txn_begin(env, ip, parent, txnpp, flags) F_SET(txn, TXN_READ_UNCOMMITTED); if (LF_ISSET(DB_TXN_FAMILY)) F_SET(txn, TXN_FAMILY | TXN_INFAMILY | TXN_READONLY); - if (LF_ISSET(DB_TXN_SNAPSHOT) || F_ISSET(dbenv, DB_ENV_TXN_SNAPSHOT) || + if (LF_ISSET(DB_TXN_SNAPSHOT | DB_TXN_SNAPSHOT_SAFE) || + F_ISSET(dbenv, DB_ENV_TXN_SNAPSHOT) || (parent != NULL && F_ISSET(parent, TXN_SNAPSHOT))) { if (IS_REP_CLIENT(env)) { __db_errx(env, DB_STR("4572", @@ -235,6 +237,10 @@ __txn_begin(env, ip, parent, txnpp, flags) } else F_SET(txn, TXN_SNAPSHOT); } + /* SSI is snapshot isolation plus serializable conflict detection. */ + if (LF_ISSET(DB_TXN_SNAPSHOT_SAFE) || + (parent != NULL && F_ISSET(parent, TXN_SNAPSHOT_SAFE))) + F_SET(txn, TXN_SNAPSHOT_SAFE); if (LF_ISSET(DB_IGNORE_LEASE)) F_SET(txn, TXN_IGNORE_LEASE); @@ -445,12 +451,17 @@ __txn_begin_int(txn) txn->txnid = id; txn->td = td; + td->si_ref = 0; /* SSI: no SIREAD markers reference it yet. */ /* Allocate a locker for this txn. */ if (LOCKING_ON(env) && (ret = __lock_getlocker(env->lk_handle, id, 1, &txn->locker)) != 0) goto err; + /* SSI: link the locker to its transaction detail. */ + if (txn->locker != NULL) + txn->locker->td_off = R_OFFSET(&mgr->reginfo, td); + txn->abort = __txn_abort_pp; txn->commit = __txn_commit_pp; txn->discard = __txn_discard; @@ -679,6 +690,18 @@ __txn_commit(txn, flags) goto err; } + /* + * SSI: a snapshot-safe transaction that became the pivot of a dangerous + * structure (both the read end and the write end of rw-conflicts, i.e. + * has both TXN_DTL_RCONF and TXN_DTL_WCONF) cannot commit -- doing so + * could produce a non-serializable schedule. + */ + if (F_ISSET(txn, TXN_SNAPSHOT_SAFE) && + F_ISSET(td, TXN_DTL_WCONF) && F_ISSET(td, TXN_DTL_RCONF)) { + ret = DB_SNAPSHOT_CONFLICT; + goto err; + } + /* Close registered cursors before committing. */ if ((ret = __txn_close_cursors(txn)) != 0) goto err; @@ -1649,6 +1672,13 @@ __txn_end(txn, is_commit) (ret = __lock_getlocker(env->lk_handle, txn->txnid, 1, &txn->locker)) != 0) return (__env_panic(env, ret)); + /* + * SSI: handle this txn's SIREAD markers before normal lock + * release -- persist them on commit, drop them on abort. + */ + if (F_ISSET(txn, TXN_SNAPSHOT_SAFE) && (ret = + __lock_sicommit(env, txn->locker, is_commit)) != 0) + return (__env_panic(env, ret)); request.op = txn->parent == NULL || is_commit == 0 ? DB_LOCK_PUT_ALL : DB_LOCK_INHERIT; request.obj = NULL; @@ -1708,7 +1738,7 @@ __txn_end(txn, is_commit) return (__env_panic(env, ret)); } - if (td != NULL) + if (td != NULL && td->si_ref == 0) __env_alloc_free(&mgr->reginfo, td); #ifdef HAVE_STATISTICS diff --git a/src/txn/txn_chkpt.c b/src/txn/txn_chkpt.c index 0834a0c43..b3826cdfe 100644 --- a/src/txn/txn_chkpt.c +++ b/src/txn/txn_chkpt.c @@ -40,6 +40,7 @@ #include "db_config.h" #include "db_int.h" +#include "dbinc/lock.h" #include "dbinc/log.h" #include "dbinc/mp.h" #include "dbinc/txn.h" @@ -137,6 +138,14 @@ __txn_checkpoint(env, kbytes, minutes, flags) */ id = renv->envid; + /* + * SSI: a checkpoint is a convenient, infrequent point at which to + * reclaim SIREAD markers left by committed snapshot-safe readers whose + * snapshots are no longer visible to any active reader. Best effort. + */ + if (LOCKING_ON(env)) + (void)__lock_sicleanup(env); + MUTEX_LOCK(env, region->mtx_ckp); /* * The checkpoint LSN is an LSN such that all transactions begun before diff --git a/src/txn/txn_region.c b/src/txn/txn_region.c index f418a2b91..0107527d8 100644 --- a/src/txn/txn_region.c +++ b/src/txn/txn_region.c @@ -495,7 +495,8 @@ __txn_remove_buffer(env, td, hash_mtx) * reference and td is on the list of committed snapshot transactions * with active pages. */ - need_free = (--td->mvcc_ref == 0) && F_ISSET(td, TXN_DTL_SNAPSHOT); + need_free = (--td->mvcc_ref == 0) && F_ISSET(td, TXN_DTL_SNAPSHOT) && + td->si_ref == 0; MUTEX_UNLOCK(env, td->mvcc_mtx); if (need_free) { diff --git a/test/tcl/TESTS b/test/tcl/TESTS index a62c02364..a3d4a7608 100644 --- a/test/tcl/TESTS +++ b/test/tcl/TESTS @@ -1,6 +1,13 @@ # Automatically built by dist/s_test; may require local editing. +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +Cold-boot a 4-site group. The first two sites start quickly and + initiate an election. The other two sites don't join the election until + the middle of the long full election timeout period. It's important that + the number of sites that start immediately be a sub-majority, because + that's the case that used to have a bug in it [#18456]. + =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= backup Test of hotbackup functionality. @@ -34,11 +41,8 @@ bigfile002 with 1K pages. Dirty page 6000000. Sync. =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -Cold-boot a 4-site group. The first two sites start quickly and - initiate an election. The other two sites don't join the election until - the middle of the long full election timeout period. It's important that - the number of sites that start immediately be a sub-majority, because - that's the case that used to have a bug in it [#18456]. +db_reptest + Wrapper to configure and run the db_reptest program. =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= dbm @@ -48,10 +52,6 @@ dbm Then reopen the file, re-retrieve everything. Finally, delete everything. -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= -db_reptest - Wrapper to configure and run the db_reptest program. - =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= dead001 Use two different configurations to test deadlock detection among a @@ -2301,6 +2301,27 @@ sql001 to make sure we get same results from both sides. Also try an insert operation on client side; it should fail. +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +ssi001 + Serializable Snapshot Isolation: the canonical write-skew anomaly. + + Two snapshot-safe transactions each read the datum the other writes + (T1: read y, write x; T2: read x, write y). Under plain snapshot + isolation both commit, producing a write-skew anomaly. Under SSI + exactly one must be aborted with DB_SNAPSHOT_CONFLICT/UNSAFE. + + x and y live in separate databases so the two writes never contend + on the same page -- the only contention we want to exercise is the + read/write antidependency tracked by SSI, not page locks. + +=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= +ssi002 + Serializable Snapshot Isolation: no false-positive aborts. + + Two snapshot-safe transactions that touch disjoint data, and a + read-only snapshot-safe transaction, must all commit. SSI must only + abort genuine dangerous structures, never independent transactions. + =-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-= test001 Small keys/data diff --git a/test/tcl/ssi001.tcl b/test/tcl/ssi001.tcl new file mode 100644 index 000000000..a4a571862 --- /dev/null +++ b/test/tcl/ssi001.tcl @@ -0,0 +1,83 @@ +# See the file LICENSE for redistribution information. +# +# Copyright (c) 2026 berkeleydb/libdb contributors. All rights reserved. +# +# $Id$ +# +# TEST ssi001 +# TEST Serializable Snapshot Isolation: the canonical write-skew anomaly. +# TEST +# TEST Two snapshot-safe transactions each read the datum the other writes +# TEST (T1: read y, write x; T2: read x, write y). Under plain snapshot +# TEST isolation both commit, producing a write-skew anomaly. Under SSI +# TEST exactly one must be aborted with DB_SNAPSHOT_CONFLICT/UNSAFE. +# TEST +# TEST x and y live in separate databases so the two writes never contend +# TEST on the same page -- the only contention we want to exercise is the +# TEST read/write antidependency tracked by SSI, not page locks. +proc ssi001 { } { + source ./include.tcl + + puts "Ssi001: Serializable Snapshot Isolation write-skew anomaly" + + env_cleanup $testdir + + # Multiversion (snapshot) + txn + lock environment. A short lock + # timeout guarantees the single-threaded interleave can never hang on + # an unexpected lock wait; it fails fast instead. + set e [berkdb_env -create -home $testdir \ + -txn -lock -log -multiversion -lock_timeout 2000000] + error_check_good env_open [is_valid_env $e] TRUE + + set dbx [berkdb open -create -auto_commit -env $e -btree -multiversion x.db] + error_check_good dbx_open [is_valid_db $dbx] TRUE + set dby [berkdb open -create -auto_commit -env $e -btree -multiversion y.db] + error_check_good dby_open [is_valid_db $dby] TRUE + + error_check_good seed_x [$dbx put k 0] 0 + error_check_good seed_y [$dby put k 0] 0 + + puts "\tSsi001.a: Interleave two snapshot-safe transactions" + set t1 [$e txn -snapshot_safe] + error_check_good t1_begin [is_valid_txn $t1 $e] TRUE + set t2 [$e txn -snapshot_safe] + error_check_good t2_begin [is_valid_txn $t2 $e] TRUE + + # Each reads the item the other will write (records the rw edges). + error_check_good t1_read_y [catch {$dby get -txn $t1 k} r1] 0 + error_check_good t2_read_x [catch {$dbx get -txn $t2 k} r2] 0 + + # Cross writes, in different databases (no page contention). + set w1 [catch {$dbx put -txn $t1 k 1} wres1] + set w2 [catch {$dby put -txn $t2 k 1} wres2] + + puts "\tSsi001.b: Commit both; SSI must abort exactly one" + set c1 [catch {$t1 commit} cres1] + set c2 [catch {$t2 commit} cres2] + + set fail1 [expr {$w1 != 0 || $c1 != 0}] + set fail2 [expr {$w2 != 0 || $c2 != 0}] + + # If a write failed, the txn handle is dead only after commit; clean up + # any txn whose write failed but whose commit we never reached. + if { $w1 != 0 && $c1 == 0 } { catch {$t1 abort} } + if { $w2 != 0 && $c2 == 0 } { catch {$t2 abort} } + + if { $fail1 } { + error_check_good t1_is_ssi_err \ + [is_substr "$wres1 $cres1" "DB_SNAPSHOT"] 1 + } + if { $fail2 } { + error_check_good t2_is_ssi_err \ + [is_substr "$wres2 $cres2" "DB_SNAPSHOT"] 1 + } + + # The SSI guarantee: the write-skew must be prevented (>=1 abort) ... + error_check_good ssi_no_write_skew [expr {$fail1 || $fail2}] 1 + # ... and we must not have spuriously aborted both. + error_check_good ssi_not_both_aborted [expr {$fail1 && $fail2}] 0 + + error_check_good dbx_close [$dbx close] 0 + error_check_good dby_close [$dby close] 0 + error_check_good env_close [$e close] 0 +} diff --git a/test/tcl/ssi002.tcl b/test/tcl/ssi002.tcl new file mode 100644 index 000000000..8092c7718 --- /dev/null +++ b/test/tcl/ssi002.tcl @@ -0,0 +1,55 @@ +# See the file LICENSE for redistribution information. +# +# Copyright (c) 2026 berkeleydb/libdb contributors. All rights reserved. +# +# $Id$ +# +# TEST ssi002 +# TEST Serializable Snapshot Isolation: no false-positive aborts. +# TEST +# TEST Two snapshot-safe transactions that touch disjoint data, and a +# TEST read-only snapshot-safe transaction, must all commit. SSI must only +# TEST abort genuine dangerous structures, never independent transactions. +proc ssi002 { } { + source ./include.tcl + + puts "Ssi002: SSI must not abort non-conflicting transactions" + + env_cleanup $testdir + set e [berkdb_env -create -home $testdir \ + -txn -lock -log -multiversion -lock_timeout 2000000] + error_check_good env_open [is_valid_env $e] TRUE + set dba [berkdb open -create -auto_commit -env $e -btree -multiversion a.db] + set dbb [berkdb open -create -auto_commit -env $e -btree -multiversion b.db] + error_check_good a_open [is_valid_db $dba] TRUE + error_check_good b_open [is_valid_db $dbb] TRUE + error_check_good seed_a [$dba put ka 0] 0 + error_check_good seed_b [$dbb put kb 0] 0 + + puts "\tSsi002.a: disjoint read/write sets both commit" + set t1 [$e txn -snapshot_safe] + set t2 [$e txn -snapshot_safe] + # t1 works only on a.db, t2 only on b.db -- no shared items. + error_check_good t1_ra [catch {$dba get -txn $t1 ka} r] 0 + error_check_good t2_rb [catch {$dbb get -txn $t2 kb} r] 0 + error_check_good t1_wa [$dba put -txn $t1 ka 1] 0 + error_check_good t2_wb [$dbb put -txn $t2 kb 1] 0 + error_check_good t1_commit [$t1 commit] 0 + error_check_good t2_commit [$t2 commit] 0 + + puts "\tSsi002.b: a read-only snapshot-safe txn commits" + set t3 [$e txn -snapshot_safe] + error_check_good t3_ra [catch {$dba get -txn $t3 ka} r] 0 + error_check_good t3_rb [catch {$dbb get -txn $t3 kb} r] 0 + error_check_good t3_commit [$t3 commit] 0 + + puts "\tSsi002.c: read-then-write on the same item by one txn commits" + set t4 [$e txn -snapshot_safe] + error_check_good t4_r [catch {$dba get -txn $t4 ka} r] 0 + error_check_good t4_w [$dba put -txn $t4 ka 2] 0 + error_check_good t4_commit [$t4 commit] 0 + + error_check_good a_close [$dba close] 0 + error_check_good b_close [$dbb close] 0 + error_check_good env_close [$e close] 0 +} diff --git a/test/tcl/testparams.tcl b/test/tcl/testparams.tcl index 4f2de1b3d..df529b866 100644 --- a/test/tcl/testparams.tcl +++ b/test/tcl/testparams.tcl @@ -79,6 +79,7 @@ set test_names(sdb) [list sdb001 sdb002 sdb003 sdb004 sdb005 sdb006 \ set test_names(sdbtest) [list sdbtest001 sdbtest002] set test_names(sec) [list sec001 sec002] set test_names(si) [list si001 si002 si003 si004 si005 si006 si007 si008] +set test_names(ssi) [list ssi001 ssi002] set test_names(test) [list test001 test002 test003 test004 test005 \ test006 test007 test008 test009 test010 test011 test012 test013 test014 \ test015 test016 test017 test018 test019 test020 test021 test022 test023 \