- 新增 docs/adr/ADR-016:daily_sales cache TTL → TTL+DB fingerprint,棄用 N-POST broadcast hack(命中率 9.4%),引述 debugger/web-researcher/migration-engineer 三方共識;殘留 HIGH-2(app.py 43 處 _SALES_PROCESSED_CACHE)列管 - docs/memory/history_logs.md 新增 4/28-29 條目:app.py -10.8%、4 個改進版死碼揭發、 cache 機制重構、hook 9 PoC 強化、12 Agent 並行作戰實踐 - CLAUDE.md 補上 ADR-015/016 索引項 Memory(個人 auto-memory,未入 repo)同步: - feedback_flask_blueprint_shadow.md(first-registered wins) - feedback_cache_invalidation_pattern.md(DB fingerprint vs N-POST) - feedback_agent_dispatch_order.md(critic 必須先於 fullstack) - feedback_cloud_vs_local_automation.md(LAN 專案禁雲端 routine) - project_phase3e_refactor_progress.md(11 commits 戰果)
3.6 KiB
3.6 KiB
ADR-016: daily_sales cache 失效策略改用 DB Fingerprint
決策
daily_sales_snapshot 相關 cache 從「TTL-only」改為「TTL + DB fingerprint」雙閘失效機制:
def _get_data_fingerprint(engine, table_name='daily_sales_snapshot'):
SELECT MAX(snapshot_date)::text, COUNT(*) FROM "{table_name}"
return (max_date, count) # 任一變動 fingerprint 跳號
def _is_cache_valid(cache_key, engine=None, table_name=...):
1. cache 不存在 → False
2. TTL 過期(>300s)→ False
3. DB fingerprint 不一致 → False
4. fingerprint 查詢失敗 → 保守維持(return True)
5. 全通過 → True
cache 寫入時同步記錄 fingerprint 欄位。
背景:為什麼不能用 N-POST broadcast hack
舊方案:import 完成後從 services/import_service.py 連續 POST /api/daily_sales/clear_cache N 次(N = GUNICORN_WORKERS=4),希望命中所有 worker 各自清掉 module-level dict。
實測不可靠(debugger + web-researcher + migration-engineer 三方共識):
- gunicorn sync worker 用 OS kernel
accept()queue 派送,不是 round-robin - 4 次 POST 命中全部 4 worker 機率 = 4!/4^4 ≈ 9.4%(coupon collector)
- 需 N=20 才能拉到 ~94% 覆蓋率,N=50 才到 99.4%
- 仍永遠不到 100%,且每次 import 多 50 個 internal HTTP 完全是浪費
DB fingerprint 把問題從「跨 worker 廣播」翻轉成「各 worker 自查 source of truth」:
- DB 是唯一一致狀態
- 每次 cache 命中前查一次
MAX(snapshot_date), COUNT(*)(< 5ms,PG 索引) - 資料寫入 → fingerprint 跳號 → 4 worker 下一次 request 各自偵測失效並重載
- 強一致,無 N-POST 浪費
影響
| 項目 | 變動 |
|---|---|
| cache 一致性窗口 | 5 min TTL → 1 個 request 週期(強一致) |
| 每次頁面讀取額外成本 | +1 次 SELECT MAX/COUNT(< 5ms) |
| 跨 worker 同步機制 | 廣播(不可靠)→ DB pull(強一致) |
/api/daily_sales/clear_cache 角色 |
必要清除路徑 → ops escape hatch |
| 端點安全 | 無 auth → @login_required + CSRF(雙重攔截,POST 無 token 回 400) |
殘留技術債(未在本 ADR 範圍內)
app.py仍有 43 處_SALES_PROCESSED_CACHE引用(sales_analysis 等獨立 cache,與 daily_sales_routes 中的同名 dict 是兩份不同 module-local dict)- 後續應抽
services/cache_manager.py統一管理,套用同樣的 fingerprint 模式 - 列入 P10 Q1 後段任務
備選方案與否決理由
| 方案 | 否決理由 |
|---|---|
| Redis pub/sub | 違反「先查官方文件 → 一次只動一個依賴」鐵律;新增 SPOF;KM 用 pgvector 不該被當訊息 broker(職責汙染) |
| File mtime sentinel | 容器化下 volume mount 設定繁瑣;NFS/iCloud 環境時序不穩 |
PostgreSQL LISTEN/NOTIFY |
需 background thread,sync worker 不友善;待 QPS 提升或升級 gthread 後再評估 |
| 維持 N-POST hack | 9.4% 命中率本質不可靠 |