Skip to content

fix(downloads): orphan cleanup skipped live empty-queue snapshots, stranding deleted downloads and blocking re-grabs with 409#755

Open
t3chnaztea wants to merge 1 commit into
Listenarrs:canaryfrom
t3chnaztea:fix/orphan-cleanup-empty-live-snapshot
Open

fix(downloads): orphan cleanup skipped live empty-queue snapshots, stranding deleted downloads and blocking re-grabs with 409#755
t3chnaztea wants to merge 1 commit into
Listenarrs:canaryfrom
t3chnaztea:fix/orphan-cleanup-empty-live-snapshot

Conversation

@t3chnaztea

Copy link
Copy Markdown

The bug

Delete a torrent directly in the download client while it is the only active download, and its Download record is stranded forever: every re-grab of that audiobook returns HTTP 409 "A download for this audiobook is already active" with no way to recover except manual removal. Delete one of several torrents and it self-heals, which makes the bug look intermittent.

Root cause

DownloadOrphanCleanupService.RemoveOrphansAsync bailed out whenever the client snapshot returned 0 queue items while downloads were tracked, on the theory that the client might be temporarily unreachable. But by that point unreachability is already fully excluded by the two guards above it (UsedCachedSnapshot and IsUnavailable): DownloadClientQueuePoller surfaces every timeout/cancel/error path as a cached or unavailable snapshot (BuildFallbackQueueResult), never as a live one — snapshotState: "live" is produced only by the success path. So a live empty queue genuinely means the items are gone, and the guard's only real effect was to make the sole-torrent deletion permanently uncleanable.

The fix

Remove the redundant guard so cleanup proceeds on live empty snapshots. The design cares from #640 are preserved unchanged:

  • unreachable clients still short-circuit via the cached/unavailable guards;
  • only Queued/Downloading/Paused are ever terminalized — ImportPending/Ready/ImportBlocked remain untouched;
  • the per-item 5-minute grace period still shields fresh adds.

Tests

  • RemovesOnlyTrackedDownloadWhenLiveSnapshotIsEmpty — the reported scenario: sole tracked download, live empty snapshot, past grace → removed.
  • DoesNotRemoveWhenEmptySnapshotIsNotTrusted (Theory: cached / unavailable) — the Two cleanup paths never wired up: unbounded DownloadProcessingJobs; orphaned downloads never terminalized #640 protection stays intact.
  • DoesNotRemoveRecentDownloadWhenLiveSnapshotIsEmpty — grace period respected.
  • Flipped DoesNotRemoveIdlessDownloadWhenLiveSnapshotIsEmptyRemovesIdlessDownloadWhenLiveSnapshotIsEmpty: the old test encoded the bug behavior.

Full suite green: 1193 passed / 0 failed; dotnet format --verify-no-changes clean.

🤖 Generated with Claude Code

…randing deleted downloads and blocking re-grabs with 409

Deleting a torrent directly in the client when it was the only active
download left its Download record stranded forever, and every re-grab
returned HTTP 409 "already active". RemoveOrphansAsync bailed whenever a
snapshot returned 0 items while downloads were tracked, on the theory the
client might be unreachable, but that condition is already fully excluded
upstream by the UsedCachedSnapshot and IsUnavailable guards: the poller
surfaces every timeout/cancel/error as a cached or unavailable snapshot,
never as a live empty one, so a live empty queue genuinely means the items
are gone. Removing the redundant guard lets cleanup proceed on live empty
snapshots while preserving the Listenarrs#640 protections unchanged: unreachable
clients are still skipped, ImportPending/Ready/ImportBlocked are never
terminalized (only Queued/Downloading/Paused are considered), and the
per-item 5-minute grace period still shields fresh adds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant