fix(incidents): batch decision token lookup
This commit is contained in:
@@ -206,11 +206,14 @@ async def list_incidents(
|
|||||||
|
|
||||||
responses = []
|
responses = []
|
||||||
background_tasks = []
|
background_tasks = []
|
||||||
|
existing_tokens = await decision_manager._find_existing_tokens_for_incidents(
|
||||||
|
[incident.incident_id for incident in incidents]
|
||||||
|
)
|
||||||
|
|
||||||
for incident in incidents:
|
for incident in incidents:
|
||||||
try:
|
try:
|
||||||
# 只查已快取的決策 (不等待 AI,立即返回)
|
# 只查已快取的決策 (不等待 AI,立即返回)
|
||||||
existing = await decision_manager._find_existing_token(incident.incident_id)
|
existing = existing_tokens.get(incident.incident_id)
|
||||||
if existing:
|
if existing:
|
||||||
decision_info = DecisionInfo(
|
decision_info = DecisionInfo(
|
||||||
token=existing.token,
|
token=existing.token,
|
||||||
|
|||||||
@@ -2953,6 +2953,52 @@ class DecisionManager:
|
|||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
async def _find_existing_tokens_for_incidents(
|
||||||
|
self,
|
||||||
|
incident_ids: list[str],
|
||||||
|
) -> dict[str, DecisionToken]:
|
||||||
|
"""
|
||||||
|
批次查找現有決策令牌。
|
||||||
|
|
||||||
|
2026-05-06 Codex: GET /api/v1/incidents 是前端輪詢路徑,不可對每個
|
||||||
|
incident 都掃描一次 decision:*。這裡只掃一次 Redis keyspace,避免
|
||||||
|
200+ incidents 時形成 O(N×M) 延遲與前端控制台卡死。
|
||||||
|
"""
|
||||||
|
wanted = set(incident_ids)
|
||||||
|
if not wanted:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
redis_client = get_redis()
|
||||||
|
found: dict[str, DecisionToken] = {}
|
||||||
|
cursor = 0
|
||||||
|
while True:
|
||||||
|
cursor, keys = await redis_client.scan(
|
||||||
|
cursor=cursor,
|
||||||
|
match=f"{DECISION_TOKEN_PREFIX}*",
|
||||||
|
count=500,
|
||||||
|
)
|
||||||
|
|
||||||
|
for key in keys:
|
||||||
|
try:
|
||||||
|
data = await redis_client.get(key)
|
||||||
|
if not data:
|
||||||
|
continue
|
||||||
|
token_data = json.loads(data)
|
||||||
|
incident_id = token_data.get("incident_id")
|
||||||
|
if incident_id in wanted and incident_id not in found:
|
||||||
|
found[incident_id] = DecisionToken.from_dict(token_data)
|
||||||
|
if len(found) == len(wanted):
|
||||||
|
return found
|
||||||
|
except Exception:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if cursor == 0:
|
||||||
|
break
|
||||||
|
|
||||||
|
return found
|
||||||
|
|
||||||
async def _persist_decision_to_db(
|
async def _persist_decision_to_db(
|
||||||
self, incident_id: str, proposal_data: dict
|
self, incident_id: str, proposal_data: dict
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|||||||
@@ -30,8 +30,15 @@ class _IncidentService:
|
|||||||
class _DecisionManager:
|
class _DecisionManager:
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self.created = 0
|
self.created = 0
|
||||||
|
self.single_token_lookups = 0
|
||||||
|
self.batch_token_lookups = 0
|
||||||
|
|
||||||
|
async def _find_existing_tokens_for_incidents(self, incident_ids: list[str]):
|
||||||
|
self.batch_token_lookups += 1
|
||||||
|
return {}
|
||||||
|
|
||||||
async def _find_existing_token(self, incident_id: str):
|
async def _find_existing_token(self, incident_id: str):
|
||||||
|
self.single_token_lookups += 1
|
||||||
return None
|
return None
|
||||||
|
|
||||||
async def get_or_create_decision(self, *args, **kwargs):
|
async def get_or_create_decision(self, *args, **kwargs):
|
||||||
@@ -51,3 +58,5 @@ async def test_list_incidents_does_not_trigger_ai_decision_by_default(monkeypatc
|
|||||||
assert result.incidents[0].incident_id == "INC-20260506-PURE01"
|
assert result.incidents[0].incident_id == "INC-20260506-PURE01"
|
||||||
assert result.incidents[0].decision is None
|
assert result.incidents[0].decision is None
|
||||||
assert decision_manager.created == 0
|
assert decision_manager.created == 0
|
||||||
|
assert decision_manager.batch_token_lookups == 1
|
||||||
|
assert decision_manager.single_token_lookups == 0
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
- `GET /api/v1/incidents` 新增 `generate_missing_decisions=false` 預設參數。
|
- `GET /api/v1/incidents` 新增 `generate_missing_decisions=false` 預設參數。
|
||||||
- 預設只讀取既有 decision token;缺少 token 時回傳 `decision=null`,不再背景觸發 Ollama / OpenClaw / Gemini。
|
- 預設只讀取既有 decision token;缺少 token 時回傳 `decision=null`,不再背景觸發 Ollama / OpenClaw / Gemini。
|
||||||
- 若維運人員明確需要舊行為,可用 `generate_missing_decisions=true` 觸發背景生成;正式修復建議仍應走 `POST /api/v1/incidents/{incident_id}/proposal` 或 AwoooP Operator Run。
|
- 若維運人員明確需要舊行為,可用 `generate_missing_decisions=true` 觸發背景生成;正式修復建議仍應走 `POST /api/v1/incidents/{incident_id}/proposal` 或 AwoooP Operator Run。
|
||||||
|
- `DecisionManager` 新增批次 token 查詢;列表路徑只掃一次 Redis `decision:*`,避免 200+ incidents 時逐筆掃描造成 O(N×M) 延遲。
|
||||||
- 新增 regression test,鎖定列表查詢預設不會呼叫 `get_or_create_decision()`。
|
- 新增 regression test,鎖定列表查詢預設不會呼叫 `get_or_create_decision()`。
|
||||||
|
|
||||||
**驗證**:
|
**驗證**:
|
||||||
|
|||||||
Reference in New Issue
Block a user