|
| 1 | +# LMDB-Backed Isolation Up To Snapshot |
| 2 | + |
| 3 | +This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds. |
| 4 | + |
| 5 | +This plan must be maintained in accordance with `PLANS.md` at the repository root. |
| 6 | + |
| 7 | +## Purpose / Big Picture |
| 8 | + |
| 9 | +After this change, the LMDB Store will rely on LMDB's own transaction isolation for NONE, READ_COMMITTED, SNAPSHOT_READ, and SNAPSHOT. Snapshot and repeatable-read behavior will come from LMDB read transactions rather than the current in-memory SnapshotSailStore. Users will see the same externally observable isolation behavior (as proven by the existing isolation tests), but with simpler transaction mechanics and fewer in-memory overlays. |
| 10 | + |
| 11 | +## Progress |
| 12 | + |
| 13 | +- [x] (2026-01-02 09:45Z) Drafted ExecPlan and initial analysis. |
| 14 | +- [x] (2026-01-02 13:30Z) Defined LMDB transaction context and APIs. |
| 15 | +- [x] (2026-01-02 14:25Z) Implemented LMDB-backed SailSource/Sink/Dataset behavior. |
| 16 | +- [x] (2026-01-02 14:25Z) Updated isolation level support and lock strategy. |
| 17 | +- [x] (2026-01-02 13:59Z) Enforced thread-affine LMDB txn context use. |
| 18 | +- [x] (2026-01-02 15:01Z) Ran LmdbStoreIsolationLevelTest and LmdbSailStoreTest. |
| 19 | +- [x] (2026-01-02 15:18Z) Added commit read-lock + pinned ValueStore reads. |
| 20 | +- [x] (2026-01-02 15:25Z) Added commit-window regression test and commit/read lock guard. |
| 21 | +- [x] (2026-01-02 21:35Z) Added SNAPSHOT begin/commit regression test. |
| 22 | +- [x] (2026-01-02 21:45Z) Guarded snapshot txn pinning with commit read lock. |
| 23 | +- [x] (2026-01-03 11:39Z) Wired SNAPSHOT_READ pinning and deferred updates in LmdbTxnContext. |
| 24 | +- [x] (2026-01-03 11:45Z) Aligned deprecated tracking with pending update buffering. |
| 25 | +- [x] (2026-01-03 11:54Z) Treated SNAPSHOT reads as pinned + deferred to keep snapshotRead stable. |
| 26 | +- [x] (2026-01-03 13:30Z) Added serializable conflict tracking + prepare checks for LMDB snapshots. |
| 27 | +- [x] (2026-01-03 13:34Z) LmdbOptimisticIsolationTest green (mvnf core/sail/lmdb LmdbOptimisticIsolationTest). |
| 28 | +- [ ] Run full LMDB module verify and isolation suite. |
| 29 | + |
| 30 | +## Surprises & Discoveries |
| 31 | + |
| 32 | +- Observation: LmdbStore wraps the backend with SnapshotSailStore and bypasses it only when isolation is disabled, so snapshot semantics are not coming from LMDB. |
| 33 | + Evidence: |
| 34 | + core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/LmdbStore.java |
| 35 | + this.store = new SnapshotSailStore(backingStore, ...) |
| 36 | + if (isIsolationDisabled()) { return backingStore.getExplicitSailSource(); } |
| 37 | +- Observation: LmdbSailSource overrides fork() and does not use SailSourceBranch; prepare() on the source is a no-op unless we wire serializable conflict checks directly into LmdbSailSource/LmdbTxnContext. |
| 38 | + Evidence: |
| 39 | + core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/LmdbSailStore.java |
| 40 | + LmdbSailSource.fork() returns a new LmdbSailSource (bypassing SailSourceBranch) |
| 41 | + LmdbSailSource.prepare() now performs serializable conflict checks |
| 42 | +- Observation: LmdbSailSink.flush() commits LMDB transactions immediately, which would prematurely commit changes if SnapshotSailStore were removed. |
| 43 | + Evidence: |
| 44 | + core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/LmdbSailStore.java |
| 45 | + tripleStore.commit(); ... valueStore.commit(); |
| 46 | +- Observation: Triple store writes can run on a background thread, so the write transaction is not on the caller thread; LMDB write transactions are thread-affine. |
| 47 | + Evidence: |
| 48 | + core/sail/lmdb/src/main/java/org/eclipse/rdf4j/sail/lmdb/LmdbSailStore.java |
| 49 | + tripleStoreExecutor.submit(() -> { tripleStore.startTransaction(); ... }); |
| 50 | +- Observation: SPARQL MODIFY updates must not see their own writes while iterating WHERE bindings. |
| 51 | + Evidence: |
| 52 | + testsuites/repository/src/main/java/org/eclipse/rdf4j/testsuite/repository/optimistic/DeleteInsertTest.java |
| 53 | + DeleteInsertTest initially failed until per-update changes were buffered. |
| 54 | +- Observation: LMDB module verify appears to stall in OptimisticIsolationTest (IsolationLevelTest logging). |
| 55 | + Evidence: |
| 56 | + logs/mvnf/20260102-213127-verify.log (no completion after IsolationLevelTest stack trace). |
| 57 | + core/sail/lmdb/target/surefire-reports/2026-01-02T22-31-54_127-jvmRun1.dumpstream |
| 58 | + |
| 59 | +## Decision Log |
| 60 | + |
| 61 | +- Decision: Implement LMDB-native isolation for NONE, READ_COMMITTED, SNAPSHOT_READ, SNAPSHOT and stop using SnapshotSailStore for those levels. |
| 62 | + Rationale: The requirement is to rely solely on LMDB isolation up to SNAPSHOT, and the current SnapshotSailStore overlay prevents that. |
| 63 | + Date/Author: 2026-01-02 / Codex |
| 64 | +- Decision: Implement SERIALIZABLE via optimistic conflict detection over LMDB snapshots. |
| 65 | + Rationale: Track observed statement patterns and compare pinned snapshot vs current state at prepare/commit; this preserves serializable semantics without a SnapshotSailStore overlay. |
| 66 | + Date/Author: 2026-01-03 / Codex |
| 67 | +- Decision: Disable async triple-store write threading for transactions that need read-your-writes. |
| 68 | + Rationale: LMDB write transactions are thread-affine; read-your-writes requires reading from the same write transaction. |
| 69 | + Date/Author: 2026-01-02 / Codex |
| 70 | +- Decision: Buffer per-update changes and merge on endUpdate to keep MODIFY WHERE evaluation stable. |
| 71 | + Rationale: SPARQL updates must not observe their own modifications while streaming bindings. |
| 72 | + Date/Author: 2026-01-02 / Codex |
| 73 | +- Decision: Bind LMDB transaction contexts to the thread that opens pinned/read-write txns; reject cross-thread access. |
| 74 | + Rationale: LMDB read/write transactions are thread-affine, so snapshot or write transactions must stay on one thread. |
| 75 | + Date/Author: 2026-01-02 / Codex |
| 76 | +- Decision: Block dataset creation during commit and pin ValueStore reads per dataset. |
| 77 | + Rationale: Avoid a window where triple-store commits become visible before the ValueStore commit, leading to unresolved IDs. |
| 78 | + Date/Author: 2026-01-02 / Codex |
| 79 | +- Decision: Split SNAPSHOT_READ vs SNAPSHOT handling in LmdbTxnContext using explicit mode flags. |
| 80 | + Rationale: Defer writes for SNAPSHOT_READ while pinning read snapshots, and eagerly start write transactions for SNAPSHOT to keep snapshot boundaries deterministic. |
| 81 | + Date/Author: 2026-01-03 / Codex |
| 82 | +- Decision: Track deprecated removals per pending update before merging into transaction-wide state. |
| 83 | + Rationale: Aborted updates must not leak deprecations into later change visibility or connection listener notifications. |
| 84 | + Date/Author: 2026-01-03 / Codex |
| 85 | +- Decision: For SNAPSHOT, pin read snapshots and defer writes like SNAPSHOT_READ. |
| 86 | + Rationale: SnapshotRead tests require stable iterators; deferring writes avoids mid-iteration visibility while preserving read-your-writes via overlays. |
| 87 | + Date/Author: 2026-01-03 / Codex |
| 88 | + |
| 89 | +## Outcomes & Retrospective |
| 90 | + |
| 91 | +Implemented LMDB-backed transaction context, forkable sources, and snapshot handling without SnapshotSailStore. Added serializable conflict detection based on observed patterns against pinned snapshots, with prepare-time checks for LMDB-backed branches. Remaining work: run full LMDB module verify and isolation suite before final acceptance. |
| 92 | + |
| 93 | +## Context and Orientation |
| 94 | + |
| 95 | +The LMDB store implementation lives under core/sail/lmdb. LmdbStore is the Sail entry point. It currently wraps LmdbSailStore in SnapshotSailStore, which uses in-memory changesets to provide snapshot semantics. SailSourceConnection in core/sail/base uses SailSource.fork() to create transaction-scoped branches when isolation is not NONE, so the LMDB store must implement a forking SailSource if it is used directly. LmdbSailSink and LmdbSailDataset are the LMDB-specific write and read adapters. TxnManager and ValueStore manage LMDB read/write transactions. |
| 96 | + |
| 97 | +Definitions used here: |
| 98 | +- LMDB environment means the LMDB database handle opened by mdb_env_open (the value store and triple store each use separate environments). |
| 99 | +- Snapshot isolation means repeatable reads within a transaction and no visibility of concurrent commits after the transaction begins. |
| 100 | +- Pinned read transaction means a long-lived LMDB read transaction held open for the duration of a Sail transaction. |
| 101 | + |
| 102 | +## Plan of Work |
| 103 | + |
| 104 | +First, define an LMDB transaction context that is created when a Sail transaction begins and closed on commit or rollback. This context must own the LMDB read transaction used for repeatable reads (SNAPSHOT, SNAPSHOT_READ) and, when writes occur, the LMDB write transactions for the value store and triple store. It must also provide a consistent story for NONE and READ_COMMITTED: reads use short-lived read transactions unless a write transaction is active, in which case reads must use the write transaction so that read-your-writes works. |
| 105 | + |
| 106 | +Next, make LmdbSailSource forkable and context-aware. A forked LmdbSailSource should bind to the transaction context so that datasets and sinks created from it use the same read/write transactions. LmdbSailSink.flush() must not commit for transaction-scoped sinks; commit should occur once per Sail transaction at SailSource.flush() or on connection commit. This removes the need for SnapshotSailStore to buffer changes in memory. |
| 107 | + |
| 108 | +Then, update the LMDB transaction machinery. TxnManager must support pinned read transactions that are not reset on every commit, and ValueStore must allow a pinned read transaction for snapshot transactions instead of its current per-call renew/reset behavior. Ensure map-resize logic does not invalidate pinned snapshot transactions; if it must, detect and fail those transactions with a clear conflict error. |
| 109 | + |
| 110 | +Finally, update LmdbStore to remove the SnapshotSailStore wrapper for SNAPSHOT and below, adjust supported isolation levels, and run the isolation and LMDB module tests to validate behavior. |
| 111 | + |
| 112 | +## Concrete Steps |
| 113 | + |
| 114 | +All commands are from the repository root. |
| 115 | + |
| 116 | +1) Baseline build (required before tests): |
| 117 | + mvn -T 1C -o -Dmaven.repo.local=.m2_repo -Pquick clean install | tail -200 |
| 118 | + |
| 119 | +2) If adding or adjusting tests, run the smallest targeted test first: |
| 120 | + python3 .codex/skills/mvnf/scripts/mvnf.py LmdbStoreIsolationLevelTest |
| 121 | + |
| 122 | + Expected tail excerpt contains "BUILD SUCCESS". |
| 123 | + |
| 124 | +3) Run LMDB module verification after implementation: |
| 125 | + python3 .codex/skills/mvnf/scripts/mvnf.py core/sail/lmdb |
| 126 | + |
| 127 | +4) If module tests pass but isolation semantics are still in doubt, run the Sail isolation suite explicitly: |
| 128 | + python3 .codex/skills/mvnf/scripts/mvnf.py SailIsolationLevelTest |
| 129 | + |
| 130 | +If any command fails because of missing offline artifacts, rerun the same command once without -o, then return to offline runs. |
| 131 | + |
| 132 | +## Validation and Acceptance |
| 133 | + |
| 134 | +Acceptance is met when: |
| 135 | +- LmdbStoreIsolationLevelTest passes for NONE, READ_COMMITTED, SNAPSHOT_READ, and SNAPSHOT. |
| 136 | +- Snapshot semantics are visible: repeated reads inside a transaction are stable; concurrent commits do not appear mid-transaction; read-your-writes works. |
| 137 | +- The LMDB module test run core/sail/lmdb is green. |
| 138 | +- LmdbStore no longer relies on SnapshotSailStore for the supported isolation levels, and LmdbSailSource.fork() is implemented. |
| 139 | + |
| 140 | +If SERIALIZABLE is removed, tests should skip it by reporting it as unsupported. |
| 141 | + |
| 142 | +## Idempotence and Recovery |
| 143 | + |
| 144 | +All steps are repeatable. If a change causes isolation tests to fail, revert only the LMDB module changes, keep any new tests, and iterate. If map resize conflicts with pinned read transactions, return a clear SailConflictException and add a targeted test to document the behavior. |
| 145 | + |
| 146 | +## Artifacts and Notes |
| 147 | + |
| 148 | +Collect short snippets (no more than a few lines) from: |
| 149 | +- core/sail/lmdb/target/surefire-reports/ showing passing isolation tests. |
| 150 | +- Any new LMDB-specific tests added for snapshot or read-your-writes behavior. |
| 151 | + |
| 152 | +Example (placeholder): |
| 153 | + Tests run: 12, Failures: 0, Errors: 0, Skipped: 0 |
| 154 | + |
| 155 | +## Interfaces and Dependencies |
| 156 | + |
| 157 | +New or adjusted LMDB-facing APIs should live in core/sail/lmdb: |
| 158 | + |
| 159 | +- New class org.eclipse.rdf4j.sail.lmdb.LmdbTxnContext |
| 160 | + Responsibilities: track isolation level, hold pinned read transactions, and manage write transaction lifecycle across TripleStore and ValueStore. |
| 161 | + Required methods (names can be adjusted as long as intent is preserved): |
| 162 | + - void begin(IsolationLevel level) |
| 163 | + - Txn acquireTripleReadTxn() (returns pinned or per-call based on level) |
| 164 | + - long acquireTripleWriteTxn() (write transaction handle on the caller thread) |
| 165 | + - ValueStore.ReadTxn acquireValueReadTxn() (pinned or per-call) |
| 166 | + - void markWriteStarted() |
| 167 | + - void commit() |
| 168 | + - void rollback() |
| 169 | + - void close() |
| 170 | + |
| 171 | +- LmdbSailStore.LmdbSailSource |
| 172 | + Implement fork() and accept an optional LmdbTxnContext. |
| 173 | + dataset(IsolationLevel) must use context-provided read transactions. |
| 174 | + sink(IsolationLevel) must create sinks that write into context-managed LMDB write transactions. |
| 175 | + |
| 176 | +- LmdbSailStore.LmdbSailSink |
| 177 | + Allow a transaction-scoped mode where flush() does not commit; commit is handled by the enclosing SailSource.flush() or connection commit. |
| 178 | + |
| 179 | +- TxnManager |
| 180 | + Add a pinned read transaction path (not reset on commit) or a way to exclude active snapshot transactions from reset(). |
| 181 | + |
| 182 | +- ValueStore |
| 183 | + Add a pinned read transaction API (similar semantics to TxnManager) and a method to run reads against that pinned transaction. |
| 184 | + |
| 185 | +- LmdbStore |
| 186 | + Remove the SnapshotSailStore wrapper for supported isolation levels and update setSupportedIsolationLevels(...) to stop claiming SERIALIZABLE if it is not implemented. |
| 187 | + |
| 188 | +Each new or changed API should be documented inline with a short comment explaining how it supports LMDB-backed snapshot isolation. |
| 189 | + |
| 190 | +## Plan Change Note |
| 191 | + |
| 192 | +(2026-01-02) Created the initial ExecPlan document from repository analysis so implementation can proceed with a living plan. |
| 193 | +(2026-01-03) Updated progress and decisions to reflect snapshot-read pinning and update deferral work in LmdbTxnContext. |
| 194 | +(2026-01-03) Recorded deprecated-tracking alignment in progress and decision log. |
| 195 | +(2026-01-03) Documented SNAPSHOT deferral decision to satisfy snapshotRead stability. |
| 196 | +(2026-01-03) Updated serializable approach and prepare-time conflict checks after LmdbOptimisticIsolationTest coverage. |
0 commit comments