Scan lifecycle honesty: 200-on-dropped-scan, non-finally unlocks, unguarded enqueue (reviewer#20 F1+F2+F6) #232
Closed
opened 2026-07-11 00:55:14 +02:00 by timothy
·
3 comments
No Branch/Tag Specified
main
renovate/meziantou.analyzer-3.x
renovate/nuget-sixlabors.imagesharp-vulnerability
913-scripted-playout-401
929-vuln-pkcs-imagesharp-v4
renovate/dotnet-monorepo
release/v26.15.0-notes
renovate/lucene.net
renovate/cliwrap-3.x
issue-806-guard-populations
scratch/767b-poisoned
scratch/767b-control
release/v26.14.0-notes
release/v26.14.0
renovate/sqlitepclraw.bundle_e_sqlite3-3.x
docs/510-skill-logo-bug-policy
fix/510-watermark-resolution-policy
fix/629-verdict-classifier-falseopens
fix/609-decisions-edit-token-scope
issue-135-clear-to-none
release/v26.12.0-notes
fix/409b-lastscan-api-parity
fix/401-updatechannel-mirror-422
fix/327-playlist-rename-validation
fix/410-scancancel-log-level
fix/409-447-librariesscreen-neverscanned
fix/338-zap-exit-code
fix/367-plex-budget-message
fix/310-debom-legacy-cs
ci/604-lane-rebalance
feat/388-design-mirror
feat/247-test-ownership
feat/247-primary-action
feat/357-player-owned-playback
feat/357-jellyfin-plugin-poc
fix/289-mcp-hardening
issue58-mcp
feat/244-channels-extract
ci/auto-bump-prod-compose
feat/multi-rerun-collections-api
feat/collections-api
feat/quick-wins
feat/185-docs-part2
feat/140-collections-screen
feat/146-channel-edit
feat/147-classic-ui-link
issue22-renovate-dashboard
feat/91-cutover
feat/63-composite-create
feat/65-library-browse
feat/85-epg
feat/86-schedule-editor
feat/109-dashboard-data
feat/99-session-tracking
fix/dockerfile-node-tag
feat/59-spa-foundation
docs/59-ui-redesign-brief
feat/102-json-guide
feat/111-schedule-durations
feat/104-artwork-upload
feat/103-media-sources-api
feat/playouts-read-api
feat/108-health-api
feat/105-picker-list-endpoints
issue-97-channel-state-api
issue42-jellyfin-musicvideos
issue46-rest-api-error-contract
dependabot/nuget/ErsatzTV.FFmpeg.Tests/multi-d307a2e06f
qsv-improvements
hdr-vulkan-cuda-test
v26.16.0
v26.15.0
v26.14.0
v26.13.0
v26.12.0
v26.11.0
v26.10.0
v26.9.0
v26.8.0
v26.7.0
blazor-final
v26.6.0
v26.5.0
v26.4.0
v26.3.1
v26.3.0
v26.2.0
v26.1.1
v26.1.0
v25.9.0
v25.8.0
v25.7.1
v25.7.0
v25.6.0
v25.5.0
v25.4.0
v25.3.1
v25.3.0
v25.2.0
v25.1.0
v0.8.8-beta
v0.8.7-beta
v0.8.6-beta
v0.8.5-beta
v0.8.4-beta
v0.8.3-beta
v0.8.2-beta
v0.8.1-beta
v0.8.0-beta
v0.7.9-beta
v0.7.8-beta
v0.7.7-beta
v0.7.6-beta
v0.7.5-beta
v0.7.4-beta
v0.7.3-beta
v0.7.2-beta
v0.7.1-beta
v0.7.0-beta
v0.6.9-beta
v0.6.8-beta
v0.6.7-beta
v0.6.6-beta
v0.6.5-beta
v0.6.4-beta
v0.6.3-beta
v0.6.2-beta
v0.6.1-beta
v0.6.0-beta
v0.5.8-beta
v0.5.7-beta
v0.5.6-beta
v0.5.5-beta
v0.5.4-beta
v0.5.3-beta
v0.5.2-beta
v0.5.1-beta
v0.5.0-beta
v0.4.5-alpha
v0.4.4-alpha
v0.4.3-alpha
v0.4.2-alpha
v0.4.1-alpha
v0.4.0-alpha
v0.3.8-alpha
v0.3.7-alpha
develop
v0.3.6-alpha
v0.3.5-alpha
v0.3.4-alpha
v0.3.3-alpha
v0.3.2-alpha
v0.3.1-alpha
v0.3.0-alpha
v0.2.5-alpha
v0.2.4-alpha
v0.2.3-alpha
v0.2.2-alpha
v0.2.1-alpha
v0.2.0-alpha
v0.1.5-alpha
v0.1.4-alpha
v0.1.3-alpha
v0.1.2-alpha
v0.1.1-alpha
v0.1.0-alpha
v0.0.62-alpha
v0.0.61-alpha
v0.0.60-alpha
v0.0.59-alpha
v0.0.58-alpha
v0.0.57-alpha
v0.0.56-alpha
v0.0.55-alpha
v0.0.54-alpha
v0.0.53-alpha
v0.0.52-alpha
v0.0.51-alpha
v0.0.50-alpha
v0.0.49-prealpha
v0.0.48-prealpha
v0.0.47-prealpha
v0.0.46-prealpha
v0.0.45-prealpha
v0.0.44-prealpha
v0.0.43-prealpha
v0.0.42-prealpha
v0.0.41-prealpha
v0.0.40-prealpha
v0.0.39-prealpha
v0.0.38-prealpha
v0.0.37-prealpha
v0.0.36-prealpha
v0.0.35-prealpha
v0.0.34-prealpha
v0.0.33-prealpha
v0.0.32-prealpha
v0.0.31-prealpha
v0.0.30-prealpha
v0.0.29-prealpha
v0.0.28-prealpha
v0.0.27-prealpha
v0.0.26-prealpha
v0.0.25-prealpha
v0.0.24-prealpha
v0.0.23-prealpha
v0.0.22-prealpha
v0.0.21-prealpha
v0.0.20-prealpha
v0.0.19-prealpha
v0.0.18-prealpha
v0.0.17-prealpha
v0.0.16-prealpha
v0.0.15-prealpha
v0.0.14-prealpha
v0.0.13-prealpha
v0.0.12-prealpha
v0.0.11-prealpha
v0.0.10-prealpha
v0.0.9-prealpha
v0.0.8-prealpha
v0.0.7-prealpha
v0.0.6-prealpha
v0.0.5-prealpha
v0.0.4-prealpha
v0.0.3-prealpha
v0.0.2-prealpha
v0.0.1-prealpha
Labels
Clear labels
ad-hoc
api
bug
ci-cd
content
dependencies
enhancement
frontend
in-progress
jellyfin
parked
priority: high
priority: low
priority: medium
review
security
One-off / ad-hoc work not tracked by a dedicated issue
REST API / HTTP endpoints
Something isn't working
Build, test, deploy pipeline
Channel content / schedules / playlists
Dependency updates (Renovate)
New feature or improvement
ChicoryTV React SPA frontend
Claimed by an active session — do not pick up
Jellyfin tuner / IPTV integration
Excluded from automatic queue pickup; work only when explicitly selected
Adversarial review finding
Security / vulnerability fix
Milestone
No items
No Milestone
Projects
Clear projects
No projects
No Assignees
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: timothy/ersatztv#232
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
From audit adversarial-reviewer#20, findings F1 (Blocker) + F2 (Blocker) + F6 (Medium) + F5.3. Builds on the ownership model from #231.
Defects
F1 — API lies about acceptance.
QueueLibraryScanByLibraryIdHandler.cs:46-82:return truesits outsideif (locker.LockLibrary(...))→ a held lock skips all enqueueing and still returnstrue→LibrariesController.cs:24-27maps to 200 OK. Sync-disabled returns the samefalseas missing → both 404. Repro: POST/api/libraries/{id}/scantwice quickly → both 200, second did nothing. The SPA papers over this withPENDING_GRACE_TICKS(web/src/api/libraries.ts:31-34).F2 — ScannerService strands locks on handler exception. Every unlock in
ErsatzTV/Services/ScannerService.cssits afterawait mediator.Send(...), not infinally(:129-132, :179-182, :242-245, :292-295, :369-372libraries;:208-211, :321-324, :398-401collections). A thrown exception jumps to the loop catch (:89-92) — the lock leaks until restart; scheduler then skips that library every cycle and the API keeps answering 200.F6 — unguarded lock→enqueue window. Only
TraktController.EnqueueWithTraktLock(TraktController.cs:170-192) guards the enqueue.QueueLibraryScanByLibraryIdHandler:46-79,CreateLocalLibraryHandler:45,UpdateLocalLibraryHandler:97,Libraries.razor:222-273,RemoteMediaSourceLibrariesEditor.razor:129,UpdateTraktListHandler:56-58,SchedulerServicescan loops all lock thenWriteAsyncbare — a cancelled token between lock and write leaks; for the Plex/Jellyfin/Emby two-message batches, cancellation between the writes half-enqueues and the lock is never released.F5.3 — batch early-unlock. Scheduler Plex Shows: one lock, two messages (
SchedulerService.cs:230-241);ScannerServiceunlocks after the first → networks-sync runs unlocked.Task
Queued | NotFound | SyncDisabled | AlreadyScanning); controller maps 202 / 404 / 422 / 409 (ProblemDetails for 409/422).try/finallyaround eachmediator.Send+ unlock in all eight ScannerService sites.EnqueueWithTraktLockpattern).web/src/api/libraries.ts), OpenAPI regen (./scripts/update-openapi.sh+npm run generate:api),OpenApiErrorResponseContractTestscases for the new 404/409/422.Required tests (evidence, not optional)
LockLibraryin the queue handler → assert released.claiming — session started 2026-07-11 (same session as #231; this lands on top of #231's ownership model).
Working this as one PR with #231 (
batch/231-232-lock-soundness). The ownership-model contract #232 implements is decided in #231's plan comment: tokenless atomic flag + single-owner release discipline (atruefromLock*= ownership of exactly one release; compensating unlock on enqueue failure per theEnqueueWithTraktLockpattern). #232 scope: outcome enum → 202/404/422/409,try/finallyunlocks in all 8 ScannerService sites, guarded enqueues, batch release-safety, SPAPENDING_GRACE_TICKSremoval, OpenAPI regen + contract tests.Closed via PR #241 (merged to main
c42928e8)What was done
QueueLibraryScanByLibraryIdHandlerreturnsQueueLibraryScanResult { Queued | NotFound | SyncDisabled | AlreadyScanning }instead of a bool that returnedtrueeven when a held lock dropped the scan.LibrariesControllermaps → 202 / 404 / 422 / 409 (ProblemDetails on errors), with[ProducesResponseType]for each.ScannerServiceunlock sites moved intotry/finally; compensating unlock-on-enqueue-failure added to the lock→enqueue producers (queue handler, Create/UpdateLocalLibrary, UpdateTraktList) per theEnqueueWithTraktLockpattern.WriteAsyncthrows (cancellation) between lock and enqueue.Unlockflag; only the trailingSynchronizePlexNetworksreleases), so the lock now spans both units.triggerScanhandles 202/409/404/422 honestly.Root cause — the handler's
return truesat unconditionally inside the library-exists branch, collapsing not-found / sync-disabled / already-scanning / queued into a single truthy result; and everyScannerServiceunlock sat afterawait mediator.Sendrather than infinally, so a handler exception stranded the lock until restart. The SPAPENDING_GRACE_TICKSexisted only to paper over the resulting dishonest 200.Files:
QueueLibraryScanByLibraryId(.cs/Handler.cs),Create/UpdateLocalLibraryHandler.cs,UpdateTraktListHandler.cs,Plex/Commands/{SynchronizePlexLibraryById,SynchronizePlexNetworks}.cs,LibrariesController.cs,ScannerService.cs,SchedulerService.cs,web/src/api/libraries.ts, generatedv1.json, docsapi-conventions.md§3b; testsQueueLibraryScanByLibraryIdHandlerTests.cs,LibrariesControllerTests.cs,OpenApiErrorResponseContractTests.cs,web/src/App.test.tsx.Adversarial review (fork + Codex) found 3 issues, all fixed in-PR (commit
702b0121):try/catch → UnlockLibrary → rethrow.Deferred: Blazor scan producers (
Libraries.razor,RemoteMediaSourceLibrariesEditor.razor) share the leak shape but are being retired in #91 phase (b) — flagged, not expanded into scope. Docs updated:api-conventions.md§3b (outcome-enum→status mapping + compensating-unlock rule), OpenAPI regenerated.Verification: solution 0 errors ·
ErsatzTV.Tests1051 ·ErsatzTV.Infrastructure.Tests53 · web 581 · typecheck +check:apiclean · CI green on702b0121.timothy referenced this issue2026-07-11 15:44:36 +02:00