fix(sync): R5 S2 — sync guards

- never prune subscriptions on an empty fetch: one anomalous empty-but-200
  response wiped every subscription (and its deep_requested flag) in one run
- push_playlist: stop after 5 consecutive write failures — a quota-dead or
  scope-revoked account otherwise burns 50 units per remaining item
- RSS: raise RssError instead of returning [] on a transport/HTTP failure, so a
  failed poll no longer stamps last_rss_at and a 404 feed stops looking healthy
- worker: exit when the schema never appears instead of "continuing" into a
  crash one call later
This commit is contained in:
2026-07-23 22:36:33 +02:00
parent 2f46947dcf
commit 4209f6bb24
6 changed files with 150 additions and 18 deletions
+18 -8
View File
@@ -93,24 +93,33 @@ def _parse_upload_date(raw) -> date | None:
# --- claim + recovery -----------------------------------------------------------------------
def _wait_for_schema(timeout: float = 300.0) -> None:
"""Block until the download tables exist before doing any DB work.
def _wait_for_schema(timeout: float = 300.0) -> bool:
"""Block until the download tables exist before doing any DB work. Returns False if they never
appeared (or we were asked to stop while waiting).
The API applies migrations on its OWN startup, and the worker is a separate container that may
boot first (they start in parallel). Without this, the worker would hit a missing `download_jobs`
table and crash-loop until the API's migrations land. Poll patiently instead."""
table and crash-loop until the API's migrations land. Poll patiently instead — and if the schema
is still absent when the timeout runs out, say so and let the caller exit, because "continuing"
only moved the same crash one call further down (into `_recover_orphans`), where it read as an
unrelated failure."""
deadline = time.monotonic() + timeout
while not _stop.is_set():
try:
with SessionLocal() as db:
db.execute(text("SELECT 1 FROM download_jobs LIMIT 1"))
return
return True
except Exception as exc: # noqa: BLE001 — table not migrated yet / DB not up yet
if time.monotonic() > deadline:
log.warning("download schema still absent after %ss; continuing: %s", timeout, str(exc)[:120])
return
log.error(
"download schema still absent after %ss — the API's migrations have not landed; "
"exiting so the container restarts: %s",
timeout, str(exc)[:120],
)
return False
log.info("waiting for the database schema (API migrations)…")
_stop.wait(3.0)
return False
def _recover_orphans() -> None:
@@ -719,8 +728,9 @@ def main() -> None:
log.info("WORKER_ENABLED is off; worker exiting (nothing to do).")
return
_wait_for_schema() # the API may still be applying migrations when we boot
if _stop.is_set():
# The API may still be applying migrations when we boot. If they never arrive, exit instead of
# pressing on into a guaranteed crash — the container's restart policy retries the whole wait.
if not _wait_for_schema():
return
_recover_orphans()
n = max(1, settings.download_worker_concurrency)