Skip to content

usenet-drain-walk: run the drain down one pass at a time, under a watchdog - #121

Merged
leonardoazeredo merged 2 commits into
mainfrom
feat/usenet-drain-walk
Sep 22, 2026
Merged

leonardoazeredo merged 2 commits into
mainfrom
feat/usenet-drain-walk

Conversation

@leonardoazeredo

Copy link
Copy Markdown
Owner

Why

The drain has been held down since 2026-09-20 (documented in docs/MAINTENANCE.md, "Holding the usenet ingest down"), and the sanctioned way to work it off is hand-running passes and reading each one. At 607 NZBs that stops being practical, and the alternative — arming the timer — is what wedged the box in the first place.

This is that manual procedure with the reading done by a rule.

What it does

  • One bounded pass at a time, in the foreground, at the unit's own settings (--report-failures --max-inflight 6).
  • A progress fingerprint — outbox depth, watch folder contents, staging bytes, state file size and mtime — sampled every 30s. The pass's whole process group is killed when none of it has moved for ten minutes.
  • After each pass, whether the drain got anywhere at all. Four passes in a row that did not ends the walk.
  • Exits 0 when the outbox ends below the mark, 3 when it does not, 1 for a refused precondition, 2 for a bad argument.

Killing a pass moves the walk on rather than repeating it, and nothing has to remember which release was stuck: run() marks last_fetch_attempt before it fetches and the fetch set is least-recently-attempted first, so a killed pass sends its releases to the back. That holds across a SIGKILL, not just a SIGTERM.

What it deliberately never does

  • Never writes logs/usenet-blackhole-state.json — the Python half's file, on a resume contract, and a second writer on it is the coupling trap the pressure gate avoids on purpose.
  • Never deletes an NZB — the outbox is the arrs' work queue.
  • Never arms, stops or disables usenet-blackhole.timer. It refuses to run while that timer is active instead, because two drains on one outbox is the load this exists to avoid.

Verification

  • 16 bats tests (tests/usenet-drain-walk.bats), all passing locally.
  • Mutation corpus entry: the suite is killed by a slack outbox comparison and by a kill that reaches the shell but not the fetch underneath it.
  • Dry run on the NAS against the live tree, resolving the same paths usenet-blackhole.sh prints (a test asserts the two agree, because a driver watching a different folder reads every running pass as stalled):
Mark: outbox below 50 NZBs
  NZB folder:   /volume1/data/usenet/blackhole/nzb
  watch folder: /volume1/data/usenet/blackhole/complete
  staging:      /volume1/data/usenet/blackhole/staging
Outbox now:   607 NZBs
Drain now:    jobs/complete/stalled/outbox = 22 21 1|607

21 of those 22 jobs are complete at TorBox and owed a local fetch, ~99 hours old: nothing has drained since 2026-09-20 21:25.

…chdog

The drain has been held down since 2026-09-20 and the documented way to work
it off is hand-running passes and reading each one. At 607 NZBs that stops
being practical, and the alternative -- arming the timer -- is what wedged the
box in the first place.

This is the manual procedure with the reading done by a rule: one bounded pass
in the foreground, a progress fingerprint (outbox depth, watch folder contents,
staging bytes, state file size and mtime) sampled every 30s, and the pass's
whole process group killed when none of it has moved for ten minutes. Four
passes in a row that moved nothing ends the walk.

Killing a pass moves the walk on rather than repeating it, and nothing here has
to remember which release was stuck: run() marks `last_fetch_attempt` before it
fetches and the fetch set is least-recently-attempted first, so a killed pass
sends its releases to the back. That holds across a SIGKILL.

It never writes the state file (the Python half's, on a resume contract), never
deletes an NZB, and never arms, stops or disables usenet-blackhole.timer -- it
refuses to run while that timer is active instead, because two drains on one
outbox is the load this exists to avoid. It exits 0 when the outbox ends below
the mark and 3 when it does not.

16 bats tests, and a mutation corpus entry: the tests are killed by a slack
outbox comparison and by a kill that reaches the shell but not the fetch, and
the second one is what the group signal is for.
CI caught two guards that a new script is supposed to trip: the scripts tree
in CONTRIBUTING.md and the no-sweep list in tests/mutation/README.md. Both
compare against the tree on disk, so both were correct to fail -- that is the
intended cost of adding a script rather than a reason to widen either list.

usenet-drain-walk.sh gets a line in each, and a description in the tree.
@leonardoazeredo
leonardoazeredo merged commit f286867 into main Sep 22, 2026
10 checks passed
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