diff --git a/RELEASES.md b/RELEASES.md index 10cc675b..3834d116 100644 --- a/RELEASES.md +++ b/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) Correctness fix for optimistic concurrency, reported from a production cutover rehearsal. N