fix(knowledge): preserve primary rows on taxonomy timeout

This commit is contained in:
Your Name
2026-07-22 18:30:25 +08:00
parent f7049837d6
commit a70eda5850
2 changed files with 165 additions and 11 deletions

View File

@@ -1059,23 +1059,85 @@ class KnowledgeService:
timeout=_PRIMARY_KM_DIRECT_QUERY_TIMEOUT_SECONDS,
)
items = [_direct_knowledge_entry_from_row(dict(row)) for row in rows]
categories = await self._read_categories_direct_with_conn(
conn,
project_id=project_id,
degraded_side_reads: list[str] = []
try:
categories = await self._read_categories_direct_with_conn(
conn,
project_id=project_id,
)
except Exception as exc: # noqa: BLE001 - preserve primary rows
logger.warning(
"knowledge_direct_categories_readback_degraded",
project_id=project_id,
error_type=type(exc).__name__,
)
categories = []
degraded_side_reads.append("categories")
categories_ready = bool(
categories and any(row.count > 0 for row in categories)
)
asset_taxonomy = await self._read_asset_taxonomy_direct_with_conn(
conn,
project_id=project_id,
if not categories_ready and "categories" not in degraded_side_reads:
degraded_side_reads.append("categories")
try:
asset_taxonomy = (
await self._read_asset_taxonomy_direct_with_conn(
conn,
project_id=project_id,
)
)
except Exception as exc: # noqa: BLE001 - preserve primary rows
logger.warning(
"knowledge_direct_asset_taxonomy_readback_degraded",
project_id=project_id,
error_type=type(exc).__name__,
)
asset_taxonomy = []
degraded_side_reads.append("asset_taxonomy")
asset_taxonomy_ready = bool(
asset_taxonomy
and any(row.count > 0 for row in asset_taxonomy)
)
if (
not asset_taxonomy_ready
and "asset_taxonomy" not in degraded_side_reads
):
degraded_side_reads.append("asset_taxonomy")
source_entries = _source_backed_entries()
partial_taxonomy = bool(degraded_side_reads)
return KnowledgeListResponse(
items=items,
total=total,
categories=categories or _source_category_counts(items),
asset_taxonomy=asset_taxonomy or _source_asset_taxonomy_counts(items),
readback_status="ready_direct_connection_after_session_timeout",
categories=(
categories
if categories_ready
else _source_category_counts(source_entries)
),
asset_taxonomy=(
asset_taxonomy
if asset_taxonomy_ready
else _source_asset_taxonomy_counts(source_entries)
),
readback_status=(
"ready_direct_connection_partial_taxonomy_degraded"
if partial_taxonomy
else "ready_direct_connection_after_session_timeout"
),
primary_readback_ready=True,
operator_stage="knowledge_readback_direct_connection_recovered",
next_step="repair_session_pool_readback_without_hiding_primary_km",
degraded_reason_code=(
"primary_km_taxonomy_readback_degraded"
if partial_taxonomy
else None
),
operator_stage=(
"knowledge_direct_primary_rows_ready_taxonomy_degraded"
if partial_taxonomy
else "knowledge_readback_direct_connection_recovered"
),
next_step=(
"repair_primary_km_taxonomy_without_hiding_primary_rows"
if partial_taxonomy
else "repair_session_pool_readback_without_hiding_primary_km"
),
writes_on_read=False,
manual_review_required=False,
)

View File

@@ -1,4 +1,6 @@
import asyncio
import sys
from types import SimpleNamespace
import pytest
from fastapi import FastAPI, HTTPException
@@ -220,6 +222,96 @@ async def test_knowledge_list_entries_uses_direct_readback_after_session_pool_ti
assert response.manual_review_required is False
@pytest.mark.asyncio
async def test_direct_list_keeps_primary_rows_when_asset_taxonomy_times_out(
monkeypatch,
) -> None:
service = KnowledgeService.__new__(KnowledgeService)
class DirectConnection:
async def execute(self, *_args):
return "OK"
async def fetchval(self, *_args):
return 727
async def fetch(self, *_args):
return [
{
"id": "km-primary-direct-1",
"title": "Primary direct KM row",
"content": "Persistent row survives taxonomy pressure",
"entry_type": "runbook",
"category": "alert_handling",
"tags": ["alert", "telegram"],
"source": "ai_extracted",
"status": "approved",
"view_count": 0,
}
]
async def close(self):
return None
connection = DirectConnection()
async def connect(_url):
return connection
async def categories(_conn, *, project_id):
assert project_id == "awoooi"
return [CategoryCount(category="alert_handling", count=113)]
async def taxonomy_timeout(_conn, *, project_id):
assert project_id == "awoooi"
raise TimeoutError("taxonomy statement timeout")
monkeypatch.setitem(
sys.modules,
"asyncpg",
SimpleNamespace(connect=connect),
)
monkeypatch.setattr(
knowledge_service_module.settings,
"DATABASE_URL",
"postgresql+asyncpg://test:test@127.0.0.1:5432/test",
)
monkeypatch.setattr(
service,
"_read_categories_direct_with_conn",
categories,
)
monkeypatch.setattr(
service,
"_read_asset_taxonomy_direct_with_conn",
taxonomy_timeout,
)
response = await service._list_entries_from_direct(
project_id="awoooi",
category=None,
entry_type=None,
status=None,
tags=None,
q=None,
limit=1,
offset=0,
)
assert response is not None
assert response.total == 727
assert [item.id for item in response.items] == ["km-primary-direct-1"]
assert response.primary_readback_ready is True
assert response.readback_status == (
"ready_direct_connection_partial_taxonomy_degraded"
)
assert response.degraded_reason_code == (
"primary_km_taxonomy_readback_degraded"
)
assert response.categories[0].category == "alert_handling"
assert any(row.key == "alert" for row in response.asset_taxonomy)
@pytest.mark.asyncio
async def test_knowledge_list_entries_keeps_primary_items_when_taxonomy_degrades(monkeypatch) -> None:
primary_entry = KnowledgeEntry(