Files
awoooi/docs/adr/ADR-123-background-loop-migration-strategy.md
Your Name 13e51802fe feat(awooop): Phase 0 全 ADR + Phase 1 control plane schema(含 critic 四項修正)
## Phase 0(文件層,全部 Accepted)
- ADR-106/107:AwoooP 平台架構 + 儲存策略
- ADR-111~118:Bootstrap → RLS 七項核心 ADR
- ADR-119~124:SAGA → Singleton Decomposition 六項 ADR
- ADR-UI-01~04:Operator Console 四個 UI ADR

## Phase 1(DB schema + migration)
- awooop_phase1_control_plane_2026-05-04.sql:7 張新表 + trigger + RLS
  - Step 1:三角色(platform_admin/migration BYPASSRLS,awooop_app 受 RLS)
  - Step 13:GRANT awooop_app 最小權限(7 條)
  - Step 14:RLS fail-closed,移除 __platform__ 後門
- awooop_phase1_batch1_rls_2026-05-04.sql:高流量四表三步式 ADD COLUMN
- awooop_phase1_batch1_backfill.py:SKIP LOCKED 分批回填腳本
- awooop_models.py:7 個 SQLAlchemy 2.x models

## Critic 修正(4 Critical + 3 Major)
- C-1:ADD CONSTRAINT IF NOT EXISTS → DO 塊 + pg_constraint 查詢
- C-2:__mapper_args__ 字串 list → primary_key=True on mapped_column
- C-3:__platform__ RLS 後門 → 全移除,改用 BYPASSRLS role
- C-4:awooop_app role 從未建立 → Step 1 + 7 條 GRANT
- M-1:active_pointer_guard SECURITY DEFINER(FORCE RLS 跨租戶保護)
- M-2:pg_partman create_parent 加冪等防護
- M-3:immutability trigger 新增身份欄位保護(project_id/family/contract_id)

## Task 1.2 修補
- agent_loader.py:硬編碼 Mac 路徑 → AGENTS_DIR 環境變數
- Dockerfile:補 COPY .claude/agents/

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-04 13:37:11 +08:00

