Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
build_unix/**
compile_commands.json
test/tcl/tclIndex
1 change: 1 addition & 0 deletions dist/api_flags
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
88 changes: 88 additions & 0 deletions docs/ssi/M2-partition-design.md
Original file line number Diff line number Diff line change
@@ -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`).
53 changes: 53 additions & 0 deletions docs/ssi/M4-commit-lifecycle.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions lang/tcl/tcl_txn.c
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ tcl_Txn(interp, objc, objv, dbenv, envip)
"-nowait",
"-parent",
"-snapshot",
"-snapshot_safe",
"-sync",
"-wrnosync",
NULL
Expand All @@ -167,6 +168,7 @@ tcl_Txn(interp, objc, objv, dbenv, envip)
TXNNOWAIT,
TXNPARENT,
TXNSNAPSHOT,
TXNSNAPSHOTSAFE,
TXNSYNC,
TXNWRNOSYNC
};
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 6 additions & 0 deletions src/common/db_err.c
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
20 changes: 18 additions & 2 deletions src/db/db_meta.c
Original file line number Diff line number Diff line change
Expand Up @@ -1162,15 +1162,31 @@ __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))) {
LOCK_INIT(*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.
Expand Down
6 changes: 5 additions & 1 deletion src/dbinc/db.in
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/*
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -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)
Expand Down
24 changes: 24 additions & 0 deletions src/dbinc/lock.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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. */
Expand Down Expand Up @@ -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;
};

Expand Down Expand Up @@ -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).
Expand All @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/dbinc/txn.h
Original file line number Diff line number Diff line change
Expand Up @@ -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. */

Expand All @@ -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 */
Expand Down
3 changes: 2 additions & 1 deletion src/dbinc_auto/api_flags.in
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/dbinc_auto/lock_ext.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
Loading