Files
DashBoard/openspec/changes/archive/2026-03-03-fix-silent-data-loss-reject-history/design.md
egg a275c30c0e feat(reject-history): fix silent data loss by propagating partial failure metadata to frontend
Chunk failures in BatchQueryEngine were silently discarded — `has_partial_failure` was tracked
in Redis but never surfaced to the API response or frontend. Users could see incomplete data
without any warning. This commit closes the gap end-to-end:

Backend:
- Track failed chunk time ranges (`failed_ranges`) in batch engine progress metadata
- Add single retry for transient Oracle errors (timeout, connection) in `_execute_single_chunk`
- Read `get_batch_progress()` after merge but before `redis_clear_batch()` cleanup
- Inject `has_partial_failure`, `failed_chunk_count`, `failed_ranges` into API response meta
- Persist partial failure flag to independent Redis key with TTL aligned to data storage layer
- Add shared container-resolution policy module with wildcard/expansion guardrails
- Refactor reason filter from single-value to multi-select (`reason` → `reasons`)

Frontend:
- Add client-side date range validation (730-day limit) before API submission
- Display amber warning banner on partial failure with specific failed date ranges
- Support generic fallback message for container-mode queries without date ranges
- Update FilterPanel to support multi-select reason chips

Specs & tests:
- Create batch-query-resilience spec; update reject-history-api and reject-history-page specs
- Add 7 new tests for retry, memory guard, failed ranges, partial failure propagation, TTL
- Cross-service regression verified (hold, resource, job, msd — 411 tests pass)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-03 14:00:07 +08:00