200 lines
6.9 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.
# ADR-123: Background Loop Migration Strategy
**狀態**Accepted
**日期**2026-05-03台北
**決策者**:統帥
**範圍**31 個背景 loop 的三分類、project_id 注入策略、退出時程表
**關聯**ADR-111bootstrap order、ADR-114worker lease、INV-3/INV-8
---
## 背景
INV-3 / INV-8 確認 `main.py` 中有 **31 個** background loop全部在沒有 `project_id` 的情況下運行。這些 loop 是 AwoooP 多租戶化的最大阻礙之一。
**三個核心問題**
1. 沒有 `project_id` → 無法使用 RLSADR-118
2. 沒有 `trace_id` → OTel 追蹤不完整ADR-121
3. 31 個 loop 不可能同時遷移,需要策略性分批
---
## 決策
### D1 — 三分類Loop 歸屬)
**分類 APlatform Internal維持 `__platform__`**
這些 loop 本就是平台層,不屬於任何 tenant永遠使用 `project_id = __platform__`
| Loop 名稱 | main.py 行 | 說明 |
|----------|-----------|------|
| `run_ai_slo_watchdog_loop` | 623 | Platform 層 SLO 監控 |
| `_run_model_version_tracker_loop` | 701 | AI Provider 版本追蹤 |
| `run_contract_outbox_relay` | Phase 2 新增)| ADR-113 outbox relay |
| `run_stale_run_reaper` | Phase 2 新增)| ADR-114 stale reaper |
**分類 BAWOOOI Tenant遷移到 `project_id = awoooi`**
這些 loop 實質上只服務 AWOOOI 這個 tenant遷移後仍只服務 AWOOOI但有正確的 project_id
| Loop 類型 | 範例 | 遷移難度 |
|----------|------|---------|
| 告警處理 loop | `run_alert_processor` | LOW加 ctx var|
| Incident 管理 loop | `run_incident_timeline_loop` | LOW |
| Approval resolver | `run_approval_timeout_resolver` | MEDIUMP0 priority|
| KM / learning loop | `run_aol_writeback_loop` | MEDIUM |
| Telegram 相關 | `resend_stale_ready_tokens` | MEDIUM |
**分類 CPer-TenantPhase 3+,需重構)**
這些 loop 未來需要為每個 tenant 運行一個實例:
| Loop 類型 | 說明 | 目標架構 |
|----------|------|---------|
| Poller / receiver loop | 收取 channel 事件 | 每 tenant 一個 worker |
| Heartbeat / health check | 監控 tenant 的 K8s 狀態 | 每 tenant 一個 worker |
**Phase 3 前,分類 C 的 loop 暫時保留在 legacy_awoooi_default 狀態。**
### D2 — project_id 注入方式(三種)
**方式 1contextvars 注入(分類 B 優先)**
```python
# 每個 loop 的頂層加入 project_id context
async def run_approval_timeout_resolver():
project_id_ctx_var.set("awoooi") # ADR-111 contextvars
trace_id = generate_trace_id()
trace_id_ctx_var.set(trace_id)
while True:
await _resolve_approval_timeouts()
await asyncio.sleep(30)
```
**方式 2platform_admin DB role分類 A**
平台層 loop 使用 `awooop_platform_admin` roleBYPASSRLSproject_id 設為 `__platform__`
```python
async def run_contract_outbox_relay():
project_id_ctx_var.set("__platform__")
async with get_platform_db_session() as db: # 使用 awooop_platform_admin role
await _relay_outbox_events(db)
```
**方式 3legacy_awoooi_default 過渡標籤(未遷移的 loop**
未遷移的 loop 在 `audit_log` 中用 `project_id = legacy_awoooi_default` 標記,讓 Phase 3+ 的審計可追溯:
```python
# 過渡期標記PR-10
LOOP_PROJECT_ID = os.getenv("LOOP_PROJECT_ID", "legacy_awoooi_default")
async def run_some_legacy_loop():
project_id_ctx_var.set(LOOP_PROJECT_ID)
# ... loop body
```
### D3 — 遷移優先序(與 PR 計畫對齊)
**P0 優先(立即)**
- `run_approval_timeout_resolver`main.py:490— 缺 trace_idapproval 超時無法追蹤
-`project_id_ctx_var.set("awoooi")` + `trace_id` 生成
**P1 優先Phase 2**
- `resend_stale_ready_tokens`main.py:362— Telegram 相關,缺 project_id 影響 RLS
- `run_aol_writeback_loop`main.py:540— KM 寫入路徑缺 project_id
**P2 優先Phase 2PR-10 批量)**
- 其餘 20+ 個分類 B 的 loop`project_id_ctx_var.set("awoooi")` 標記
**Phase 3重構**
- 分類 C 的 loop 重構為 per-tenant worker 架構
### D4 — 退出時程表legacy_awoooi_default 清零)
| 里程碑 | 條件 | 目標 Phase |
|--------|------|-----------|
| 分類 A 全部標記 `__platform__` | 4 個 loop | Phase 2 完成 |
| 分類 B 全部標記 `awoooi` | 27 個 loop | Phase 2 完成PR-10|
| `legacy_awoooi_default` loop 清零 | RLS 可真正強制 | Phase 3 完成前 |
| 分類 C 重構為 per-tenant | EwoooC 支援前置 | Phase 3 |
| K8s `LOOP_PROJECT_ID` env var 移除 | 不再需要過渡標籤 | Phase 3 |
### D5 — Loop Health 監控
每個 loop 必須有 Prometheus gauge 記錄最後一次執行時間:
```python
# 標準 loop 健康監控模板
from prometheus_client import Gauge
LOOP_LAST_RUN = Gauge(
"awooop_background_loop_last_run_timestamp",
"Unix timestamp of last successful loop execution",
labelnames=["loop_name", "project_id"]
)
async def run_approval_timeout_resolver():
project_id = "awoooi"
project_id_ctx_var.set(project_id)
while True:
try:
await _resolve_approval_timeouts()
LOOP_LAST_RUN.labels(
loop_name="approval_timeout_resolver",
project_id=project_id
).set_to_current_time()
except Exception as e:
logger.error(f"Loop error: {e}")
await asyncio.sleep(30)
```
**告警規則**
```yaml
# Prometheus alert: loop 超過 5 分鐘沒有更新
- alert: BackgroundLoopStale
expr: time() - awooop_background_loop_last_run_timestamp > 300
labels:
severity: warning
```
---
## 後果
### Benefits
- 31 個 loop 的 project_id 遷移有明確分類和優先序
- Phase 2 完成後,`legacy_awoooi_default` loop 清零RLS 可真正強制
- Loop 健康監控:每個 loop 都有 Prometheus gaugeSLO watchdog 可監控
### Costs
- PR-10 需要修改 ~27 個 loop 的頂部(逐個加 ctx var
- 分類 C 的 loop 重構Phase 3是大型工程需要 planner 拆解
### Risks
- 過渡期有 `legacy_awoooi_default` loop 繞過 RLS設計上允許
- 緩解RLS 的空字串 policy 在 Phase 3 移除Phase 1~2 可接受
- P0 priority loopapproval_timeout_resolver若遷移失敗 → approval 超時告警無法送達
---
## 驗收標準
- [ ] `run_approval_timeout_resolver` 加 project_id + trace_idP0立即
- [ ] 全部 31 個 loop 標記 project_idPR-10Phase 2
- [ ] `legacy_awoooi_default` loop 清零Phase 3 前提條件)
- [ ] 每個 loop 有 Prometheus gauge 健康監控Phase 2
- [ ] Loop stale alert 觸發測試Phase 2 驗收)
## 關聯
- ADR-111三 identity markersplatform_internal / legacy_awoooi_default / requires_project_id
- ADR-114stale reaper新增 platform_internal loop
- ADR-118RLSloop project_id 正確性是 RLS 前置條件)
- INV-3/INV-831 個 loop 詳細清單)
- PR-10批量加 loop tagging