fix(sync): a failed feed-fallback read waits its window out, and spent quota ends the round

A channel whose uploads playlist also failed was never stamped, so a terminated channel was charged
again on every 20-minute poll instead of once per rss_fallback_hours. An upstream quotaExceeded now
stops the round instead of charging every remaining channel the same refusal.
This commit is contained in:
2026-09-19 05:47:16 +02:00
parent 582f20fab6
commit 1fa6a0bea8
3 changed files with 63 additions and 4 deletions
+28
View File
@@ -8,12 +8,16 @@ import pytest
from app import sysconfig
from app.models import Channel, Video
from app.sync import runner
from app.youtube.client import YouTubeError
NOW = datetime.now(timezone.utc)
_PUB = datetime(2020, 1, 1, tzinfo=timezone.utc)
class _FakeYt:
# playlist id -> the reason its read fails with (the tests put entries here)
failing: dict[str, str] = {}
def __init__(self, calls):
self.calls = calls
@@ -25,6 +29,8 @@ class _FakeYt:
def get_playlist_items(self, playlist_id, page_token=None):
self.calls.append(playlist_id)
if playlist_id in self.failing:
raise YouTubeError("refused", reason=self.failing[playlist_id])
cid = "UC" + playlist_id[2:]
return {
"items": [
@@ -42,6 +48,7 @@ def calls(monkeypatch):
monkeypatch.setattr(runner, "get_service_user", lambda db: object())
monkeypatch.setattr(runner, "YouTubeClient", lambda db, user: _FakeYt(made))
monkeypatch.setattr(runner.quota, "backfill_reserve_reached", lambda: False)
monkeypatch.setattr(_FakeYt, "failing", {})
return made
@@ -113,3 +120,24 @@ def test_it_yields_to_the_backfill_reserve(db, calls, monkeypatch):
assert runner.run_feed_fallback(db, ["UCdark"])["read"] == 0
assert calls == []
def test_a_read_that_fails_waits_the_window_out_too(db, calls):
# A terminated channel: its feed 404s and so does its uploads playlist, for good. Without a
# stamp it was charged on every 20-minute poll instead of once per window.
_channel(db, "UCgone", rss=NOW - timedelta(hours=3))
_FakeYt.failing["UUgone"] = "playlistNotFound"
assert runner.run_feed_fallback(db, ["UCgone"]) == {"read": 0, "new": 0, "failed": 1}
assert db.get(Channel, "UCgone").feed_fallback_at is not None
assert runner.run_feed_fallback(db, ["UCgone"])["failed"] == 0
assert calls == ["UUgone"]
def test_out_of_quota_upstream_stops_the_round(db, calls):
_channel(db, "UCa")
_channel(db, "UCb")
_FakeYt.failing["UUa"] = "quotaExceeded"
assert runner.run_feed_fallback(db, ["UCa", "UCb"])["failed"] == 1
assert calls == ["UUa"]