From b1fe25a339bfdfb8a35c981b5bec450c5323a399 Mon Sep 17 00:00:00 2001 From: David Snelling Date: Wed, 8 Jul 2026 14:53:34 -0700 Subject: [PATCH] docs: RELEASES.md entry for 8.0.15 (atomic ifRev CAS) --- RELEASES.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index 018856c0..10cc675b 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -10,6 +10,34 @@ Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/so --- +## v8.0.15 — 2026-07-08 (`ifRev` CAS is now atomic — exactly one winner under concurrency) + +Correctness fix for optimistic concurrency, reported from a production cutover rehearsal. N +**concurrent** `update({ ifRev })` calls carrying the same expected revision ALL succeeded — zero +`RevisionConflictError`s, last-writer-wins, the other N−1 writes silently lost. (Sequential calls +conflicted correctly.) The revision check ran before the commit mutex, so interleaved callers all +passed it before any apply landed — breaking the exactly-one-winner semantics the +[optimistic-concurrency guide](docs/guides/optimistic-concurrency.md) promises, which advisory +locks and per-entity ledgers/counters build on. + +The fix makes the check-and-apply a **conditional commit**: the generation store's commit paths +accept a precondition that runs under the commit mutex against the just-read authoritative +before-images — the per-record analogue of `ifAtGeneration`, which always ran there. `update()` +and `transact()` per-op `ifRev` both re-verify at that point (the earlier check remains as a cheap +fast-fail). A conflict aborts atomically: nothing staged, nothing applied, the same +`RevisionConflictError` as before. Two adjacent behaviors also became honest: + +- **`_rev` is now monotonic under concurrency.** The winner's stamp derives from the + authoritative before-image, so N concurrent plain updates advance `_rev` by N (previously they + could all stamp the same stale value). +- **CAS against a concurrently-deleted entity** now throws `EntityNotFoundError` instead of + silently resurrecting it (plain updates keep their last-writer-wins re-create semantics). + +Verified with a concurrency regression suite: 8 parallel same-rev updates → exactly 1 winner + 7 +conflicts; the documented read→CAS→retry ledger loop converges exactly (8 workers, 8 decrements, +0 lost) — `tests/integration/ifrev-concurrent-cas.test.ts`. If you serialized writes in your own +adapter as a workaround, it can come out after this upgrade. + ## v8.0.14 — 2026-07-07 (7→8 migration preserves branch-scoped non-entity state instead of deleting it) Defense-in-depth for the one-time 7→8 layout migration. The migration rescues the head branch's