Worker 失败原因不再只留类名:区分"可落库的固定文案"与"异常消息"
起因是查 Worker 运行状态时发现库里 373 条死信的 last_error 全是裸的 "ValueError"
(工具 tools/probe_worker_state.py,证据 docs/evidence/worker-state.json)。
dispatch(run not found)、dispatch_run_completed、dispatch_memory_extraction、
dispatch_profile_rebuild 抛的都是 ValueError,只记类名等于把"哪一处失败"也一起丢了。
但"直接存 str(exc)"是错的:tests/unit/worker/test_outbox_worker.py 那条
RuntimeError("credential=do-not-log") 断言异常消息不得落库 —— 它可能含凭据、SQL 或
客户标识。第一版改动就是这么写的,被这个测试当场拦下(这测试写得值)。
折中:
- 新增 OutboxHandlerError(继承 ValueError,这些失败本就是 ValueError 语义,保持
继承关系才不会改动既有的 except ValueError 行为与断言)。它的 reason 由代码写死、
不含任何请求数据,因此可以落库;
- safe_error_text:OutboxHandlerError → "类名: 固定文案"(截断 500 字符),
其余异常 → 仍只记类名;
- runtime.py 的 5 处 handler 失败改抛 OutboxHandlerError。
测试:新增"固定文案落库"用例;并把既有用例的断言收紧为 last_error == "RuntimeError"
(原先只断言"不含 do-not-log",太松,漏掉的情况测不出来)。
顺带产出 tools/probe_worker_state.py(只读):outbox / agent_run 各状态计数、按事件
类型分组、死信原因聚合。当前环境实测 pending 347、dead 373、published 410、
agent_run 无 queued/running。
门禁:ruff 干净 / mypy 138 文件 / 697 unit+contract / 33 integration。
This commit is contained in:
@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, Mock
|
||||
import pytest
|
||||
|
||||
from app.model.platform import DomainEventOutbox
|
||||
from app.worker.outbox_worker import OutboxWorker
|
||||
from app.worker.outbox_worker import OutboxHandlerError, OutboxWorker
|
||||
|
||||
|
||||
def event() -> DomainEventOutbox:
|
||||
@@ -66,9 +66,32 @@ async def test_handler_failure_records_attempt(attempts: int, expected: str) ->
|
||||
assert row.status == expected
|
||||
assert row.retry_count == attempts + 1
|
||||
assert "do-not-log" not in (row.last_error or "")
|
||||
# 其他异常一律只记类名:消息可能含凭据/SQL/客户标识,不落库。
|
||||
assert row.last_error == "RuntimeError"
|
||||
session.commit.assert_awaited_once()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handler_error_reason_is_recorded() -> None:
|
||||
"""`OutboxHandlerError` 的固定文案可以落库 —— 否则失败原因根本无从分辨。
|
||||
|
||||
实测库里 373 条死信的 `last_error` 全是裸的 `"ValueError"`:`dispatch`(run not
|
||||
found)、`dispatch_run_completed`、`dispatch_memory_extraction`、
|
||||
`dispatch_profile_rebuild` 抛的都是 ValueError,只记类名等于把"哪一处失败"也丢了。
|
||||
这些 handler 现在改抛 `OutboxHandlerError`,它的 reason 由代码写死、不含请求数据。
|
||||
"""
|
||||
row = event()
|
||||
session = AsyncMock()
|
||||
session.add = Mock()
|
||||
session.scalar.side_effect = [row, None]
|
||||
handler = AsyncMock(side_effect=OutboxHandlerError("run not found"))
|
||||
|
||||
await OutboxWorker(session, {"test.event": handler}).publish_one()
|
||||
|
||||
assert row.last_error == "OutboxHandlerError: run not found"
|
||||
assert row.status == "failed"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_empty_matching_queue_releases_transaction() -> None:
|
||||
session = AsyncMock()
|
||||
|
||||
Reference in New Issue
Block a user