mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-01 17:00:21 +08:00
fix(stability): 健康监控 DB 操作异步化,防止 worker 超时崩溃
- executor: record_success 通过 asyncio.to_thread offload 到线程池 - error_handler: 3 处 record_failure 同样 offload 到线程池 - sync_execute: 设置 expire_on_commit=False 防止 commit 后 ORM 懒加载 - handler_adapter_base: 归一化 check_endpoint 的 base_url 输入
This commit is contained in:
118
tests/services/test_sync_execute_expire_on_commit.py
Normal file
118
tests/services/test_sync_execute_expire_on_commit.py
Normal file
@@ -0,0 +1,118 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
|
||||
import pytest
|
||||
|
||||
from src.services.task.core.schema import ExecutionResult
|
||||
from src.services.task.execute.sync_execute import SyncTaskExecutionService
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_sync_unified_temporarily_disables_expire_on_commit(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
db = MagicMock()
|
||||
db.expire_on_commit = True
|
||||
db.query.return_value.filter.return_value.first.return_value = SimpleNamespace(username="alice")
|
||||
|
||||
pool_ops = MagicMock()
|
||||
pool_ops.apply_pool_reorder = AsyncMock(return_value=([], []))
|
||||
|
||||
service = SyncTaskExecutionService(
|
||||
db,
|
||||
None,
|
||||
recorder=MagicMock(),
|
||||
pool_ops=pool_ops,
|
||||
error_ops=MagicMock(),
|
||||
failure_ops=MagicMock(),
|
||||
)
|
||||
|
||||
cache_scheduler = SimpleNamespace(_ensure_initialized=AsyncMock())
|
||||
|
||||
class _StubCandidateResolver:
|
||||
def __init__(self, db: object, cache_scheduler: object) -> None:
|
||||
self.db = db
|
||||
self.cache_scheduler = cache_scheduler
|
||||
|
||||
async def fetch_candidates(self, **kwargs: object) -> tuple[list[object], str]:
|
||||
return [], "gpt-4.1"
|
||||
|
||||
async def create_candidate_records_async(
|
||||
self, **kwargs: object
|
||||
) -> dict[tuple[int, int], str]:
|
||||
return {}
|
||||
|
||||
def count_total_attempts(self, _all_candidates: list[object]) -> int:
|
||||
return 0
|
||||
|
||||
class _StubFailoverEngine:
|
||||
def __init__(self, db: object, **kwargs: object) -> None:
|
||||
self.db = db
|
||||
|
||||
async def execute(self, **kwargs: object) -> ExecutionResult:
|
||||
assert getattr(self.db, "expire_on_commit") is False
|
||||
return ExecutionResult(success=True)
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.services.task.execute.sync_execute.SystemConfigService.get_config",
|
||||
lambda _db, _key, default=None: default,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.task.execute.sync_execute.get_cache_aware_scheduler",
|
||||
AsyncMock(return_value=cache_scheduler),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.task.execute.sync_execute.CandidateResolver",
|
||||
_StubCandidateResolver,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.task.execute.sync_execute.ErrorClassifier",
|
||||
lambda **kwargs: MagicMock(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.task.execute.sync_execute.RequestDispatcher",
|
||||
lambda **kwargs: MagicMock(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.task.execute.sync_execute.FailoverEngine",
|
||||
_StubFailoverEngine,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.task.execute.sync_execute.UsageService.create_pending_usage",
|
||||
lambda **kwargs: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.scheduling.utils.release_db_connection_before_await",
|
||||
lambda _db: None,
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.rate_limit.concurrency_manager.get_concurrency_manager",
|
||||
AsyncMock(return_value=MagicMock()),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.rate_limit.adaptive_rpm.get_adaptive_rpm_manager",
|
||||
lambda: MagicMock(),
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"src.services.request.executor.RequestExecutor",
|
||||
lambda **kwargs: MagicMock(),
|
||||
)
|
||||
|
||||
result = await service.execute_sync_unified(
|
||||
api_format="openai_chat",
|
||||
model_name="gpt-4.1",
|
||||
user_api_key=SimpleNamespace(id="key-1", user_id="user-1", name="default-key"),
|
||||
request_func=AsyncMock(),
|
||||
request_id="req-1",
|
||||
is_stream=True,
|
||||
capability_requirements=None,
|
||||
preferred_key_ids=None,
|
||||
request_body_ref=None,
|
||||
request_headers=None,
|
||||
request_body=None,
|
||||
)
|
||||
|
||||
assert result.success is True
|
||||
assert db.expire_on_commit is True
|
||||
62
tests/unit/test_handler_adapter_base.py
Normal file
62
tests/unit/test_handler_adapter_base.py
Normal file
@@ -0,0 +1,62 @@
|
||||
import pytest
|
||||
|
||||
from src.api.handlers.claude.adapter import ClaudeChatAdapter
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_endpoint_accepts_base_url_dict(monkeypatch: pytest.MonkeyPatch) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_run_endpoint_check(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"status_code": 200, "headers": {}, "response_time_ms": 1, "request_id": "test"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.api.handlers.base.endpoint_checker.run_endpoint_check",
|
||||
fake_run_endpoint_check,
|
||||
)
|
||||
|
||||
await ClaudeChatAdapter.check_endpoint(
|
||||
client=None,
|
||||
base_url={"base_url": "https://api.anthropic.com"},
|
||||
api_key="test-key",
|
||||
request_data={
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"max_tokens": 32,
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert captured["url"] == "https://api.anthropic.com/v1/messages"
|
||||
assert isinstance(captured["json_body"], dict)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_check_endpoint_accepts_url_key_in_base_url_dict(
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
captured: dict[str, object] = {}
|
||||
|
||||
async def fake_run_endpoint_check(**kwargs):
|
||||
captured.update(kwargs)
|
||||
return {"status_code": 200, "headers": {}, "response_time_ms": 1, "request_id": "test"}
|
||||
|
||||
monkeypatch.setattr(
|
||||
"src.api.handlers.base.endpoint_checker.run_endpoint_check",
|
||||
fake_run_endpoint_check,
|
||||
)
|
||||
|
||||
await ClaudeChatAdapter.check_endpoint(
|
||||
client=None,
|
||||
base_url={"url": "https://api.anthropic.com/v1"},
|
||||
api_key="test-key",
|
||||
request_data={
|
||||
"model": "claude-sonnet-4-5-20250929",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"max_tokens": 32,
|
||||
"stream": False,
|
||||
},
|
||||
)
|
||||
|
||||
assert captured["url"] == "https://api.anthropic.com/v1/messages"
|
||||
Reference in New Issue
Block a user