Files
awoooi/docs/adr/ADR-040-global-autorepair-governance.md
OG T 998b5a7b5f docs: ADR-039 重編號為 ADR-040 + LOGBOOK 更新
ADR 變更:
- ADR-039 (gitea-cicd-migration) 保留給 Gitea CI/CD 遷移
- 原 ADR-039 (global-autorepair-governance) 改為 ADR-040

LOGBOOK:
- 新增 Phase 19.4 Terminal Service API 整合記錄
- 更新當前狀態

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-03-30 01:20:50 +08:00

7.4 KiB
Raw Permalink Blame History

ADR-040: 全域自動修復熔斷機制

ADR-040: Global Auto-repair Governance Strategy

狀態: 已批准 日期: 2026-03-30 01:10 (台北時間) 決策者: 統帥 + Antigravity (首席架構師) 觸發事件: 沙盤推演發現跨資源 Poison Pill 可導致 AI 骨牌修復死循環


問題陳述

場景:跨資源修復骨牌效應

流量激增
  → AI 執行 scale_up (擴容 API Pod)
  → API Pod 數量增加,連線池耗盡 PostgreSQL
  → AI 收到 DB 告警,執行 restart_api_pod (釋放 DB 連線)
  → API 重啟導致 502 激增
  → AI 再次執行 scale_up
  → 死循環,資源榨乾

核心問題:現有 max_repairs_per_resource: 3 只限單一資源,無法防止骨牌效應。


決策:雙層全域保護機制

機制一全域修復冷卻期Global Action Cooldown

當系統整體在過去 15 分鐘內自動修復超過 5 次(不論對象),強制凍結所有 Auto-Repair轉為 AWAITING_APPROVAL

機制二StatefulSet 硬禁令Stateful Service Blacklist

有狀態服務PostgreSQL、Redis、ClickHouse、MinIO 等)永遠不允許自動重啟,強制人工介入。


實作規範

全域計數器Redis 實作)

# apps/api/src/services/global_repair_cooldown.py
"""
全域修復熔斷機制
================
ADR-039防止跨資源循環修復

設計原則:
- Redis TTL 滑動窗口15 分鐘)
- 失敗降級Redis 故障時保守跳過自動修復,強制人工確認
"""

import structlog
from src.core.redis_client import get_redis

logger = structlog.get_logger(__name__)

GLOBAL_COOLDOWN_KEY = "global:auto_repair:count"
GLOBAL_COOLDOWN_TTL = 900        # 15 分鐘窗口
GLOBAL_COOLDOWN_THRESHOLD = 5    # 超過 5 次強制凍結

STATEFUL_SERVICE_BLACKLIST = frozenset({
    "postgres", "postgresql", "awoooi-postgres",
    "redis", "awoooi-redis", "redis-stack",
    "clickhouse", "signoz-clickhouse",
    "elasticsearch", "etcd",
    "minio", "awoooi-minio",
})


async def check_global_repair_cooldown(
    incident_id: str,
    affected_services: list[str],
) -> tuple[bool, str]:
    """
    檢查是否允許自動修復

    Returns:
        (can_repair: bool, reason: str)
    """
    redis = get_redis()

    # === 硬禁令:有狀態服務黑名單 ===
    for service in affected_services:
        if any(bl in service.lower() for bl in STATEFUL_SERVICE_BLACKLIST):
            reason = f"服務 {service} 為有狀態服務,禁止自動重啟,請統帥手動介入"
            logger.warning(
                "stateful_service_blocked",
                service=service,
                incident_id=incident_id,
            )
            return False, reason

    # === 全域冷卻期Redis 計數 ===
    try:
        count_raw = await redis.get(GLOBAL_COOLDOWN_KEY)
        current_count = int(count_raw) if count_raw else 0

        if current_count >= GLOBAL_COOLDOWN_THRESHOLD:
            reason = (
                f"系統在過去 15 分鐘內已自動修復 {current_count} 次,"
                f"超出安全閾值 {GLOBAL_COOLDOWN_THRESHOLD}"
                "強制轉為人工審核模式"
            )
            logger.warning(
                "global_repair_cooldown_active",
                current_count=current_count,
                threshold=GLOBAL_COOLDOWN_THRESHOLD,
                incident_id=incident_id,
            )
            return False, reason

        return True, "允許自動修復"

    except Exception as e:
        # Redis 故障 → 保守策略:禁止自動修復
        logger.error(
            "global_repair_cooldown_redis_error",
            error=str(e),
            fallback="blocking_auto_repair_for_safety",
        )
        return False, f"Redis 連線異常,保守禁止自動修復(原因:{e}"


