Round 4 fixes: scheduler DB save check, empty validation_targets, proxy list page fallback, scheduler frontend state sync, tests
This commit is contained in:
@@ -16,31 +16,41 @@ export function useScheduler() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function startScheduler(onSuccess) {
|
async function startScheduler(onSuccess, onError) {
|
||||||
schedulerLoading.value = true
|
schedulerLoading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await schedulerService.start()
|
const response = await schedulerService.start()
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
schedulerRunning.value = true
|
schedulerRunning.value = response.data.running
|
||||||
|
if (response.data.running) {
|
||||||
onSuccess?.('自动验证已启动')
|
onSuccess?.('自动验证已启动')
|
||||||
|
} else {
|
||||||
|
onError?.('启动调度器失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('启动调度器失败:', error)
|
console.error('启动调度器失败:', error)
|
||||||
|
onError?.('启动调度器失败')
|
||||||
} finally {
|
} finally {
|
||||||
schedulerLoading.value = false
|
schedulerLoading.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function stopScheduler(onSuccess) {
|
async function stopScheduler(onSuccess, onError) {
|
||||||
schedulerLoading.value = true
|
schedulerLoading.value = true
|
||||||
try {
|
try {
|
||||||
const response = await schedulerService.stop()
|
const response = await schedulerService.stop()
|
||||||
if (response.code === 200) {
|
if (response.code === 200) {
|
||||||
schedulerRunning.value = false
|
schedulerRunning.value = response.data.running
|
||||||
|
if (!response.data.running) {
|
||||||
onSuccess?.('自动验证已停止')
|
onSuccess?.('自动验证已停止')
|
||||||
|
} else {
|
||||||
|
onError?.('停止调度器失败')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('停止调度器失败:', error)
|
console.error('停止调度器失败:', error)
|
||||||
|
onError?.('停止调度器失败')
|
||||||
} finally {
|
} finally {
|
||||||
schedulerLoading.value = false
|
schedulerLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -202,6 +202,10 @@ async function fetchProxies() {
|
|||||||
|
|
||||||
if (!success) {
|
if (!success) {
|
||||||
ElMessage.error('获取代理列表失败')
|
ElMessage.error('获取代理列表失败')
|
||||||
|
} else if (proxyStore.proxies.length === 0 && currentPage.value > 1) {
|
||||||
|
// 当前页无数据时自动回退一页
|
||||||
|
currentPage.value -= 1
|
||||||
|
return fetchProxies()
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.name === 'AbortError') {
|
if (error.name === 'AbortError') {
|
||||||
|
|||||||
@@ -290,11 +290,17 @@ async function fetchSettings() {
|
|||||||
|
|
||||||
// ==================== 调度器控制 ====================
|
// ==================== 调度器控制 ====================
|
||||||
async function handleStartScheduler() {
|
async function handleStartScheduler() {
|
||||||
await startScheduler((msg) => ElMessage.success(msg))
|
await startScheduler(
|
||||||
|
(msg) => ElMessage.success(msg),
|
||||||
|
(msg) => ElMessage.error(msg)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleStopScheduler() {
|
async function handleStopScheduler() {
|
||||||
await stopScheduler((msg) => ElMessage.success(msg))
|
await stopScheduler(
|
||||||
|
(msg) => ElMessage.success(msg),
|
||||||
|
(msg) => ElMessage.error(msg)
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function handleValidateNow() {
|
async function handleValidateNow() {
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ async def lifespan(app: FastAPI):
|
|||||||
connect_timeout=app_settings.validator_connect_timeout,
|
connect_timeout=app_settings.validator_connect_timeout,
|
||||||
max_concurrency=db_settings.get("default_concurrency", app_settings.validator_max_concurrency),
|
max_concurrency=db_settings.get("default_concurrency", app_settings.validator_max_concurrency),
|
||||||
)
|
)
|
||||||
if db_settings.get("validation_targets"):
|
if db_settings.get("validation_targets") is not None:
|
||||||
validator.update_test_urls(db_settings["validation_targets"])
|
validator.update_test_urls(db_settings["validation_targets"])
|
||||||
|
|
||||||
# 验证 WorkerPool
|
# 验证 WorkerPool
|
||||||
|
|||||||
@@ -13,7 +13,9 @@ router = APIRouter(prefix="/api/scheduler", tags=["scheduler"])
|
|||||||
async def _save_auto_validate_setting(enabled: bool, settings_repo: SettingsRepository):
|
async def _save_auto_validate_setting(enabled: bool, settings_repo: SettingsRepository):
|
||||||
"""保存自动验证设置"""
|
"""保存自动验证设置"""
|
||||||
async with get_db() as db:
|
async with get_db() as db:
|
||||||
await settings_repo.save(db, {"auto_validate": enabled})
|
success = await settings_repo.save(db, {"auto_validate": enabled})
|
||||||
|
if not success:
|
||||||
|
raise RuntimeError("保存自动验证设置失败")
|
||||||
|
|
||||||
|
|
||||||
@router.post("/start")
|
@router.post("/start")
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ async def save_settings(
|
|||||||
validator._init_timeout = request.validation_timeout
|
validator._init_timeout = request.validation_timeout
|
||||||
validator._init_connect_timeout = request.validation_timeout
|
validator._init_connect_timeout = request.validation_timeout
|
||||||
validator._init_max_concurrency = request.default_concurrency
|
validator._init_max_concurrency = request.default_concurrency
|
||||||
if request.validation_targets:
|
if request.validation_targets is not None:
|
||||||
validator.update_test_urls(request.validation_targets)
|
validator.update_test_urls(request.validation_targets)
|
||||||
# 先关闭现有 session,再重置 semaphore,避免竞态窗口
|
# 先关闭现有 session,再重置 semaphore,避免竞态窗口
|
||||||
await validator.close()
|
await validator.close()
|
||||||
|
|||||||
@@ -92,6 +92,25 @@ class TestSchedulerAPI:
|
|||||||
job = executor.get_job(job_id)
|
job = executor.get_job(job_id)
|
||||||
assert job is not None
|
assert job is not None
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_start_scheduler_db_save_failure(self, client, monkeypatch):
|
||||||
|
"""测试启动调度器时数据库保存失败应返回 running=False"""
|
||||||
|
from app.repositories.settings_repo import SettingsRepository
|
||||||
|
|
||||||
|
# lifespan 启动时调度器可能已自动启动,先停止它
|
||||||
|
await client.post("/api/scheduler/stop")
|
||||||
|
|
||||||
|
async def mock_save(*args, **kwargs):
|
||||||
|
return False
|
||||||
|
|
||||||
|
monkeypatch.setattr(SettingsRepository, "save", mock_save)
|
||||||
|
response = await client.post("/api/scheduler/start")
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["code"] == 200
|
||||||
|
assert data["data"]["running"] is False
|
||||||
|
assert "失败" in data["message"]
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_scheduler_full_workflow(self, client):
|
async def test_scheduler_full_workflow(self, client):
|
||||||
"""测试调度器完整工作流"""
|
"""测试调度器完整工作流"""
|
||||||
|
|||||||
@@ -164,3 +164,22 @@ class TestSettingsAPI:
|
|||||||
response = await client.get("/api/settings")
|
response = await client.get("/api/settings")
|
||||||
saved_settings = response.json()["data"]
|
saved_settings = response.json()["data"]
|
||||||
assert saved_settings["validation_targets"] == test_settings["validation_targets"]
|
assert saved_settings["validation_targets"] == test_settings["validation_targets"]
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_save_settings_empty_validation_targets(self, client):
|
||||||
|
"""测试保存空的 validation_targets"""
|
||||||
|
response = await client.get("/api/settings")
|
||||||
|
current_settings = response.json()["data"]
|
||||||
|
|
||||||
|
new_settings = current_settings.copy()
|
||||||
|
new_settings["validation_targets"] = []
|
||||||
|
|
||||||
|
response = await client.post("/api/settings", json=new_settings)
|
||||||
|
assert response.status_code == 200
|
||||||
|
data = response.json()
|
||||||
|
assert data["data"]["validation_targets"] == []
|
||||||
|
|
||||||
|
# 读取确认
|
||||||
|
response = await client.get("/api/settings")
|
||||||
|
saved_settings = response.json()["data"]
|
||||||
|
assert saved_settings["validation_targets"] == []
|
||||||
|
|||||||
Reference in New Issue
Block a user