docs: RELEASES.md entry for 8.0.15 (atomic ifRev CAS)
This commit is contained in:
parent
9a3d1bd494
commit
b1fe25a339
1 changed files with 28 additions and 0 deletions
28
RELEASES.md
28
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)
|
## 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
|
Defense-in-depth for the one-time 7→8 layout migration. The migration rescues the head branch's
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue