Fix SABnzbd downloads completing with no path set (race between active queue and history) - #840
Fix SABnzbd downloads completing with no path set (race between active queue and history)#840lisim wants to merge 1 commit into
Conversation
SABnzbd briefly reports an item as status "Completed" in its active
queue before archiving it to history with the real storage path.
MapQueueSlotToQueueItem mapped that slot into a completed QueueItem
regardless, which let QueueItemConverter mark the download Completed
with an empty DownloadPath - permanently blocking import with
"Inconsistency: Download {id} has no path set", since nothing ever
backfills the path afterward.
When status is "completed" but no storage path is present yet, return
null instead of a pathless completed item. Excluding it from the
active-queue result set makes the poller treat the download as
missing, which triggers a same-cycle history lookup - and history
reliably has the storage path by then. This resolves the download via
the code path that already works correctly, rather than completing
early on unreliable telemetry.
Fixes Listenarrs#839.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
|
Heads up for review: this touches the same method as #759 ( No functional overlap that I can see — #759 is about tracked jobs getting hidden by category mismatch, this PR is about the active-queue/history completion race — but wanted to flag the proximity since I noticed it late (after already opening this PR, not before). |
Summary
Fixes #839.
SABnzbd downloads occasionally get marked
Completedwith an emptyDownloadPath, which permanently blocks import:DownloadProcessingJobProcessor.ProcessJobAsyncthrowsInconsistency: Download {id} has no path setand nothing ever retries with a corrected path afterward — the download just sitsImportBlockedforever. Hit this repeatedly (6 different releases) across a single session of grabbing several audiobooks in quick succession.Root cause: SABnzbd briefly reports an item as
status: "Completed"in the active queue (mode=queue) before archiving it to history (mode=history) with the realstoragepath.SabnzbdResponseMapper.MapQueueSlotToQueueItem's own existing comment already acknowledges this ("Active SABnzbd queue slots do not reliably expose the completed storage path") but the method still mapped that slot into aQueueItemwithStatus = "completed"regardless.QueueItemConverter.UpdateFromQueueItemthen seesnormalizedState == "completed"and callsdownload.Completed()immediately, before a path was ever set (download.DownloadPathis only assignedif (!string.IsNullOrEmpty(item.LocalPath))). OnceStatus == Completed, nothing backfills the path — the download is stuck.Changes
Fixed
SabnzbdResponseMapper.MapQueueSlotToQueueItem: when the mapped status is"completed"and there's no explicitstoragepath on the slot, returnnullinstead of a pathless completedQueueItem.This isn't a dead end for that download — excluding it from the active-queue result set makes
SabnzbdQueueFetchWorkflow.GetMissingTrackedIdstreat it as missing from the queue, which setshistoryRequired = trueand triggers a history lookup in the same poll cycle. History reliably has the realstoragepath by the time an item shows up there, so the download resolves correctly viaMapHistorySlotToQueueItem(the code path that already works), instead of completing early on unreliable telemetry.Added
SabnzbdResponseMapperTests.cs— three focused unit tests directly against the static mapper (no DI/database/filesystem needed):MapQueueSlotToQueueItem_CompletedStatusWithoutStorage_ReturnsNull— the regression case.MapQueueSlotToQueueItem_CompletedStatusWithStorage_ReturnsCompletedItem— a real storage path still resolves normally, unaffected.MapQueueSlotToQueueItem_DownloadingStatusWithoutStorage_StillReturnsItem— the guard only targets"completed"; genuinely in-progress items (which never carrystorage) are unaffected.Testing
dotnet test tests/Listenarr.Tests.csproj --filter 'FullyQualifiedName~SabnzbdResponseMapperTests'— 3/3 pass on this branch.canaryHEAD —MapQueueSlotToQueueItem_CompletedStatusWithoutStorage_ReturnsNullfails there (Assert.Null() Failure: Value is not null), the other two pass on both. So this isn't a test that would have passed regardless of the fix.dotnet build listenarr.slnx— clean, 0 warnings, 0 errors.dotnet format listenarr.slnx --no-restore --verify-no-changes --include <the 2 changed files>— clean, no formatting changes needed.These tests are plain static-method unit tests (no
BaseTests/DI/EF/filesystem semantics involved), so they run cleanly in a sandboxed container without hitting theIFileSystemSemanticsResolverenvironment limitation I ran into on my previous PR (#837).Review coverage (per
.github/AGENTS.md)Related
I also opened #838 (missing blocklist/blocklist feature) from the same session — unrelated root cause, separate issue, not addressed by this PR.