From c1ac157aaf8d7efdcea520e99ee488ca26a339e6 Mon Sep 17 00:00:00 2001 From: Your Name Date: Wed, 6 May 2026 17:03:22 +0800 Subject: [PATCH] fix(km): keep backfill reconciler loop alive --- .../src/jobs/km_backfill_reconciler_job.py | 2 ++ .../test_km_writer_backfill_reconciler.py | 31 +++++++++++++++++-- docs/LOGBOOK.md | 30 ++++++++++++++++++ 3 files changed, 61 insertions(+), 2 deletions(-) diff --git a/apps/api/src/jobs/km_backfill_reconciler_job.py b/apps/api/src/jobs/km_backfill_reconciler_job.py index 8c21444b..bd3928a4 100644 --- a/apps/api/src/jobs/km_backfill_reconciler_job.py +++ b/apps/api/src/jobs/km_backfill_reconciler_job.py @@ -25,7 +25,9 @@ Feature Flag: from __future__ import annotations +import asyncio import json + import structlog from src.core.config import settings diff --git a/apps/api/tests/test_km_writer_backfill_reconciler.py b/apps/api/tests/test_km_writer_backfill_reconciler.py index eabbb9f4..6d7481d4 100644 --- a/apps/api/tests/test_km_writer_backfill_reconciler.py +++ b/apps/api/tests/test_km_writer_backfill_reconciler.py @@ -16,19 +16,20 @@ P1-1 C1 修復 2026-04-28 ogt + Claude Sonnet 4.6 """ import json -from unittest.mock import AsyncMock, MagicMock, patch +from unittest.mock import AsyncMock, patch import pytest +import src.jobs.km_backfill_reconciler_job as reconciler_job from src.jobs.km_backfill_reconciler_job import ( run_km_backfill_reconciler, + run_km_backfill_reconciler_loop, ) from src.services.km_writer import ( KM_BACKFILL_DLQ_KEY, _backfill_path_a_approval_safe, ) - # ============================================================================= # Helper # ============================================================================= @@ -136,6 +137,32 @@ async def test_reconciler_empty_dlq(): assert result["failed"] == 0 +@pytest.mark.asyncio +async def test_reconciler_loop_can_sleep(monkeypatch: pytest.MonkeyPatch): + """loop 必須能 sleep;避免少 import asyncio 導致 background task 啟動即死亡。""" + + class StopLoop(Exception): + pass + + calls = 0 + + async def fake_run_once(): + nonlocal calls + calls += 1 + return {"processed": 0, "success": 0, "failed": 0} + + async def fake_sleep(_seconds: int): + raise StopLoop + + monkeypatch.setattr(reconciler_job, "run_km_backfill_reconciler", fake_run_once) + monkeypatch.setattr(reconciler_job.asyncio, "sleep", fake_sleep) + + with pytest.raises(StopLoop): + await run_km_backfill_reconciler_loop() + + assert calls == 1 + + # ============================================================================= # 5. ENABLE_KM_BACKFILL_RECONCILER=false → 跳過 # ============================================================================= diff --git a/docs/LOGBOOK.md b/docs/LOGBOOK.md index 2e889972..8a2cd573 100644 --- a/docs/LOGBOOK.md +++ b/docs/LOGBOOK.md @@ -3858,3 +3858,33 @@ ruff check apps/api/src/core/logging.py apps/api/src/services/failover_alerter.p ### 注意 - `telegram_gateway.py` 全檔仍有大量既有 ruff 債,本次只針對 token 外洩與 MarkdownV2 400 風險做最小安全修補,避免在 6000+ 行 gateway 巨檔混入無關機械改動。 + +--- + +## 2026-05-06(台北)— KM backfill reconciler background loop 修復 + +**觸發**:production API 啟動後出現 `Task exception was never retrieved`,`run_km_backfill_reconciler_loop()` 因 `NameError: name 'asyncio' is not defined` 在第一次 sleep 前死亡,導致 `km:backfill:dlq` 補救 loop 沒有持續運作。 + +### 已修正 + +| 範圍 | 結果 | +|------|------| +| `km_backfill_reconciler_job.py` | 補上 `import asyncio`,讓 5 分鐘循環 sleep 可正常執行 | +| 回歸測試 | 新增 `test_reconciler_loop_can_sleep()`,用 fake sleep 主動中止 loop,驗證 loop 至少能跑一次 reconciler 並進入 sleep | + +### 驗證 + +```text +pytest apps/api/tests/test_km_writer_backfill_reconciler.py -q +# 8 passed + +py_compile apps/api/src/jobs/km_backfill_reconciler_job.py apps/api/tests/test_km_writer_backfill_reconciler.py +# 通過 + +ruff check apps/api/src/jobs/km_backfill_reconciler_job.py apps/api/tests/test_km_writer_backfill_reconciler.py +# All checks passed +``` + +### 影響 + +- KM / PlayBook / RAG 飛輪的 backfill 補救鏈恢復可持續執行,避免 DLQ 堆積後造成知識庫關聯缺口。