--- key: api.postcommit-cancellation-none title: '2026-07-11 — Post-commit side effects run on `CancellationToken.None` (generalized from #251 to #254)' status: active since: '2026-07-11' supersedes: none superseded-by: none rule: Once a mutation commits, its entire compensating side effect (enqueues, publishes, reindexes, cache refresh, and any post-commit lookup gating one of those) runs on `CancellationToken.None` so a late client disconnect can't half-abort an already-committed change. signals: '`WriteAsync` enqueue, `mediator.Publish`, search-index reindex · paths: `docs/api-conventions.md` §7b, `MediaCollections/`, `ProgramSchedules/`, `Playouts/`, `Channels/` handlers · issues: #251, #254, adversarial-reviewer#22' mechanics: '`docs/api-conventions.md` §7b' --- Audit #22 (adversarial-reviewer) found ~20 command handlers threading the request `cancellationToken` into work that runs **after** `SaveChangesAsync` commits — the post-commit `WriteAsync` enqueue that rebuilds/refreshes the affected entity, `mediator.Publish`, search-index reindex, cache refresh. A late HTTP-client disconnect cancels that token, so the *already-committed* mutation throws on the way out **and silently drops its side effect** (the playout rebuild is never queued → the persisted edit never takes visible effect until a manual Reset). #251 fixed this for the deco handlers; #254 generalizes the policy across the codebase. **Decision.** Once a mutation has committed, the *entire* compensating side effect — enqueues, publishes, reindexes, cache refreshes, and any post-commit lookup that **gates** one of those enqueues — runs on `CancellationToken.None`. The commit is the point of no return; past it the side effect must not be half-abortable. Full convention + the two boundaries in `docs/api-conventions.md` §7b. **Scope of the #254 sweep (this PR).** Swept the single-`SaveChanges` handlers under `MediaCollections/`, `ProgramSchedules/`, `Playouts/`, `Channels/` (20 handlers). Deliberately **excluded**: - **`BuildPlayoutHandler`** — a background/worker handler; its token is the worker shutdown token, not a client-disconnect token, so its downstream enqueues *correctly* honor cancellation. - **`UpdateFFmpegSettingsHandler` + the two `Configuration/` settings handlers** — they commit via several sequential `IConfigElementRepository.Upsert` calls with an interleaved enqueue; "when is it committed" is a partial-commit-under-cancellation question broader than the clean single-`SaveChanges` F4 pattern. Left for a separate follow-up. - **Response-projection reloads** (`ReplaceProgramScheduleItemsHandler` / `AddProgramScheduleItemHandler` post-commit graph reload that builds the *returned* view model) keep the request token — a cancelled response after a durable commit + `None`-enqueue loses nothing. - Handlers a no-token `WriteAsync()` already makes behaviorally correct (`default` == `None`) were left alone (explicit-`None` there is cosmetic). Also folded in the two other #254 items on the same handlers: the channel-guide `{number}.xml` delete in `DeleteChannelHandler`/`DeletePlayoutHandler` now routes through `IFileSystem.File.Delete` (observable under `MockFileSystem`) **before** the commit (a post-commit delete orphans the xml on a crash; the guide xml is regenerable on demand, so a pre-commit delete is the safe ordering), and `ReplacePlayoutAlternateScheduleItemsHandler` now rejects an empty item list in the handler (not only at the controller pre-guard) so a direct caller can't trip the `Max()`-on-empty crash. **Coordination note for #253 PR2–PR4.** Those PRs add `Version++` (pre-commit) to the mutating handlers of the versioned aggregates — several of which this sweep also touched (post-commit token, a different line region). Low git-conflict risk, but merge `main` in and expect to see the `CancellationToken.None` convention already present on the post-commit enqueues.