fix(api): #269 rotate aggregate ETag on Collection/Playout config siblings #307
2
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
7a9b30de71 |
fix(api): #269 review fixes — rebase force-write delta so rotation survives a race (Codex F1/F3)
Build ErsatzTV Image / decisions.md append-only (pull_request) Successful in 6s
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 7s
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (pull_request) Successful in 13s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 5m20s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 7m50s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
Build ErsatzTV Image / API docs in sync (OpenAPI + endpoint index) (push) Has been skipped
Build ErsatzTV Image / Docs update reminder (push) Has been skipped
Build ErsatzTV Image / decisions.md append-only (push) Has been skipped
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (push) Successful in 3m43s
Build ErsatzTV Image / Build & test (.NET) (push) Successful in 6m51s
Build ErsatzTV Image / Build & push image (amd64) (push) Successful in 3m26s
Independent Codex review (reconciled by Fable against a MERGEABLE fork verdict) found SaveChangesForcingVersion silently DROPPED a pending Version++ under a concurrent versioned-write race: on DbUpdateConcurrencyException it adopted the DB's current Version verbatim (original = current = dbVersion), so a bumping sibling committed at dbVersion instead of dbVersion+1. Net: an editor holding the concurrent writer's ETag was never invalidated by the sibling's change — the exact lost-update the #253/#269 contract exists to close, lost under the very condition the helper handles. F1 fix (shared helper, corrects all 25 bumpers incl. the pre-existing Add*ToPlaylist / schedule-item writers): rebase the pending delta on top of the stored token — pendingDelta = current - original; original = dbVersion; current = dbVersion + pendingDelta Bumpers (delta 1) advance to dbVersion+1; non-bumpers/deletes (delta 0, e.g. ErasePlayoutHistory) still adopt the stored token unchanged, so RootWriterForceVersionTests is unaffected. Idempotent across the bounded retry loop. F3: the force-race tests now assert Version==3 (rebase), not just membership survival; added the missing Playout force-race+rotate test. Negative-controlled: with the helper fix reverted, both strengthened tests go red. F2 (Medium, deferred → #308): two concurrent same-item Add*ToCollection can both pass the membership check and the loser 500s on the composite-PK violation (DbUpdateException, which the helper doesn't catch). Pre-existing and narrow (no corruption); doc claims softened to name it. Filed #308. Docs: api-conventions §7a + decisions.md prose corrected from "adopt the stored token" to the rebase semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
83f753b211 |
fix(api): #269 rotate aggregate ETag on Collection/Playout config siblings
Complete the #253 optimistic-concurrency contract's cross-editor ETag rotation tail. The non-If-Match config siblings mutated editor-visible state without bumping Version, so a concurrent editor of the same root never invalidated. Now the Collection Add*/Remove handlers bump Collection.Version, and UpdateCollection / UpdatePlayout / the three ScheduleFile writers (which already force-wrote past a concurrent bump) now bump too — all via SaveChangesForcingVersion (no If-Match → force write, never 412/500). No-op idempotence (Fable-caught trap): these gate reindex/BuildPlayout fan-out on SaveChanges()>0, so an unconditional bump would fire spurious rebuilds on an idempotent re-add / same-value re-submit. Each now short-circuits a genuine no-op before the bump — Add handlers by an explicit membership check (also fixing a latent duplicate-CollectionItem insert), scalar writers by ChangeTracker.HasChanges(). Corrects #269's framing: the Add*ToCollection family is not repository-mediated (IMediaCollectionRepository is read-only); each handler writes via its own dbContext, so the scanner's separate membership path is unaffected (a background scan does not rotate the editor ETag). Tests: CollectionEtagRotationTests + PlayoutScheduleFileEtagRotationTests (rotation, no-op-without-bump-or-rebuild, force-write-past-concurrent-bump), no-op guard proven non-vacuous by inverting the membership check. Docs: api-conventions §7a + decisions.md. No new status codes / no OpenAPI change (these endpoints take no If-Match, never 412). The #265 RFC-7232 If-Match parser refinement is a separate PR. fixes #269 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |