Skip to content

fix(mongo): MongoBatchUploader.stop 防止 in-flight 資料 silent loss#136

Open
csp0924 wants to merge 2 commits into
mainfrom
fix/uploader-shutdown-loss
Open

fix(mongo): MongoBatchUploader.stop 防止 in-flight 資料 silent loss#136
csp0924 wants to merge 2 commits into
mainfrom
fix/uploader-shutdown-loss

Conversation

@csp0924

@csp0924 csp0924 commented May 12, 2026

Copy link
Copy Markdown
Owner

問題

Closed-loop probe 發現:MongoBatchUploader.stop() 在 Mongo 不可用時靜默丟資料。Shutdown 路徑單次 flush_all 失敗後走 _handle_write_failurequeue.restore() → loop break,docs 滯留 in-memory queue 直到 process exit。WARNING log 寫 "重試 1/2" 還會誤導 operator 以為會繼續重試。

Sandbox 證據(sandbox/data_upload_backpressure_demo.py H3 scenario):300 docs enqueue + immediate stop with unavailable Mongo → queue size = 300、write_calls = 1、persisted = 0。

改動

  • csp_lib/mongo/uploader.py 新增 _shutdown_drain():drain queues → bounded retry(預設 3 次、_SHUTDOWN_FLUSH_RETRY_DELAY=0.1s),不再走 _handle_write_failure 的誤導 WARNING + restore 路徑
  • Retry 用盡時改 emit ERROR log,明列每個 collection 漏掉的 doc 數
  • _flush_loop stop branch + CancelledError branch 都改呼叫 _shutdown_drain()
  • 無 public API 變更(patch bump)

驗證

  • 新增 tests/mongo/test_uploader_shutdown.py(2 案:persistent failure 確認 ERROR log;transient failure 確認 eventual persist)
  • 全 4853 regression pass、ruff / mypy clean

當 Mongo 在 stop() 時不可用,原本 _flush_loop 停止分支只執行單次 flush_all(),
失敗時 _handle_write_failure 把 documents restore 回 queue 並印 "重試 N/M"
WARNING(誤導 operator,因為 loop 已 break 不會有後續 retry),最終 documents
silent 留在記憶體中,process 結束後資料遺失,無任何 ERROR log。

修復:新增 _shutdown_drain() 取代 stop 分支對 flush_all 的單次呼叫:
- bounded retry write_batch(documents) 最多 _SHUTDOWN_FLUSH_MAX_ATTEMPTS=3 次,
  每次間隔 0.1s 給暫時性網路抖動恢復時間
- 全部失敗時 emit ERROR log,明確列出每個 collection 未落庫筆數
- 不走 _handle_write_failure 的 WARNING + restore 路徑(避免誤導 operator)

H3 closed-loop probe scenario:300 筆 + 不可用 Mongo + immediate stop
- 修復前:queue 殘留 300 筆,僅有 1 條 "重試 1/2" WARNING
- 修復後:3 次 retry 後 emit ERROR "shutdown 後仍有 300 筆資料無法寫入
  Mongo(per-collection: metrics=300)",operator 可從 log 辨認遺失規模

Public API signature 不變(patch-level);新增的常數為 module-private。
Copilot AI review requested due to automatic review settings May 12, 2026 23:01

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens MongoBatchUploader.stop() shutdown behavior to avoid silent in-memory data loss when MongoDB is unavailable, by adding a shutdown-specific bounded retry drain path and validating it with new tests.

Changes:

  • Add _shutdown_drain() to drain queues and perform bounded retry writes during shutdown, emitting an ERROR with per-collection lost counts when retries are exhausted.
  • Update _flush_loop stop and CancelledError branches to use _shutdown_drain() instead of the normal failure/restore path.
  • Add shutdown-focused tests covering persistent failure (must ERROR with counts) and transient failure (eventual persistence).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
csp_lib/mongo/uploader.py Adds shutdown drain + bounded retry + improved error logging; routes stop/cancel paths through it.
tests/mongo/test_uploader_shutdown.py Adds regression tests to ensure stop() does not silently lose in-flight docs.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread csp_lib/mongo/uploader.py Outdated
Comment on lines 313 to 317
@@ -308,8 +316,9 @@ async def _flush_loop(self) -> None:
await self._wait_for_trigger()

Comment thread csp_lib/mongo/uploader.py Outdated
Comment on lines 331 to 333
# 結束前 bounded retry flush 所有資料
await self._shutdown_drain()
break
Comment thread csp_lib/mongo/uploader.py Outdated
Comment on lines +352 to +356
pending: dict[str, list[dict[str, Any]]] = {}
for collection_name in list(self._queues.keys()):
docs = await self._queues[collection_name].drain()
if docs:
pending[collection_name] = docs
Comment on lines +96 to +100
@pytest.mark.asyncio
async def test_stop_with_transient_failure_eventually_persists(self) -> None:
"""
暫時性故障 — 前幾次 write_batch 失敗、第 N 次成功。
bounded retry 應該讓資料最終落庫,queue 清空且不噴 ERROR。
回應 PR #136 Copilot review 四點:

1. `_flush_loop` 改 try/finally,drain 從 break 內 inline 移到 finally:
   - 若 stop_event 在進 loop 前就 set,原本 `while not stop_event.is_set()`
     會直接跳過 drain → silent loss;現在 finally 必跑一次 drain。
   - CancelledError 與正常 stop 兩條退出路徑都收斂到同一個 drain 呼叫,
     避免實作分歧。

2. Python 3.11+ cancel 語意處理:drain 內含多個 await 點(drain queue
   + write_batch + sleep),cancel count 累積會在每個 await 重新 raise。
   進 finally 前若是 cancelled,先 `current_task().uncancel()` 清掉
   pending count 讓 drain 跑完,再於最末重新 raise CancelledError
   保留 cancellation 語意。

3. `_shutdown_drain` 在每輪 retry 前、retry 全成功後、retry 配額耗盡前
   都再 drain 一次 queue,把 stop 期間其他協程 enqueue 進來的 late
   docs 併入 pending;確保 lost-count ERROR 反映 shutdown 全程實際
   未落庫筆數,不會被 race 進來的 docs silently 漏掉。

4. test_stop_with_transient_failure_eventually_persists 加 LogCapture
   斷言:retry-success 路徑不應留下任何 ERROR log,防止未來改動把
   transient failure 誤判成 silent loss。

完整回歸:tests/mongo/ 143 passed;tests/ 全體 4853 passed。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants