fix(database): harden worker shutdown and overload propagation

This commit is contained in:
InfinityPacer
2026-08-23 02:29:54 +08:00
parent 58352c2eb7
commit b79f927fcf
7 changed files with 172 additions and 11 deletions
+2
View File
@@ -2640,6 +2640,8 @@
"app.application.plugin.folders -> app.runtime.log",
"app.application.plugin.folders -> app.schemas",
"app.application.plugin.folders -> app.schemas.types",
"app.application.plugin.install -> app.application",
"app.application.plugin.install -> app.application.database",
"app.application.recognition -> app.application",
"app.application.recognition -> app.application.configuration",
"app.application.recognition -> app.schemas",
+36
View File
@@ -7,6 +7,7 @@ from fastapi import APIRouter, Depends, FastAPI, HTTPException
from fastapi.responses import JSONResponse
from fastapi.routing import APIRoute
from pydantic import BaseModel, ValidationError
from starlette.requests import Request
from starlette.responses import Response as StarletteResponse
from starlette.responses import StreamingResponse
@@ -24,6 +25,7 @@ from app.factory import (
)
from app.application.database import DatabaseWorkerOverloadedError
from app.runtime.localization import LocaleHelper
from app.runtime.config import settings
from app.schemas.common import JsonData
from app.schemas.response import Response
@@ -217,6 +219,40 @@ async def test_database_worker_overload_is_retryable_service_unavailable(
}
@pytest.mark.parametrize(
"path",
[
f"{settings.API_V1_STR}/openai/v1/chat/completions",
f"{settings.API_V1_STR}/anthropic/v1/messages",
f"{settings.API_V1_STR}/mcp",
],
)
async def test_database_worker_overload_preserves_retry_after_for_native_protocols(
path: str,
):
"""OpenAI、Anthropic 和 MCP 的原生 503 也必须保留重试提示。"""
scope = {
"type": "http",
"http_version": "1.1",
"method": "GET",
"scheme": "http",
"path": path,
"raw_path": path.encode(),
"query_string": b"",
"headers": [],
"server": ("testserver", 80),
"client": ("testclient", 123),
"root_path": "",
}
response = await database_worker_overloaded_handler(
Request(scope),
DatabaseWorkerOverloadedError("worker full"),
)
assert response.status_code == 503
assert response.headers["retry-after"] == "1"
async def test_validation_error_uses_unified_model(api_app: FastAPI):
"""请求参数校验失败应返回统一协议和明确的错误项结构。"""
async with make_client(api_app) as client:
+62
View File
@@ -2,6 +2,7 @@
import asyncio
import threading
from unittest.mock import patch
import pytest
@@ -138,3 +139,64 @@ async def test_shutdown_rejects_new_work_and_waits_for_running_work() -> None:
assert worker.snapshot().closing is True
assert worker.snapshot().queued == 0
assert worker.snapshot().running == 0
@pytest.mark.asyncio
async def test_shutdown_timeout_keeps_running_owner_until_transaction_finishes() -> None:
"""关闭超时应返回给生命周期编排,并保留执行器等待事务收敛。"""
worker = DatabaseWorker(max_workers=1, capacity=1)
await worker.start()
started = threading.Event()
release = threading.Event()
def operation() -> None:
started.set()
release.wait(1)
running = asyncio.create_task(worker.run(operation))
await asyncio.to_thread(started.wait)
with pytest.raises(asyncio.TimeoutError):
await asyncio.wait_for(worker.shutdown(), timeout=0.01)
assert worker.snapshot().closing is True
assert worker._executor is not None
release.set()
await running
await asyncio.sleep(0)
await worker.shutdown()
assert worker._executor is None
@pytest.mark.asyncio
async def test_worker_depth_metrics_emit_deltas_and_return_to_zero() -> None:
"""队列和运行量指标按增减量上报,不能把绝对值累加成漂移。"""
worker = DatabaseWorker(max_workers=1, capacity=1)
started = threading.Event()
release = threading.Event()
def operation() -> None:
started.set()
release.wait(1)
with patch("app.db.worker.record_metric") as record_metric:
await worker.start()
running = asyncio.create_task(worker.run(operation))
await asyncio.to_thread(started.wait)
release.set()
await running
await worker.shutdown()
queue_values = [
call.args[1]
for call in record_metric.call_args_list
if call.args[0] == "db.worker.queue.depth"
]
active_values = [
call.args[1]
for call in record_metric.call_args_list
if call.args[0] == "db.worker.active"
]
assert queue_values == [1.0, -1.0]
assert active_values == [1.0, -1.0]
+21
View File
@@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, Mock
import pytest
from app.application.database import DatabaseWorkerOverloadedError
from app.application.plugin.install import PluginInstallCommand
@@ -181,6 +182,26 @@ async def test_persistence_failure_restores_package_without_touching_runtime():
reloader.assert_not_awaited()
@pytest.mark.asyncio
async def test_database_worker_overload_rolls_back_and_reaches_api_boundary():
"""配置 worker 背压完成补偿后继续抛出,交由 API 映射为 503。"""
checkpoint = object()
rollback = AsyncMock()
command = _command(
checkpointer=AsyncMock(return_value=checkpoint),
writer=AsyncMock(side_effect=DatabaseWorkerOverloadedError("worker full")),
rollback=rollback,
)
with pytest.raises(DatabaseWorkerOverloadedError):
await command.execute(
plugin_id="DemoPlugin",
repo_url="https://github.com/demo/plugins",
)
rollback.assert_awaited_once_with(checkpoint)
@pytest.mark.asyncio
async def test_reload_failure_restores_list_files_and_previous_runtime():
"""重载失败时依次恢复已安装列表、包文件和旧运行态。"""