async def record_global_repair_action() -> None:
    """
    記錄一次全域修復動作

    使用 INCR + EXPIRE 實現滑動窗口計數
    注意INCR 是原子操作,多個 Worker 並發安全
    """
    try:
        redis = get_redis()
        count = await redis.incr(GLOBAL_COOLDOWN_KEY)

        # 只在第一次設定 TTL避免頻繁重設導致窗口延長
        if count == 1:
            await redis.expire(GLOBAL_COOLDOWN_KEY, GLOBAL_COOLDOWN_TTL)

        logger.info(
            "global_repair_action_recorded",
            count=count,
            threshold=GLOBAL_COOLDOWN_THRESHOLD,
        )

    except Exception as e:
        # Redis 故障:靜默失敗
        logger.warning("global_repair_record_failed", error=str(e))

整合到 auto_repair_service.py

# auto_repair_service.py - evaluate_auto_repair() 加入前置檢查

from src.services.global_repair_cooldown import check_global_repair_cooldown

async def evaluate_auto_repair(self, incident: Incident) -> AutoRepairDecision:
    # === 最優先:全域熔斷檢查(在所有其他邏輯之前)===
    can_repair, cooldown_reason = await check_global_repair_cooldown(
        incident_id=incident.incident_id,
        affected_services=incident.affected_services or [],
    )

    if not can_repair:
        return AutoRepairDecision(
            can_auto_repair=False,
            reason=cooldown_reason,
            blocked_by="GLOBAL_GUARDRAIL",
        )

    # === 現有邏輯Severity 檢查 ===
    if incident.severity and incident.severity.value in ["P0", "P1"]:
        ...

    # ... 後續現有邏輯 ...
# auto_repair_service.py - execute_auto_repair() 執行後記錄

async def execute_auto_repair(self, incident, playbook) -> AutoRepairResult:
    # ... 現有執行邏輯 ...

    # === 執行成功後,記錄全域計數 ===
    if result.success:
        from src.services.global_repair_cooldown import record_global_repair_action
        await record_global_repair_action()

    return result

全域熔斷狀態視覺化(未來)

Dashboard 首頁 → AI 自治指標面板 → 顯示:
「⚠️ 系統保護模式:過去 15 分鐘修復 5 次,暫停自動修復」
→ 統帥點擊解除 → POST /api/v1/repair/cooldown/resetTier 1 操作)

閾值設計依據

參數 理由
GLOBAL_COOLDOWN_THRESHOLD 5 正常運作時每天 < 5 次修復5 次以上代表異常模式
GLOBAL_COOLDOWN_TTL 900s15 分鐘) 大多數骨牌效應在 10 分鐘內完成15 分鐘提供緩衝

模組化合規驗證

項目 說明 合規狀態
層次 services/ 獨立服務層 合規
依賴 只依賴 core/redis_client 合規
Redis 降級 故障時保守處理(禁止自動修復) 合規
原子操作 使用 INCRRedis 原生原子性) 合規

驗收標準

項目 通過條件
有狀態服務保護 PostgreSQL/Redis 的 Incident 永遠返回 GLOBAL_GUARDRAIL
全域計數 第 6 次修復請求返回 can_auto_repair=False
Redis 降級 Redis 故障時 check_global_repair_cooldown 返回 (False, "Redis 連線異常...")
Dashboard 可見 (未來)保護模式顯示在 AI 自治指標面板

相關文件

  • docs/proposals/ARCHITECTURAL_RISK_WAR_GAME.md:風險沙盤推演
  • apps/api/src/services/auto_repair_service.py:自動修復服務
  • ADR-028Failure Auto-Repair Loop
  • ADR-030Intelligent Auto-Remediation
  • ADR-038OpenClaw Concurrency GovernanceSemaphore + Circuit Breaker