# 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 實作）

```python
# 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

```python
# 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"]:
        ...

    # ... 後續現有邏輯 ...
```

```python
# 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/reset（Tier 1 操作）
```

---

## 閾值設計依據

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

---

## 模組化合規驗證

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

---

## 驗收標準

| 項目 | 通過條件 |
|------|---------|
| 有狀態服務保護 | 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-028：Failure Auto-Repair Loop
- ADR-030：Intelligent Auto-Remediation
- ADR-038：OpenClaw Concurrency Governance（Semaphore + Circuit Breaker）
