Two concurrent adds of the same item both membership-check it absent, both
insert the CollectionItem composite key, and the loser's
SaveChangesForcingVersion threw an uncaught DbUpdateException (SQLite 19 /
MySQL 1062) -> 500. Now the loser is an idempotent no-op.
- ConcurrencyExtensions.TrySaveChangesForcingVersion: bool-returning sibling
that catches only a classified unique/PK violation and returns false.
- 10 single-item Add*ToCollection handlers: return Unit.Default (no-op, skip
fan-out) on false — the racing winner already inserted + rotated + rebuilt.
- Bulk AddItemsToCollection: retry on a fresh context against recomputed
membership so a partial-overlap collision doesn't drop the non-colliding
items (bounded loop; common no-collision path runs once).
- Provider detection via a TvContext.IsUniqueConstraintViolation static
delegate (matches the existing IsSqlite/LastInsertedRowId provider seam),
wired from Startup to SqliteErrorClassifier / MySqlErrorClassifier.
- Add*ToPlaylist is NOT affected (PlaylistItem has its own identity PK; a
playlist may legitimately contain the same item more than once).
Tests: a negative-control anchor proves the race genuinely throws a classified
exception; end-to-end handler tests reproduce a real cross-connection race via
a shared-cache SQLite harness + a SavingChanges interceptor (the single-conn
in-memory fixture cannot). Every fix-dependent test verified to fail with the
catch disabled.
Docs: api-conventions.md §7a (idempotent insert under concurrency) +
decisions/optimistic-concurrency.md.
fixes#308
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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>
Backend mutation-hardening cluster from the 2026-07-11 mutation-safety audit
sweep (adversarial-reviewer #22/#23), the parallel-safe backend-isolated slice.
audit#22 F4 — standardize post-commit enqueues on CancellationToken.None:
20 command handlers under MediaCollections/, ProgramSchedules/, Playouts/,
Channels/ threaded the request cancellationToken into work that runs AFTER
SaveChangesAsync commits (WriteAsync rebuild/refresh enqueues, mediator.Publish,
reindex, cache Refresh, and post-commit lookups that gate an enqueue). A late
client-disconnect then turns an already-durable commit into a thrown request AND
drops the side effect. Generalizes the #251 deco-handler fix. Excludes
BuildPlayoutHandler (worker/background token, not a client-disconnect token),
the config/FFmpeg multi-upsert handlers (partial-commit case, separate
follow-up), and response-projection reloads (correctly keep the request token).
audit#22 F2 — DeleteChannelHandler/DeletePlayoutHandler now delete the channel
guide {number}.xml through IFileSystem.File.Delete (observable under
MockFileSystem) and BEFORE the commit (a post-commit delete orphans the xml on a
crash; the xml is regenerable on demand, so pre-commit delete is the safe order).
audit#23 F4 — ReplacePlayoutAlternateScheduleItemsHandler rejects an empty item
list in the handler (not only the controller pre-guard) so a direct caller can't
trip the Max()-on-empty crash.
Docs: api-conventions.md §7a (post-commit token convention + boundaries),
decisions.md entry (rationale, sweep scope, #253 PR2-4 coordination note).
Tests: guide-cache-delete-through-FS for both delete handlers, empty-list guard.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix validation in new form layout
* pin mediatr to last oss version
* update dependencies
* cleanup code in core
* cleanup code in ffmpeg
* cleanup code in infra
* cleanup code in scanner
* cleanup code in application
* cleanup main code
* cleanup test code
* solution-wide code cleanup
* initial work on maintaining playout state
* debugging wip
* fix refresh playout logic
* fix failing test
* more fixes
* update changelog
* comment out some debug logs
* comment out more logs
* clean up genre, tag, studio orphans
* enforce foreign keys at connection level
* wip
* fix fragment scroll offset
* fix see all link for music videos
* add fake artist metadata
* not null artist id
* add artist scanning
* remove improperly named music videos
* code cleanup
* add artists to search results and collections
* clean up music video metadata / artist
* add artist view
* show music videos on artist page
* add music video artwork placeholder
* add music videos library
* add music video tables
* first pass at music video library scan
* support music videos in playouts
* display music videos in search results and collections
* fix music video thumbnails
* remove some obsolete fields