feat(api): Phase 16 R1.3 IncidentEngine 絞殺者模式
新增: - IncidentMemoryAdapter: 實作 IIncidentMemory Protocol - BlastRadiusAdapter: 實作 IBlastRadiusAnalyzer Protocol - get_incident_engine() 雙軌切換 (USE_NEW_ENGINE) 絞殺者模式設計: - 預設 USE_NEW_ENGINE=false (使用內嵌版) - 設為 true 時使用 lewooogo-brain IncidentEngine - 回滾: kubectl set env deployment/awoooi-api USE_NEW_ENGINE=false Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -593,7 +593,8 @@ class IncidentEngine:
|
||||
# 修復可能被轉成空物件的陣列欄位
|
||||
array_fields = ["signals", "affected_services", "proposal_ids"]
|
||||
for field in array_fields:
|
||||
if field in data and isinstance(data[field], dict) and len(data[field]) == 0:
|
||||
is_empty_dict = isinstance(data[field], dict) and len(data[field]) == 0
|
||||
if field in data and is_empty_dict:
|
||||
data[field] = []
|
||||
|
||||
return Incident.model_validate(data)
|
||||
@@ -643,15 +644,175 @@ class IncidentEngine:
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Singleton
|
||||
# Phase 16: 絞殺者模式 - Adapter 實作
|
||||
# =============================================================================
|
||||
|
||||
class IncidentMemoryAdapter:
|
||||
"""
|
||||
Incident Memory Adapter - 實作 lewooogo-brain 的 IIncidentMemory Protocol
|
||||
|
||||
Phase 16 R1.3: 橋接現有 Lua Scripts + DualIncidentMemory 到新 IncidentEngine
|
||||
|
||||
版本: v1.0
|
||||
建立: 2026-03-26 (台北時區)
|
||||
建立者: Claude Code
|
||||
"""
|
||||
|
||||
def __init__(self, memory: DualIncidentMemory) -> None:
|
||||
self._memory = memory
|
||||
self._lua_create_sha: str | None = None
|
||||
self._lua_aggregate_sha: str | None = None
|
||||
|
||||
async def _ensure_lua_scripts(self) -> None:
|
||||
"""確保 Lua Scripts 已載入"""
|
||||
if self._lua_create_sha:
|
||||
return
|
||||
redis_client = get_redis()
|
||||
self._lua_create_sha = await redis_client.script_load(LUA_CREATE_OR_AGGREGATE)
|
||||
self._lua_aggregate_sha = await redis_client.script_load(LUA_AGGREGATE_SIGNAL)
|
||||
|
||||
async def load_incident(self, incident_id: str) -> Incident | None:
|
||||
"""從 Working Memory 載入 Incident"""
|
||||
return await self._memory.load_incident(incident_id)
|
||||
|
||||
async def save_incident(
|
||||
self, incident: Incident, ttl_seconds: int = 604800
|
||||
) -> bool:
|
||||
"""儲存 Incident 到 Working Memory"""
|
||||
try:
|
||||
redis_client = get_redis()
|
||||
key = f"{INCIDENT_KEY_PREFIX}{incident.incident_id}"
|
||||
await redis_client.set(key, incident.model_dump_json(), ex=ttl_seconds)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.exception("save_incident_error", error=str(e))
|
||||
return False
|
||||
|
||||
async def persist_incident(self, incident: Incident) -> bool:
|
||||
"""持久化到 Episodic Memory (PostgreSQL)"""
|
||||
return await self._memory.persist_incident(incident)
|
||||
|
||||
async def find_related_incident(
|
||||
self,
|
||||
namespace: str,
|
||||
target: str,
|
||||
window_minutes: int = 30, # noqa: ARG002
|
||||
) -> Incident | None:
|
||||
"""尋找相關的活躍 Incident (用於聚合)"""
|
||||
redis_client = get_redis()
|
||||
|
||||
# 嘗試 namespace 索引
|
||||
ns_key = f"{INCIDENT_INDEX_NS}{namespace}"
|
||||
incident_id = await redis_client.get(ns_key)
|
||||
|
||||
if not incident_id:
|
||||
# 嘗試 target 索引
|
||||
target_key = f"{INCIDENT_INDEX_TARGET}{target}"
|
||||
incident_id = await redis_client.get(target_key)
|
||||
|
||||
if incident_id:
|
||||
if isinstance(incident_id, bytes):
|
||||
incident_id = incident_id.decode()
|
||||
return await self.load_incident(incident_id)
|
||||
|
||||
return None
|
||||
|
||||
async def update_index(
|
||||
self,
|
||||
incident_id: str,
|
||||
namespace: str,
|
||||
target: str,
|
||||
) -> bool:
|
||||
"""更新反向索引"""
|
||||
try:
|
||||
redis_client = get_redis()
|
||||
ttl = AGGREGATION_WINDOW_SECONDS
|
||||
|
||||
ns_key = f"{INCIDENT_INDEX_NS}{namespace}"
|
||||
target_key = f"{INCIDENT_INDEX_TARGET}{target}"
|
||||
|
||||
await redis_client.set(ns_key, incident_id, ex=ttl, nx=True)
|
||||
await redis_client.set(target_key, incident_id, ex=ttl, nx=True)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.exception("update_index_error", error=str(e))
|
||||
return False
|
||||
|
||||
|
||||
class BlastRadiusAdapter:
|
||||
"""
|
||||
Blast Radius Adapter - 實作 lewooogo-brain 的 IBlastRadiusAnalyzer Protocol
|
||||
|
||||
Phase 16 R1.3: 包裝現有 topology_graph
|
||||
|
||||
版本: v1.0
|
||||
建立: 2026-03-26 (台北時區)
|
||||
建立者: Claude Code
|
||||
"""
|
||||
|
||||
def __init__(self, graph=None) -> None:
|
||||
self._graph = graph or topology_graph
|
||||
|
||||
def analyze(self, target: str) -> list[str]:
|
||||
"""分析受影響的服務列表"""
|
||||
try:
|
||||
result: BlastRadiusResult = self._graph.get_blast_radius(target)
|
||||
return result.affected_services
|
||||
except Exception as e:
|
||||
logger.warning("blast_radius_analysis_failed", target=target, error=str(e))
|
||||
return [target] if target != "unknown" else []
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Singleton + 絞殺者模式切換
|
||||
# =============================================================================
|
||||
|
||||
_incident_engine: IncidentEngine | None = None
|
||||
_new_incident_engine = None # Type: lewooogo_brain IncidentEngine
|
||||
|
||||
|
||||
def get_incident_engine() -> IncidentEngine:
|
||||
"""取得 Incident Engine 實例 (Singleton)"""
|
||||
def _get_new_engine():
|
||||
"""取得 lewooogo-brain 的 IncidentEngine (Phase 16 新版)"""
|
||||
global _new_incident_engine
|
||||
if _new_incident_engine is None:
|
||||
from lewooogo_brain.engines import IncidentEngine as NewIncidentEngine
|
||||
|
||||
# 建立 Adapters
|
||||
memory_adapter = IncidentMemoryAdapter(get_incident_memory())
|
||||
blast_adapter = BlastRadiusAdapter()
|
||||
|
||||
_new_incident_engine = NewIncidentEngine(
|
||||
memory=memory_adapter,
|
||||
blast_analyzer=blast_adapter,
|
||||
logger=logger,
|
||||
)
|
||||
logger.info("new_incident_engine_initialized", version="lewooogo-brain")
|
||||
return _new_incident_engine
|
||||
|
||||
|
||||
def _get_legacy_engine() -> IncidentEngine:
|
||||
"""取得舊版 IncidentEngine"""
|
||||
global _incident_engine
|
||||
if _incident_engine is None:
|
||||
_incident_engine = IncidentEngine()
|
||||
return _incident_engine
|
||||
|
||||
|
||||
def get_incident_engine():
|
||||
"""
|
||||
取得 Incident Engine 實例 (Singleton + 絞殺者模式)
|
||||
|
||||
Phase 16: 根據 USE_NEW_ENGINE 設定切換引擎
|
||||
- False (預設): 使用內嵌版 IncidentEngine
|
||||
- True: 使用 lewooogo-brain 的 IncidentEngine
|
||||
|
||||
回滾方式:
|
||||
kubectl set env deployment/awoooi-api USE_NEW_ENGINE=false
|
||||
"""
|
||||
from src.core.config import settings
|
||||
|
||||
if settings.USE_NEW_ENGINE:
|
||||
logger.debug("using_new_incident_engine", version="lewooogo-brain")
|
||||
return _get_new_engine()
|
||||
else:
|
||||
return _get_legacy_engine()
|
||||
|
||||
Reference in New Issue
Block a user