fix(persistence): separate retryable rejection errors (#6423)

This commit is contained in:
InfinityPacer
2026-08-23 20:10:59 +08:00
committed by GitHub
parent 46d5c8538e
commit c6ffc309d4
8 changed files with 79 additions and 48 deletions
+4 -4
View File
@@ -12,7 +12,7 @@ import pytest
from sqlalchemy import delete, select
from app.schemas.exception import (
DatabaseWorkerClosedError,
AgentChatPersistenceUnavailableError,
DatabaseWorkerOverloadedError,
)
from app.application.messaging.chat import AgentChatPersistenceService, AgentChatService
@@ -302,7 +302,7 @@ async def test_agent_chat_persistence_bounds_session_waiters_and_releases_cancel
messages=[],
)
)
with pytest.raises(DatabaseWorkerOverloadedError):
with pytest.raises(AgentChatPersistenceUnavailableError):
await third
second.cancel()
with pytest.raises(asyncio.CancelledError):
@@ -347,7 +347,7 @@ async def test_agent_chat_persistence_session_admission_is_fair() -> None:
)
)
await asyncio.sleep(0)
with pytest.raises(DatabaseWorkerOverloadedError):
with pytest.raises(AgentChatPersistenceUnavailableError):
await service.async_save_agent_messages(
session_id="hot-session", user_id="1", messages=[]
)
@@ -393,7 +393,7 @@ async def test_agent_chat_persistence_shutdown_drains_active_writes() -> None:
shutdown = asyncio.create_task(service.shutdown())
await asyncio.sleep(0)
assert not shutdown.done()
with pytest.raises(DatabaseWorkerClosedError):
with pytest.raises(AgentChatPersistenceUnavailableError):
await service.async_save_agent_messages(
session_id="new-session", user_id="1", messages=[]
)
+31 -12
View File
@@ -18,14 +18,16 @@ from app.api.response import (
ResponseAPIRouter,
)
from app.factory import (
database_worker_overloaded_handler,
localized_http_exception_handler,
localized_unhandled_exception_handler,
localized_validation_exception_handler,
persistence_unavailable_handler,
)
from app.schemas.exception import (
AgentChatPersistenceUnavailableError,
DatabaseWorkerClosedError,
DatabaseWorkerOverloadedError,
PersistenceUnavailableError,
)
from app.runtime.localization import LocaleHelper
from app.runtime.config import settings
@@ -67,12 +69,8 @@ def api_app() -> FastAPI:
app.router.route_class = ResponseAPIRoute
app.add_exception_handler(HTTPException, localized_http_exception_handler)
app.add_exception_handler(
DatabaseWorkerOverloadedError,
database_worker_overloaded_handler,
)
app.add_exception_handler(
DatabaseWorkerClosedError,
database_worker_overloaded_handler,
PersistenceUnavailableError,
persistence_unavailable_handler,
)
from fastapi.exceptions import RequestValidationError
@@ -137,6 +135,11 @@ def api_app() -> FastAPI:
"""模拟数据库 worker 在关闭态拒绝新任务。"""
raise DatabaseWorkerClosedError("worker closed")
@app.get("/agent-chat-persistence-unavailable")
async def get_agent_chat_persistence_unavailable() -> None:
"""模拟 AgentChat 自身 admission 拒绝新写入。"""
raise AgentChatPersistenceUnavailableError("agent persistence full")
@app.get("/native", response_model=None)
async def get_native_response() -> dict[str, bool]:
"""返回显式旁路的原生 JSON 协议。"""
@@ -247,14 +250,30 @@ async def test_database_worker_closed_is_retryable_service_unavailable(
}
def test_create_app_registers_closed_database_worker_handler() -> None:
"""生产组合根必须为 worker 关闭态登记 503 处理器。"""
async def test_agent_chat_persistence_rejection_is_retryable_service_unavailable(
api_app: FastAPI,
) -> None:
"""AgentChat 自身 admission 拒绝也应返回可重试的 503。"""
async with make_client(api_app) as client:
response = await client.get("/agent-chat-persistence-unavailable")
assert response.status_code == 503
assert response.headers["retry-after"] == "1"
assert response.json() == {
"success": False,
"message": "服务当前繁忙,请稍后重试",
"data": None,
}
def test_create_app_registers_persistence_unavailable_handler() -> None:
"""生产组合根必须为持久化暂不可用登记统一 503 处理器。"""
from app.factory import create_app
app = create_app()
assert app.exception_handlers[DatabaseWorkerClosedError] is (
database_worker_overloaded_handler
assert app.exception_handlers[PersistenceUnavailableError] is (
persistence_unavailable_handler
)
@@ -283,7 +302,7 @@ async def test_database_worker_overload_preserves_retry_after_for_native_protoco
"client": ("testclient", 123),
"root_path": "",
}
response = await database_worker_overloaded_handler(
response = await persistence_unavailable_handler(
Request(scope),
DatabaseWorkerOverloadedError("worker full"),
)
+15 -5
View File
@@ -3,7 +3,11 @@ from unittest.mock import AsyncMock, Mock, patch
import pytest
from app.schemas.exception import DatabaseWorkerOverloadedError
from app.schemas.exception import (
DatabaseWorkerClosedError,
DatabaseWorkerOverloadedError,
PersistenceUnavailableError,
)
from app.application.plugin.install import PluginInstallCommand
@@ -249,17 +253,23 @@ async def test_persistence_exception_after_write_restores_installed_list():
@pytest.mark.asyncio
async def test_database_worker_overload_rolls_back_and_reaches_api_boundary():
"""配置 worker 背压完成补偿后继续抛出,交由 API 映射为 503。"""
@pytest.mark.parametrize(
"error_type",
[DatabaseWorkerClosedError, DatabaseWorkerOverloadedError],
)
async def test_persistence_unavailable_rolls_back_and_reaches_api_boundary(
error_type: type[PersistenceUnavailableError],
) -> None:
"""持久化能力暂不可用时完成补偿并交由 API 映射为 503。"""
checkpoint = object()
rollback = AsyncMock()
command = _command(
checkpointer=AsyncMock(return_value=checkpoint),
writer=AsyncMock(side_effect=DatabaseWorkerOverloadedError("worker full")),
writer=AsyncMock(side_effect=error_type("persistence unavailable")),
rollback=rollback,
)
with pytest.raises(DatabaseWorkerOverloadedError):
with pytest.raises(error_type):
await command.execute(
plugin_id="DemoPlugin",
repo_url="https://github.com/demo/plugins",