test(mining): 使用实际持锁 PID 验证 Windows 进程接管

持锁脚本回传自身 PID,避免 Windows 虚拟环境启动器 PID 与解释器 PID 不同导致错误断言;测试清理核验同一进程身份,并统一 Windows CI 的 UTF-8 编码。62 项本地相关回归通过。

Signed-off-by: shy3130 <shy3130@qq.com>
This commit is contained in:
shy3130
2026-11-02 09:55:13 +08:00
parent 7e0278b395
commit 4629dd98b1
2 changed files with 18 additions and 6 deletions
+2
View File
@@ -67,6 +67,8 @@ jobs:
name: Windows mining lock recovery name: Windows mining lock recovery
runs-on: windows-latest runs-on: windows-latest
timeout-minutes: 15 timeout-minutes: 15
env:
PYTHONUTF8: '1'
defaults: defaults:
run: run:
working-directory: backend working-directory: backend
+16 -6
View File
@@ -7,10 +7,12 @@ import subprocess
import sys import sys
import time import time
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor
from contextlib import suppress
from pathlib import Path from pathlib import Path
from types import SimpleNamespace from types import SimpleNamespace
from unittest.mock import Mock from unittest.mock import Mock
import psutil
import pytest import pytest
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
@@ -35,11 +37,11 @@ from app.services.mining_process_lock import (
_BACKEND_ROOT = Path(__file__).resolve().parents[1] _BACKEND_ROOT = Path(__file__).resolve().parents[1]
_HOLDER_SCRIPT = """ _HOLDER_SCRIPT = """
import sys, time import os, sys, time
from app.services.mining_process_lock import MiningProcessLock from app.services.mining_process_lock import MiningProcessLock
lock = MiningProcessLock(sys.argv[1]) lock = MiningProcessLock(sys.argv[1])
lock.acquire(role="worker") lock.acquire(role="worker")
print("HELD", flush=True) print(f"HELD {os.getpid()}", flush=True)
time.sleep(600) time.sleep(600)
""" """
@@ -70,7 +72,10 @@ def _spawn_holder(data_dir: Path) -> subprocess.Popen:
cwd=str(_BACKEND_ROOT), cwd=str(_BACKEND_ROOT),
) )
line = proc.stdout.readline() line = proc.stdout.readline()
assert line.strip() == "HELD", f"外部持锁进程未就绪: {line!r} {proc.stderr.read()}" assert line.startswith("HELD "), f"外部持锁进程未就绪: {line!r} {proc.stderr.read()}"
# Windows venv launchers may have a different PID from the Python process
# executing the script. Verify termination against the script's own identity.
proc.holder_process = psutil.Process(int(line.split()[1]))
return proc return proc
@@ -87,10 +92,14 @@ def _spawn_legacy_holder(data_dir: Path) -> subprocess.Popen:
) )
line = proc.stdout.readline() line = proc.stdout.readline()
assert line.strip() == "HELD", f"旧格式持锁进程未就绪: {line!r} {proc.stderr.read()}" assert line.strip() == "HELD", f"旧格式持锁进程未就绪: {line!r} {proc.stderr.read()}"
proc.holder_process = psutil.Process(proc.pid)
return proc return proc
def _kill(proc: subprocess.Popen) -> None: def _kill(proc: subprocess.Popen) -> None:
# Keep the original Process object so cleanup also checks for PID reuse.
with suppress(psutil.NoSuchProcess):
proc.holder_process.kill()
if proc.poll() is None: if proc.poll() is None:
proc.kill() proc.kill()
proc.wait(timeout=10) proc.wait(timeout=10)
@@ -223,7 +232,7 @@ def test_force_release_kills_external_holder_and_takes_over(tmp_path):
lock = MiningProcessLock(tmp_path) lock = MiningProcessLock(tmp_path)
report = force_release(tmp_path, lock) report = force_release(tmp_path, lock)
assert report["released"] is True assert report["released"] is True
assert report["terminated_pid"] == proc.pid assert report["terminated_pid"] == proc.holder_process.pid
assert report["reacquired"] is True assert report["reacquired"] is True
assert lock.acquired is True assert lock.acquired is True
proc.wait(timeout=10) proc.wait(timeout=10)
@@ -359,7 +368,7 @@ def test_lock_endpoints_full_recovery_loop(tmp_path):
assert resp.status_code == 200 assert resp.status_code == 200
assert resp.json() == { assert resp.json() == {
"released": True, "released": True,
"terminated_pid": proc.pid, "terminated_pid": proc.holder_process.pid,
"reacquired": True, "reacquired": True,
} }
proc.wait(timeout=10) proc.wait(timeout=10)
@@ -599,7 +608,8 @@ def test_force_release_rechecks_confirmed_pid(tmp_path):
client = _api_client(tmp_path, acquired=False) client = _api_client(tmp_path, acquired=False)
try: try:
response = client.post( response = client.post(
"/api/backtest/mining/lock/force-release", json={"expected_pid": proc.pid + 1}, "/api/backtest/mining/lock/force-release",
json={"expected_pid": proc.holder_process.pid + 1},
) )
assert response.status_code == 409 assert response.status_code == 409
assert "持有者已变化" in response.json()["detail"] assert "持有者已变化" in response.json()["detail"]