"""Phase 4: Global Digest 워커. 7일 뉴스를 country × topic 으로 묶어 cluster-level LLM 요약을 생성하고 global_digests / digest_topics 테이블에 저장한다. - APScheduler cron (매일 04:00 KST) + 수동 호출 공용 진입점 - PIPELINE_HARD_CAP = 600초 hard cap 으로 cron stuck 절대 방지 - 단독 실행: `python -m workers.digest_worker` """ import asyncio from core.config import settings from core.database import engine as db_engine from core.utils import setup_logger from services.background_jobs import finish_job, start_job from services.digest.pipeline import run_digest_pipeline logger = setup_logger("digest_worker") # 2026-06-15: config 단일소스 (구 600s = 빠른 Gemma 기준, Qwen 27B 교체 후 누락 → 초과). PIPELINE_HARD_CAP = settings.digest_pipeline_hard_cap_s async def run() -> None: """APScheduler + 수동 호출 공용 진입점. pipeline 자체는 timeout 으로 감싸지 않음 (per-call timeout 은 summarizer 가 처리). 여기서는 전체 hard cap 만 강제. """ if "digest" in settings.pipeline_held_stages: logger.info("[global_digest] 보류 (pipeline.held_stages) — 이번 실행 skip") return # 보드 가시화: 큐 밖 cron 생성 작업이라 background_jobs 로 노출 (best-effort, 맥미니 귀속) job_id = await start_job(db_engine, "global_digest", label="글로벌 다이제스트 생성") try: result = await asyncio.wait_for( run_digest_pipeline(job_id=job_id), timeout=PIPELINE_HARD_CAP, ) await finish_job(db_engine, job_id, state="done") logger.info(f"[global_digest] 워커 완료: {result}") except asyncio.TimeoutError: await finish_job(db_engine, job_id, state="failed", error=f"HARD CAP {PIPELINE_HARD_CAP}s 초과") logger.error( f"[global_digest] HARD CAP {PIPELINE_HARD_CAP}s 초과 — 워커 강제 중단. " f"기존 digest 는 commit 시점에만 갱신되므로 그대로 유지됨. " f"다음 cron 실행에서 재시도." ) except Exception as e: await finish_job(db_engine, job_id, state="failed", error=str(e)[:300]) logger.exception(f"[global_digest] 워커 실패: {e}") if __name__ == "__main__": asyncio.run(run())