mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
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:
@@ -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
|
||||||
|
|||||||
@@ -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"]
|
||||||
|
|||||||
Reference in New Issue
Block a user