From 01db4816fd52afe2f3cb5d41ef29a8f0b1c44204 Mon Sep 17 00:00:00 2001 From: hyungi Date: Thu, 11 Jun 2026 13:42:10 +0900 Subject: [PATCH] =?UTF-8?q?feat(workers):=20drain=20=EC=97=B0=EC=86=8D?= =?UTF-8?q?=EB=B3=B4=EB=A5=98=20=EB=82=B4=EC=84=B1=20=E2=80=94=20=EB=84=A4?= =?UTF-8?q?=ED=8A=B8=EC=9B=8C=ED=81=AC=20=ED=94=8C=EB=9E=A9=20=ED=9D=A1?= =?UTF-8?q?=EC=88=98=20(--defer-retries/--defer-wait)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 실측 origin: Tailscale direct 경로 ~10분 플랩(13:25~13:34)으로 300건 run 이 32건에서 조기 종료. 보류 시멘틱 자체는 정상(무손상) — run 지속성만 보강. 연속 보류 5회까지 120s 간격 재시도, 한도 도달 = sleep 판정 종료. 성공 시 카운터 리셋. Co-Authored-By: Claude Fable 5 --- app/workers/queue_drain.py | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/app/workers/queue_drain.py b/app/workers/queue_drain.py index 4cec1de..bfe2cbc 100644 --- a/app/workers/queue_drain.py +++ b/app/workers/queue_drain.py @@ -10,8 +10,10 @@ ds-macbook-offload-1 P2-3. 운영 패턴 = csb_collector --bulk 와 동일 (컨 - claim = FOR UPDATE SKIP LOCKED 단건 전이 → consumer(1분 주기)와 이중처리 0 - per-item 커밋 = sleep-안전: 중단돼도 완료분 무손상, 진행 중 1건만 stale recovery (10분) 로 pending 복귀. 재실행 멱등 (summarize 는 ai_summary 존재 시 skip) - - 보류(StageDeferred = 맥북 sleep/cold/editor_busy): attempts 반환 + deferred_until - 백오프 기록 후 run 즉시 종료 — 불가 상태의 맥북을 계속 두드리지 않는다 + - 보류(StageDeferred = 맥북 sleep/cold/editor_busy/네트워크 플랩): attempts 반환 + + deferred_until 백오프 기록. 연속 보류 --defer-retries(기본 5)회까지 --defer-wait + (기본 120s) 간격 재시도(분 단위 플랩 흡수), 한도 도달 = sleep 판정으로 run 종료 — + 불가 상태의 맥북을 계속 두드리지 않는다 - 폴백 0: 맥미니/cloud 강등 없음 """ @@ -94,7 +96,7 @@ async def _mark_failed(queue_id: int, exc: Exception) -> None: await session.commit() -async def drain(stage: str, limit: int) -> None: +async def drain(stage: str, limit: int, defer_retries: int = 5, defer_wait: int = 120) -> None: if stage not in DRAIN_STAGES: raise SystemExit(f"--stage 는 {DRAIN_STAGES} 만 허용 (classify 등은 맥미니 적합 — plan Q-4)") if settings.ai.deep is None: @@ -108,6 +110,7 @@ async def drain(stage: str, limit: int) -> None: done = failed = 0 deferred = False + consecutive_defers = 0 while done + failed < limit: claimed = await _claim_one(stage) if claimed is None: @@ -124,15 +127,27 @@ async def drain(stage: str, limit: int) -> None: await worker_session.commit() await _mark_completed(queue_id) done += 1 + consecutive_defers = 0 logger.info(f"[drain:{stage}] {done}/{limit} doc={document_id} 완료") except StageDeferred as defer: + # 일시 불가는 종류가 둘: 진짜 sleep(장시간) vs 일시 네트워크 플랩(분 단위 — + # 2026-06-11 실측: Tailscale direct 경로 ~10분 플랩으로 32/300 조기 종료). + # 연속 보류 한도까지 대기 후 재시도해 플랩을 흡수, 한도 도달 시 종료(sleep 판정). await _mark_deferred(queue_id, defer) - deferred = True + consecutive_defers += 1 + if consecutive_defers >= defer_retries: + deferred = True + logger.warning( + f"[drain:{stage}] doc={document_id} 맥북 불가({defer}) — 연속 보류 " + f"{consecutive_defers}회 한도 도달, run 종료. 맥북 깨운 뒤(또는 " + f"{defer.retry_after_minutes}분 후) 재실행" + ) + break logger.warning( - f"[drain:{stage}] doc={document_id} 맥북 불가({defer}) — 보류 기록 후 run 종료. " - f"맥북 깨운 뒤(또는 {defer.retry_after_minutes}분 후) 재실행" + f"[drain:{stage}] doc={document_id} 맥북 일시 불가({defer}) — " + f"{defer_wait}s 대기 후 재시도 ({consecutive_defers}/{defer_retries})" ) - break + await asyncio.sleep(defer_wait) except Exception as exc: await _mark_failed(queue_id, exc) failed += 1 @@ -156,8 +171,12 @@ def main() -> None: parser = argparse.ArgumentParser(description="맥북(M5 Max) burst-drain — 수동 백로그 분담 레버") parser.add_argument("--stage", required=True, choices=DRAIN_STAGES) parser.add_argument("--limit", type=int, default=50, help="이번 run 최대 처리 건수 (기본 50)") + parser.add_argument("--defer-retries", type=int, default=5, + help="연속 보류 허용 횟수 — 네트워크 플랩 흡수 (기본 5, 한도 도달 시 종료)") + parser.add_argument("--defer-wait", type=int, default=120, + help="보류 재시도 간 대기 초 (기본 120)") args = parser.parse_args() - asyncio.run(drain(args.stage, args.limit)) + asyncio.run(drain(args.stage, args.limit, args.defer_retries, args.defer_wait)) if __name__ == "__main__":