Skip to content

Scale audio timeouts with the file, confirm orphans before deleting - #84

Open
ttlequals0 wants to merge 4 commits into
mainfrom
fix/audio-decode-timeout-scaling
Open

Scale audio timeouts with the file, confirm orphans before deleting#84
ttlequals0 wants to merge 4 commits into
mainfrom
fix/audio-decode-timeout-scaling

Conversation

@ttlequals0

@ttlequals0 ttlequals0 commented Sep 9, 2026

Copy link
Copy Markdown
Owner

Why

Two bugs found on a live library.

Large mp3s were never validated. Audio ran three full-file passes on fixed deadlines (120s decode, 120s demux, 60s flac -t) while video scaled its deadline with size and duration, so a two-hour episode timed out and was recorded as a warning instead: 159 of the first 400 warning files on one library, every one over 62 MB.

Orphan cleanup could not finish. It judged a whole run by the share of it that read as missing, so cleaning up records for 331 files the operator had deleted aborted at 96 percent, deleted nothing, and the UI reported "Removed 331 orphaned records" anyway. The same test missed a mount dropping under a scan path that holds several, where the share stays low.

What changed

  • Video and audio compute their deadline through one helper with their own coefficients; video deadlines are unchanged. The audio passes share one per-file budget, and later passes are skipped once it is spent rather than started and killed. New tunable timeouts.audio_decode_base_secs.
  • Cleanup confirms each file by reading its own directory: other files in it prove the storage is present, so the record goes. An empty or missing directory is kept and counted, since an unmounted mountpoint reads the same. Flagged files are re-checked before deletion, so a mount that dropped mid-sweep and returned cannot have its library deleted.
  • Kept records are the operator's call: the UI offers to remove them and the API takes trust_unreadable_dirs, never set for a scheduled run, bounded by what the previous run kept.
  • A run that stops early reports its reason as an error, writes a report, and no longer renders the orphan count as a deletion count. New column cleanup_states.records_kept.
  • 2.8.11 lets the library above a vanished folder answer for it. A library with one film per folder had nothing left to read once a film was deleted, so every record was kept for confirmation: 44 of 46 flagged files in one real run. The parent now has to list folders with a recorded file inside one of them, which leftovers on an unmounted mountpoint cannot fake.
  • 2.8.10 fixes that column's migration, which named cleanup_states where the table is cleanup_state. Migrations here log and swallow failures, so the column never arrived while the model kept selecting it, and a deployed 2.8.9 returned 500 on every cleanup-status poll. The name now comes from the model, and a test rejects any migration naming a table the models do not define.

Verified

  • Full suite: 684 passed, 9 skipped. 25 new tests cover both fixes, including an unmounted mountpoint, a returned file, a partial outage, the migration-name guard, and leftovers on an unmounted mountpoint failing to answer.
  • Video deadlines checked identical to the old inline formula (1 GB/1 h = 2520 s, 30 GB/2 h = 6840 s).
  • Images 2.8.9 and 2.8.10 built for linux/amd64 and pushed. Trivy: 4 CRITICAL / 87 HIGH, none with an available fix, against 5 / 156 in the running 2.8.8; no CVE is introduced by this change.
  • Deployed and confirmed live at 2.8.10 (2.8.11 not yet built): /api/version reports 2.8.10, /api/cleanup-status serves records_kept and error_message, and the new tunable appears under Timeouts.

…v2.8.9)

Audio validation ran three full-file passes on fixed deadlines (120s, 120s,
60s) while the video path scaled its deadline with size and duration, so a
two-hour episode timed out and was recorded as a warning without ever being
validated: 159 files on one library. Video and audio now compute the deadline
through one helper, and the audio passes share a single per-file budget.

Orphan cleanup judged a whole run by the share of it that read as missing,
which aborted a cleanup of records for files the operator had deleted and
missed a mount dropping under a scan path holding several. Each file is now
confirmed by reading its own directory: other files in it prove the storage is
present. An empty or missing directory is kept and counted instead, since an
unmounted mountpoint reads the same, and the operator can confirm those.

An aborted run also reported itself as a success, with the orphan count
rendered as a deletion count and the reason unread in the status payload.
The v2.8.9 migration altered 'cleanup_states'; the table is 'cleanup_state'.
Migrations here log and swallow their failures, so the column never arrived
while the model kept selecting it, and /api/cleanup-status returned 500 on
every poll of a deployed 2.8.9. A test now rejects any migration naming a
table the models do not define.
A library with one file per folder could not clean up anything: deleting a
film empties or removes its folder, leaving nothing in it to read, so every
record was kept for confirmation. On a real library that was 44 of 46 flagged
files in one run.

When a folder has nothing to say, the question moves up a level. The parent
has to list folders and one of those has to hold a file this scanner recorded,
which is the library answering rather than leftovers on an unmounted
mountpoint. A tree that has gone entirely still has nothing above it to answer.

Also corrects the completion message, which called kept records ones whose
folder could not be read; a folder emptied by a deletion reads perfectly well.
Three issues found in production logs.

The cross-worker start lock read db.session.bind, which is None unless a
session was bound explicitly, so every call raised and the handler's
log-and-proceed fallback waved through every caller it ever saw.

A schedule skipped by a running scan spent its retry budget while waiting:
144 retries ten minutes apart is one day, and a large scan runs longer. Live,
a 27-hour scan had one schedule at retry 106 of 144. Waiting on a scan that is
reporting progress no longer counts; a scan quiet for half an hour still does.

Two INFO lines on /api/scan-status were 42% of all output, on an endpoint each
open tab polls every few seconds, against a log table already at a million
rows. Both are DEBUG now.
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