fix(scheduler): per-connector hard timeout + reorder mangoporn-first

Bug-report 2026-05-30 "ingest znów się zawiesił". streamporn/pandamovies
wieszały się intermittentnie mid-run (zależnie od live-contentu danego dnia),
blokując sekwencyjny _job_movie_ingest → mangoporn (jedyny mirror z realnym
new-content: 72 nowych 05-28) nigdy nie startował. try/except chronił przed
wyjątkiem, NIE przed hangiem.

Fix:
- _job_movie_ingest: każdy connector w ThreadPoolExecutor z future.result
  (timeout=360s). Hang jednego źródła → log + shutdown(wait=False) + kolejka
  leci dalej. Healthy run ~50s, cap 6min = zapas.
- get_movie_connectors: reorder paradisehill, MANGOPORN, streamporn, pandamovies
  — mangoporn zaraz po canonical primary, przed wolniejszymi/wieszającymi się.

Zweryfikowane: pełny _job_movie_ingest przeszedł wszystkie 4 success w nowej
kolejności (mangoporn 2nd, 23s). 33 osierocone "running" rows (worker ubity
mid-run przy deployach) wyczyszczone osobno.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
jtrzupek 2026-05-31 11:19:13 +02:00
parent c19da51aff
commit 05c0f6ef93
2 changed files with 31 additions and 2 deletions

View file

@ -40,9 +40,15 @@ def get_movie_connectors() -> list[tuple[str, type]]:
) )
from app.connectors.paradisehill import ParadisehillConnector from app.connectors.paradisehill import ParadisehillConnector
# Kolejność ingestu: paradisehill FIRST (canonical primary, mirrory się do
# niego przyklejają), potem mangoporn (jedyny mirror z realnym new-content —
# 72 nowych 2026-05-28; streamporn/pandamovies zwracają stale 0 new), na końcu
# streamporn + pandamovies. Powód reorderu (2026-05-30): gdy streamporn wiesza
# się intermittentnie, mangoporn musi zdążyć przed nim — patrz per-connector
# timeout w _job_movie_ingest.
return [ return [
("paradisehill", ParadisehillConnector), ("paradisehill", ParadisehillConnector),
("mangoporn", MangopornConnector),
("streamporn", StreampornConnector), ("streamporn", StreampornConnector),
("pandamovies", PandamoviesConnector), ("pandamovies", PandamoviesConnector),
("mangoporn", MangopornConnector),
] ]

View file

@ -98,11 +98,34 @@ def _job_movie_ingest() -> None:
Kolejność: paradisehill FIRST (żeby mirrory miały do czego się przykleić), Kolejność: paradisehill FIRST (żeby mirrory miały do czego się przykleić),
potem mirrory. Pojedynczy failed connector NIE zatrzymuje pozostałych potem mirrory. Pojedynczy failed connector NIE zatrzymuje pozostałych
każdy w osobnym try/except. każdy w osobnym try/except.
HARD TIMEOUT per-connector (bug-report 2026-05-30 "ingest znów się zawiesił"):
sam try/except chroni przed *wyjątkiem*, ale NIE przed *hangiem* (CPU-bound
ReDoS na patologicznej stronie / thread-stall) wtedy jeden mirror blokuje
resztę i mangoporn (jedyny z realnym new-content) nigdy nie startuje.
Każdy connector leci w osobnym wątku z `future.result(timeout)`; po
przekroczeniu logujemy i idziemy dalej (osierocony wątek dożywa do restartu
workera OK, bo loop się odblokowuje). Healthy run ~50s, cap 6 min = zapas.
""" """
from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeout
PER_CONNECTOR_TIMEOUT = 360 # sekundy
for name, cls in get_movie_connectors(): for name, cls in get_movie_connectors():
log.info("[scheduler] movie ingest %s starting", name) log.info("[scheduler] movie ingest %s starting", name)
try: try:
ingest_movies_from_connector(cls(), use_delta=True) ex = ThreadPoolExecutor(max_workers=1)
fut = ex.submit(ingest_movies_from_connector, cls(), use_delta=True)
try:
fut.result(timeout=PER_CONNECTOR_TIMEOUT)
except FutureTimeout:
log.error(
"[scheduler] movie ingest %s HUNG > %ds — skip, kolejka leci dalej",
name, PER_CONNECTOR_TIMEOUT,
)
finally:
# shutdown(wait=False): nie blokuj na join osieroconego wątku.
ex.shutdown(wait=False)
except Exception: except Exception:
log.exception("[scheduler] movie ingest %s failed", name) log.exception("[scheduler] movie ingest %s failed", name)