Skip to content

Rebuild reliability core: SQLite store, durable queue, contained errors - #11

Merged
miridius merged 1 commit into
mainfrom
fix-outage-class-errors
Jul 16, 2026
Merged

Rebuild reliability core: SQLite store, durable queue, contained errors#11
miridius merged 1 commit into
mainfrom
fix-outage-class-errors

Conversation

@miridius

@miridius miridius commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Problem

The bot's job queue, pending confirmations, and video cache were all coordinated through raw files under /storage: job JSONs sorted by filename, pending-download files taken and put back, an info-cache directory with symlink aliases, and videos named by a title-based template with .id sidecar files. Coordinating concurrent access that way kept producing whole classes of user-visible failures:

  • "yt-dlp output file not found" on perfectly good downloads: the bot predicted the output path from scrape-time metadata, and anything that shifted between scrape and download (Instagram carousels, format re-selection, title changes) made it look for a file that was never produced.
  • Races between confirm and cancel, between duplicate requests for one video, and between retries and cleanup, each patched individually as they bit.
  • A restart lost or duplicated in-flight work; failures burned retries on permanently-dead URLs (or gave up on transient ones) and spammed chats with per-attempt noise.
  • Instagram stopped working entirely: yt-dlp's stable channel carries a broken Instagram extractor, and the self-updater tracked stable.

Fix

One embedded bun:sqlite database becomes the durable coordination store (jobs, parked confirmations, an identity-keyed blob index, a URL info cache), so every multi-step state change is a transaction instead of a rename/unlink dance.

  • Blobs are keyed by yt-dlp's stable identity (extractor:id:format, with a URL-salted fallback for identity-less extractors). Downloads write into a private per-download staging dir and the bot takes whatever file actually appeared, so path prediction, and its whole failure class, is gone. Bytes are refcounted: dropped only when nothing references them, kept as a file_id cache after upload.
  • A durable job queue dispatches up to 3 downloads in parallel with at-least-once recovery on boot, exponential-backoff retries for transient failures, and fail-fast classification of permanent ones (live-verified wordings). Each attempt continues one status message instead of spawning threads; groups see only a terminal report.
  • Dup-free shutdown: SIGTERM kills in-flight downloads immediately (they re-run next boot; bytes not yet sent can't duplicate) and drains in-flight sends (a started send completes server-side, verified live), with the process held alive until both queue and inline work finish.
  • yt-dlp self-updates every 5 minutes on the nightly channel (version pre-check against the nightly release tag, update on a copy, atomic rename). Nightly is where extractor fixes land first; the switch is what restores Instagram.
  • Boot and hourly sweeps reconcile storage: legacy-era layouts, orphaned bytes, expired info/pending/handled rows; dev and prod get separate DB/blob/staging paths on the shared volume.
  • Failure reporting is delivery-reliable: flushes retry transiently-failed sends within bounded passes, deterministically-rejected HTML is sanitized in place and repaired in the same flush, and oversize lines are hard-split tag-aware so a chunk can never become permanently unsendable.

Testing

Tests mock only unowned boundaries (Telegram at the fetch layer via a parity-checked MockBotApi, GitHub releases): real SQLite, real filesystem, real child processes via on-PATH stubs. 342 unit tests plus a network-free restart-recovery e2e; ./e2e.sh full exercises real downloads (youtube, reddit, instagram) against the real yt-dlp, self-updated to the channel prod runs. Live QA against the dev bot covered the happy path, group confirmations, permanent/transient error classification, and mid-download/mid-upload SIGTERM.

The .claude workflow now enforces review deterministically: commit and merge gates verify content-bound approvals minted only by fresh attestation agents.

🤖 Generated with Claude Code

@miridius miridius changed the title Fix outage-class error handling (baseline review) Stop slow downloads and handler errors from silently killing the bot Jun 11, 2026
@miridius miridius changed the title Stop slow downloads and handler errors from silently killing the bot Stop slow downloads and handler errors from crash-looping the bot Jun 11, 2026
@miridius miridius changed the title Stop slow downloads and handler errors from crash-looping the bot Durable job queue with parallel downloads; contain handler errors; workflow gates Jun 12, 2026
@miridius
miridius force-pushed the fix-outage-class-errors branch from c019d33 to cbca4d8 Compare July 16, 2026 06:10
@miridius miridius changed the title Durable job queue with parallel downloads; contain handler errors; workflow gates Rebuild reliability core: SQLite store, durable queue, contained errors Jul 16, 2026
Replaces the filesystem-as-a-database layer (job files, pending files, an
info-cache dir with symlink aliases, title-named videos with .id sidecars)
with one embedded bun:sqlite database — tables for jobs, parked
confirmations, an identity-keyed blob index, and the URL info cache — so
every multi-step state change is a transaction instead of a race-prone dance
of renames and unlinks. Video bytes move to /storage/blobs, named by yt-dlp's
stable identity (extractor:id:format, length-capped; salted filename
fallback) and refcounted through the DB: bytes drop only when nothing
references them and no telegram file_id makes them redundant. Dev and prod
share the volume but get separate DB/blob stores (file_ids are per-bot).

Message handlers now persist a job row and return; a worker pool downloads up
to 3 in parallel with at-least-once recovery on boot, exponential-backoff
retries for transient failures, and fail-fast classification of permanent
ones — each attempt edits one status message instead of spawning threads.
bot.catch contains per-update errors so the polling loop survives them, and
a fatal polling crash exits deliberately for docker to restart.

yt-dlp self-updates every 5 minutes (copy + --update + atomic rename, gated
by a GitHub release-tag pre-check so an up-to-date tick costs one API call,
not a 35MB copy). Cached scrape info expires after 6h — its signed media
URLs rot — and is evicted on download failure so retries re-scrape. Long
videos in groups ask before downloading; the real duration is probed once
post-download and stored, so the gate holds even for metadata that lies and
bytes long since uploaded and disposed. Private-chat message edits no longer
re-send the same video (URL-level dedup per message). The first boot after
the upgrade sweeps the dead pre-SQLite /storage layout.

Tests mock only unowned boundaries (Telegram at fetch, GitHub releases):
real SQLite, real filesystem, real child processes via PATH stubs, and a
restart-recovery e2e over real persistence. The .claude workflow now
enforces review deterministically: commit and merge gates verify a
content-bound approval minted by fresh attestation agents, so an unreviewed
or re-staged change cannot reach main.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@miridius
miridius force-pushed the fix-outage-class-errors branch from cbca4d8 to 8d74c8a Compare July 16, 2026 06:20
@miridius
miridius merged commit aed3e6c into main Jul 16, 2026
3 checks passed
@miridius
miridius deleted the fix-outage-class-errors branch July 16, 2026 07:33
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