Skip to content

fix(import-watcher): stop *arr import floods (backoff + give-up)#63

Merged
roziscoding merged 5 commits into
mainfrom
fix/import-watcher-item-reconcile
Jul 4, 2026
Merged

fix(import-watcher): stop *arr import floods (backoff + give-up)#63
roziscoding merged 5 commits into
mainfrom
fix/import-watcher-item-reconcile

Conversation

@roziscoding

@roziscoding roziscoding commented Jul 4, 2026

Copy link
Copy Markdown
Owner

Stops the import watcher from flooding a destination Radarr/Sonarr when an import_queued row can't be confirmed as imported.

Problem

A jack_manual row re-fired ManualImport at *arr on every tick when it couldn't be confirmed imported — either because the import aged out of *arr's recent-history window, or because the trigger itself kept failing (e.g. *arr returns 500 because the movie's library folder is missing). That hammered the destination indefinitely.

Approach

Try to import; if *arr doesn't report success after a few attempts, mark the row failed and move on. Success and failure are read from unambiguous signals only:

  • Success: *arr's import history (recentlyImportedDownloadIds) or the tracked manual-import command status (we poll the exact command id we created — no fuzzy matching).
  • Failure / give-up: per-row exponential backoff on a failing trigger, and after maxManualImportAttempts the row is marked failed with a clear error. In-memory backoff state (a restart is a fair signal to retry). New config (all defaulted): maxManualImportAttempts (6), manualImportBackoffBaseMs (60s), manualImportBackoffMaxMs (30m).

What changed vs the first cut

An earlier revision added an item-level reconciliation — asking *arr whether the target movie/episode already held a file "matching" the release via size / scene-title / release-group+quality. That heuristic was dropped: the coarse group+quality fallback (and the size-only match across a series) could retire a row when the item held a different release of the same tier (e.g. a proper/repack upgrade), silently skipping the import we actually wanted. Confirmation now comes only from history + command status; anything unconfirmed after N attempts fails and is left for future user-driven recovery actions.

Tests

bun test green. Watcher tests cover: history-hash flip, single push while absent from history, no re-trigger after restart once a command id is persisted, backoff (no re-fire every tick), and give-up (marks failed after N). Removed the matcher/item-reconcile tests along with the code.

Requires the gitignored generated schema types (bun run generate in packages/schemas) for typecheck — unchanged from current CI expectations.

Greptile Summary

This PR updates the import watcher to stop repeated manual import pushes. The main changes are:

  • Adds retry limits and exponential backoff for failed manual import triggers.
  • Wires new downloads config defaults into the backend watcher.
  • Adds tests for retry, backoff, and give-up behavior.

Confidence Score: 5/5

This looks safe to merge.

  • No blocking issues found in the changed code.

Reviews (5): Last reviewed commit: "refactor(import-watcher): drop item-leve..." | Re-trigger Greptile

Context used (5)

  • Context used - packages/schemas/CLAUDE.md (source)
  • Context used - CLAUDE.md (source)
  • Context used - Use Bun instead of Node.js, npm, pnpm, or vite. (source)
  • Context used - apps/backend/CLAUDE.md (source)
  • Context used - AGENTS.md (source)

The watcher only recognized an import once its stub infohash appeared in
*arr's recent-history query. That history is time-windowed, so an import
that completed long ago falls out of it and the row never leaves
import_queued — and because manualImportCommandId stays null, every tick
re-fires ManualImport at the destination, flooding it.

Add an item-level check: ask *arr whether the target item (movieId /
seriesId) already holds a file matching the release, and if so retire the
row as imported instead of re-triggering. Matching prefers identical byte
size (a move import never rewrites the file), then a normalized scene-title
match, then release-group + quality as a last resort, so a different file
on the same item won't false-match.
Comment thread apps/backend/src/lib/servers/arr/base.ts Outdated
Comment thread apps/backend/src/lib/servers/arr/sonarr.ts Outdated
Matching on release group + quality tier could retire a queued row when
the target item merely held a *different* file of the same tier — a
different edition of the same movie, or (via Sonarr's whole-series scan) a
sibling episode. Both would skip the manual import for a release that is
still missing.

Keep only the per-file signals: byte-identical size (primary) and a
normalized scene-title match (fallback). This leaves the whole-series scan
safe, since neither signal collides across distinct files.
Comment thread apps/backend/src/lib/servers/arr/sonarr.ts Outdated
Reverts dropping the group+quality fallback — that signal is fine. The real
hazard was importedReleasesFor returning every on-disk episode of a series,
which let the coarse fallback match a *different* episode and retire the row
before its own episode imported.

importedReleasesFor now takes the queued release and Sonarr narrows to the
release's own season/episode (empty when it can't be determined, so the
normal history/command flow drives the import). Matching stays per-item.
Comment thread apps/backend/src/lib/servers/arr/base.ts Outdated
A jack_manual trigger that keeps failing — e.g. *arr returns 500 because the
movie's library folder is missing — was re-fired on every tick, flooding the
destination. The watcher now tracks per-row trigger failures: it backs off
exponentially between attempts (base/cap configurable) and, after
maxManualImportAttempts, marks the row failed so it stops.

State is in-memory (a restart is a fair signal to try again). The history and
item-level checks still run each tick during back-off, so a genuine import is
picked up the moment *arr gains the file. New config: maxManualImportAttempts
(6), manualImportBackoffBaseMs (60s), manualImportBackoffMaxMs (30m).
@roziscoding roziscoding changed the title fix(import-watcher): reconcile queued imports against the target item fix(import-watcher): stop *arr import floods (item reconcile + backoff) Jul 4, 2026
Remove hasImportedRelease / importedReleasesFor / releaseFilesMatch and the
watcher's item-level "does *arr already hold this release?" check. The fuzzy
size/title/group+quality matching was error-prone (it could retire a row when
the item held a *different* release of the same tier).

The watcher now relies on the two unambiguous signals: *arr's import history
(recentlyImportedDownloadIds) and the tracked manual-import command status for
success, and the backoff + give-up path to mark a row failed after N attempts.
A stuck import simply fails and moves on, to be resolved by (future) user
actions rather than guessed at here.
@roziscoding roziscoding changed the title fix(import-watcher): stop *arr import floods (item reconcile + backoff) fix(import-watcher): stop *arr import floods (backoff + give-up) Jul 4, 2026
@roziscoding roziscoding merged commit 1d6100d into main Jul 4, 2026
10 checks passed
@roziscoding roziscoding deleted the fix/import-watcher-item-reconcile branch July 4, 2026 08:23
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