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

6.9 KiB
Raw Blame History

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 優先)

# 每個 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__

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+ 的審計可追溯:

# 過渡期標記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_resolvermain.py:490— 缺 trace_idapproval 超時無法追蹤
  • project_id_ctx_var.set("awoooi") + trace_id 生成

P1 優先Phase 2

  • resend_stale_ready_tokensmain.py:362— Telegram 相關,缺 project_id 影響 RLS
  • run_aol_writeback_loopmain.py:540— KM 寫入路徑缺 project_id

P2 優先Phase 2PR-10 批量)

  • 其餘 20+ 個分類 B 的 loopproject_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 記錄最後一次執行時間:

# 標準 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)

告警規則

# 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