81 lines
6.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## Context
報廢歷史查詢使用 `BatchQueryEngine` 將長日期範圍拆成 10 天 chunks 平行查詢 Oracle。每個 chunk 有記憶體上限256 MB和 timeout300s防護。當 chunk 失敗時,`has_partial_failure` 旗標寫入 Redis HSETkey: `batch:reject:{hash}:meta`),但此資訊**在三個斷點被丟失**
1. `reject_dataset_cache.py``execute_primary_query()` 未讀取 batch progress metadata
2. API route 直接 `jsonify({"success": True, **result})`,在 partial chunk failure 路徑下仍回 HTTP 200 + `success: true`,不區分完整與不完整結果
3. 前端 `App.vue` 沒有任何 partial failure 處理邏輯
另一個問題:`redis_clear_batch()``execute_primary_query()` 的清理階段會刪除 metadata key所以讀取必須在清理之前。
前端的 730 天日期上限驗證只在後端 `_validate_range()` 做,前端缺乏即時回饋。
## Goals / Non-Goals
**Goals:**
-`has_partial_failure` 從 Redis metadata 傳遞到 API response `meta` 欄位
- 追蹤失敗 chunk 的時間範圍,讓前端可顯示具體的缺漏區間
- 前端顯示 amber warning banner告知使用者資料可能不完整
- 前端加入日期範圍即時驗證,避免無效 API 請求
- 對 transient errorOracle timeout、連線失敗加入單次重試減少不必要的 partial failure
- 持久化 partial failure 旗標到獨立 Redis key讓 cache-hit 路徑也能還原警告狀態
**Non-Goals:**
- 不改變現有 chunk 分片策略或記憶體上限數值
- 不實作前端的自動重查/重試機制
- 不修改 `EVENT_FETCHER_ALLOW_PARTIAL_RESULTS` 的行為(預設已是安全的 false
- 不加入 progress bar / 即時進度追蹤 UI
## Decisions
### D1: 在 `redis_clear_batch` 之前讀取 metadata
**決定**: 在 `execute_primary_query()` 中,`merge_chunks()` 之後、`redis_clear_batch()` 之前,呼叫 `get_batch_progress("reject", engine_hash)` 讀取 partial failure 狀態。
**理由**: `redis_clear_batch` 會刪除包含 metadata 的 key之後就讀不到了。此時 chunk 資料已合併完成,是最後可讀取 metadata 的時機點。
### D2: 用獨立 Redis key 持久化 partial failure flagTTL 對齊實際資料層
**決定**: 在 `_store_query_result()` 之後,將 partial failure 資訊存到 `reject_dataset:{query_id}:partial_failure` Redis HSET。**TTL 必須與資料實際存活的層一致**:若資料 spill 到 parquet spool`_REJECT_ENGINE_SPOOL_TTL_SECONDS = 21600s`partial failure flag 的 TTL 也要用 21600s若資料存在 L1/L2`_CACHE_TTL = 900s`flag TTL 用 900s。實作方式`_store_partial_failure_flag()` 接受 `ttl` 參數,由呼叫端根據 `should_spill` 判斷傳入 `_REJECT_ENGINE_SPOOL_TTL_SECONDS``_CACHE_TTL`。Cache-hit 路徑透過 `_load_partial_failure_flag(query_id)` 還原。
**替代方案 A**: 將 flag 嵌入 DataFrame 的 attrs 或另外 pickle。
**為何不採用**: DataFrame attrs 在 parquet 序列化時會丟失pickle 增加反序列化風險。
**替代方案 B**: 固定 TTL=900s。
**為何不採用**: 大查詢 spill 到 parquet spool21600s TTL資料還能讀 6 小時,但 partial failure flag 15 分鐘就過期,造成「資料讀得到但警告消失」。
### D3: 在 `_update_progress` 中追蹤 failed_ranges僅 time-range chunk
**決定**: 擴充 `_update_progress()` 接受 `failed_ranges: Optional[List[Dict]]` 參數,以 JSON 字串存入 Redis HSET。Sequential 和 parallel path 均從失敗的 chunk descriptor 提取 `chunk_start` / `chunk_end`。**僅當 chunk descriptor 包含 `chunk_start`/`chunk_end` 時才記錄**(即 `decompose_by_time_range` 產生的 time-range chunk
**container-id 分塊的情境**: reject 的 container 模式使用 `decompose_by_ids()`chunk 結構為 `{"ids": [...]}` 不含日期範圍。此時 `failed_ranges` 為空 list前端透過 `failed_chunk_count > 0` 顯示 generic 警告訊息「N 個查詢批次的資料擷取失敗」),不含日期區間。
**理由**: chunk descriptor 的結構由 decompose 函式決定engine 層不應假設所有 chunk 都有時間範圍。
### D4: Memory guard 失敗不重試
**決定**: `_execute_single_chunk()` 加入 `max_retries=1`,但只對 `_is_retryable_error()` 回傳 true 的 exception 重試。Memory guard記憶體超限和 Redis store 失敗直接 return False不重試。
**理由**: Memory guard 代表該時段資料量確實過大重試結果相同Oracle timeout 和連線錯誤則可能是暫態問題。
### D5: 前端 warning banner 使用既有 amber 色系
**決定**: 新增 `.warning-banner` CSS class使用 `background: #fffbeb; color: #b45309`,與既有 `.resolution-warn` 的 amber 色系一致。放在 `.error-banner` 之後。
**替代方案**: 使用 toast/notification 元件。
**為何不採用**: 此專案無 toast 系統amber banner 與 red error-banner 模式統一。
### D6: 前端日期驗證函式放在共用 filters module
**決定**: 在 `frontend/src/core/reject-history-filters.js` 新增 `validateDateRange()`,複用 `resource-history/App.vue:231-248` 的驗證模式。
**理由**: reject-history-filters.js 已是此頁面的 filter 工具模組validateDateRange 屬於 filter 驗證邏輯。
## Risks / Trade-offs
- **[中] 重試邏輯影響所有 execute_plan 呼叫端** — `_execute_single_chunk()` 是 shared function被 reject / hold / resource / job / msd 五個服務共用。重試邏輯為加法行為(新增 retry loop 包在既有 try/except 外),成功路徑不變。→ 需要對其他 4 個服務執行 smoke test既有測試通過即可。若需更保守可加入 `max_retries` 參數讓呼叫端控制(預設 1但目前判斷統一重試對所有服務都是正面效果。
- **[低] 重試增加 Oracle 負擔** — 單次重試最多增加 1 倍的失敗查詢量。→ 透過 `_is_retryable_error()` 嚴格過濾,只重試 transient error且 parallel path 最多 3 worker影響可控。
- **[低] failed_ranges JSON 大小** — 理論上 73 chunks730/10全部失敗會產生 73 筆 rangeJSON < 5 KB。→ 遠低於 Redis HSET 欄位限制