Skip to content

fix: race-free yt-dlp update (staging + atomic symlink swap) - #3

Merged
skoelle merged 2 commits into
mainfrom
fix/ytdlp-atomic-update
Oct 10, 2026
Merged

skoelle merged 2 commits into
mainfrom
fix/ytdlp-atomic-update

Conversation

@skoelle

@skoelle skoelle commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Problem

pip install --target /config/ytdlp-lib wrote the live PYTHONPATH directory non-atomically, and the 403 update check (update_ytdlp(wait_for_idle=False)) could start while jobs were still downloading (the wait only covered the current job, discovery not at all). A job could import a half-written library:

ModuleNotFoundError: No module named 'yt_dlp.extractor.youtube' while listing a playlist.

Changes

  • Atomic update: pip installs into <config>/ytdlp-libs/.staging, the version is read from the staged copy, then the install is parked as ytdlp-libs/yt-dlp-<version> and <config>/ytdlp-lib is flipped via a temporary symlink + os.replace. Any concurrent run sees a complete library (old or new); a failed pip run keeps the previous install.
  • Previous install is parked as ytdlp-lib.prev (never deleted, replaced on the next swap), versioned directories rotate to the two newest.
  • Gate while swapping: flag ytdlp_updating – the queue worker waits instead of starting a job, discovery skips (including the manual button) with skipped: updating. A running job finishes; the next one starts against the new library.
  • runner classifies ModuleNotFoundError while listing as an infrastructure error ("yt-dlp installation was being updated, retry") instead of a broken playlist.
  • The 403 path stays wait_for_idle=False (the gate covers it), the daily update keeps its idle wait and the asyncio.Lock.
  • UI: jobs.error_summary is clamped to one line in the job history and playlist tables (ellipsis, full message on hover / in the job log) – long tracebacks no longer break the table layout.

Tests

8 new tests: queue gate, discovery skip, failed pip keeps the old install, successful swap flips the symlink, swap units (park / .prev / rotation), lib_env / path helpers.

ruff check . clean, pytest -q 172 passed, node --check app/static/app.js ok.

Docs: SPEC §6.7 + §14, AGENTS, PLAN decision log, README.

pip --target wrote into the live PYTHONPATH directory non-atomically and
the 403 update check ran while jobs were still downloading, so a job could
import a half-written library (ModuleNotFoundError: yt_dlp.extractor.youtube).

- install into <config>/ytdlp-libs/.staging, park it as yt-dlp-<version>
  and flip <config>/ytdlp-lib with a temp symlink + os.replace; previous
  install parked as ytdlp-lib.prev (never deleted), versions rotate to newest two
- flag ytdlp_updating: the queue worker waits instead of starting a job,
  discovery skips (also force=True) with 'skipped: updating'
- runner classifies ModuleNotFoundError while listing as infra ('was being
  updated, retry'), not as a broken playlist
- 403 path stays wait_for_idle=False (the gate covers it)
- tests: queue gate, discovery skip, failed pip keeps old install, successful
  swap flips the symlink, swap units (park/.prev/rotation)

SPEC 6.7/14, AGENTS, PLAN decision log, README updated. 172 tests green.
Long tracebacks in jobs.error_summary wrapped across rows and broke the
job history layout. Clamp to 40ch with an ellipsis (title shows the full
message, the job log has it anyway). SPEC 14 updated.
@skoelle
skoelle merged commit 0375657 into main Oct 10, 2026
2 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