feat(cleanup): 解耦 request_candidates 与 provider_api_keys 生命周期

- 移除 request_candidates.key_id 对 provider_api_keys 的外键约束(含迁移脚本)
- 删除 Key 时不再级联删除候选记录,改为独立按保留天数定时清理
- 新增 request_candidates_retention_days / request_candidates_cleanup_batch_size 配置项
- batch_delete_task 增加 lock_timeout 及超时自动降批重试机制
- cleanup_key_references 提取阶段化清理流程,移除 RequestCandidate 联动删除
- 前端 CleanupPolicySection 新增候选记录保留天数和清理批次配置

Closes #227

Co-authored-by: Entropy-Xu <entropy.xu@cloudhabitatsh.com>
This commit is contained in:
fawney19
2026-03-14 01:31:39 +08:00
parent bdfe4adc98
commit 776dd2f8ea
14 changed files with 516 additions and 70 deletions

View File

@@ -2,6 +2,7 @@ from __future__ import annotations
import inspect
from types import SimpleNamespace
from unittest.mock import MagicMock
import pytest
@@ -41,3 +42,76 @@ async def test_maintenance_scheduler_start_skips_startup_task_when_disabled(
await scheduler.start()
assert created is False
@pytest.mark.asyncio
async def test_candidate_cleanup_uses_dedicated_retention_and_batch_settings(
monkeypatch: pytest.MonkeyPatch,
) -> None:
scheduler = MaintenanceScheduler()
class _FakeLoop:
async def run_in_executor(self, _executor, func): # type: ignore[no-untyped-def]
return func()
class _ConfigSession:
def close(self) -> None:
return None
class _BatchSession:
def __init__(self, ids: list[str]) -> None:
self.ids = ids
self.closed = False
self.committed = False
self.query_obj = MagicMock()
filtered = self.query_obj.filter.return_value
filtered.order_by.return_value.limit.return_value.all.return_value = [
SimpleNamespace(id=value) for value in ids
]
def query(self, _model): # type: ignore[no-untyped-def]
return self.query_obj
def execute(self, _statement): # type: ignore[no-untyped-def]
return SimpleNamespace(rowcount=len(self.ids))
def commit(self) -> None:
self.committed = True
def rollback(self) -> None:
raise AssertionError("rollback should not be called")
def close(self) -> None:
self.closed = True
config_session = _ConfigSession()
batch_one = _BatchSession(["candidate-1", "candidate-2"])
batch_two = _BatchSession([])
sessions = iter([config_session, batch_one, batch_two])
def fake_create_session(): # type: ignore[no-untyped-def]
return next(sessions)
config_values = {
"enable_auto_cleanup": True,
"request_candidates_retention_days": 21,
"request_candidates_cleanup_batch_size": 2,
}
monkeypatch.setattr(maintenance_scheduler_module, "create_session", fake_create_session)
monkeypatch.setattr(
maintenance_scheduler_module.SystemConfigService,
"get_config",
lambda _db, key, default=None: config_values.get(key, default),
)
monkeypatch.setattr(
maintenance_scheduler_module.asyncio, "get_running_loop", lambda: _FakeLoop()
)
await scheduler._perform_candidate_cleanup()
batch_one.query_obj.filter.return_value.order_by.return_value.limit.return_value.all.assert_called_once()
batch_one.query_obj.filter.return_value.order_by.return_value.limit.assert_called_once_with(2)
assert batch_one.committed is True
assert batch_one.closed is True
assert batch_two.closed is True