docs: RELEASES.md entry for 8.0.16 (atomic ifAbsent/upsert + exact blob refCounts)
This commit is contained in:
parent
867939ed50
commit
54e7c0eced
1 changed files with 31 additions and 0 deletions
31
RELEASES.md
31
RELEASES.md
|
|
@ -10,6 +10,37 @@ Full auto-generated changelog: `CHANGELOG.md` · Releases: https://github.com/so
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## v8.0.16 — 2026-07-08 (concurrency fast-follow: atomic `ifAbsent`/`upsert` + exact blob reference counts)
|
||||||
|
|
||||||
|
Closes the two remaining check-then-act races found in the sweep that followed v8.0.15's CAS fix
|
||||||
|
(disclosed in that release's notes). Same bug class — a check performed before the serialization
|
||||||
|
point that guards the apply — same cure.
|
||||||
|
|
||||||
|
**1 — `add({ ifAbsent })` and `add({ upsert })` are now atomic.** The absence check ran before the
|
||||||
|
commit mutex, so N concurrent same-id creates could all pass it and all write — the second
|
||||||
|
silently overwriting the first, violating ifAbsent's "returns the existing id **without writing**"
|
||||||
|
contract and upsert's "merge, never clobber" contract. The insert leg now carries a must-be-absent
|
||||||
|
precondition (the same conditional-commit primitive as `ifRev`), verified under the commit mutex:
|
||||||
|
exactly one concurrent create wins; every other caller takes its documented resolution — ifAbsent
|
||||||
|
returns the existing id with zero writes, upsert merges into the now-existing entity (with a
|
||||||
|
bounded retry if a concurrent delete intervenes). Verified: 8 concurrent `ifAbsent` creates
|
||||||
|
advance the store by exactly ONE generation; 8 concurrent upserts on an absent id produce one
|
||||||
|
create + seven merges (`_rev` lands at exactly 8). Note: `transact()`'s batch-level
|
||||||
|
ifAbsent/upsert keep planning-time semantics (the batch converges to a valid entity; the window is
|
||||||
|
documented, not silent).
|
||||||
|
|
||||||
|
**2 — Blob reference counts are exact under concurrency.** The content-addressed blob store's
|
||||||
|
`write()` (dedup: exists → add a reference, absent → create) and `delete()` (decrement → remove at
|
||||||
|
zero) were unserialized read-modify-writes over the blob's metadata. Concurrent writes of
|
||||||
|
identical content could lose references — making a later delete remove bytes **another file still
|
||||||
|
referenced** (data loss), or leak unreferenced blobs. All reference-count-bearing mutations are
|
||||||
|
now serialized per content hash (distinct content never contends): N concurrent identical writes
|
||||||
|
yield exactly N references, and a blob is physically removed only when the true last reference
|
||||||
|
drops. Verified with concurrent write/delete storms.
|
||||||
|
|
||||||
|
Both fixes are in-process complete by construction — storage enforces single-writer-per-directory,
|
||||||
|
so the process is the whole concurrency domain. No API changes.
|
||||||
|
|
||||||
## v8.0.15 — 2026-07-08 (`ifRev` CAS is now atomic — exactly one winner under concurrency)
|
## 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
|
Correctness fix for optimistic concurrency, reported from a production cutover rehearsal. N
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue