Небольшие доработки по трейсу
This commit is contained in:
+110
-3
@@ -5,6 +5,8 @@ from dataclasses import dataclass, field
|
||||
from threading import Event, Lock, Thread
|
||||
from time import sleep
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from app_runtime.contracts.application import ApplicationModule
|
||||
from app_runtime.contracts.health import HealthContributor
|
||||
from app_runtime.contracts.worker import Worker, WorkerHealth, WorkerStatus
|
||||
@@ -176,6 +178,28 @@ class BlockingModule(ApplicationModule):
|
||||
registry.add_worker(RoutineWorker("blocking-worker", self.routine))
|
||||
|
||||
|
||||
class WorkerModuleAdapter(ApplicationModule):
|
||||
def __init__(self, worker: Worker) -> None:
|
||||
self._worker = worker
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
return "worker-adapter"
|
||||
|
||||
def register(self, registry: ModuleRegistry) -> None:
|
||||
registry.add_worker(self._worker)
|
||||
|
||||
|
||||
class ForceRecordingWorker(RoutineWorker):
|
||||
def __init__(self) -> None:
|
||||
super().__init__("force-recorder", CollectingRoutine(), interval=0.01)
|
||||
self.stop_flags: list[bool] = []
|
||||
|
||||
def stop(self, force: bool = False) -> None:
|
||||
self.stop_flags.append(force)
|
||||
super().stop(force=force)
|
||||
|
||||
|
||||
class RecordingTransport(NoOpTraceTransport):
|
||||
def __init__(self) -> None:
|
||||
self.contexts: list[object] = []
|
||||
@@ -188,6 +212,21 @@ class RecordingTransport(NoOpTraceTransport):
|
||||
self.messages.append(record)
|
||||
|
||||
|
||||
def _build_control_client(runtime: RuntimeManager, *, control_timeout: int = 1) -> TestClient:
|
||||
from app_runtime.control.base import ControlActionSet
|
||||
from app_runtime.control.http_channel import HttpControlChannel
|
||||
|
||||
channel = HttpControlChannel("127.0.0.1", 0, control_timeout)
|
||||
channel._actions = ControlActionSet(
|
||||
health=runtime.health_status,
|
||||
start=runtime.start_runtime,
|
||||
stop=runtime.stop_runtime,
|
||||
status=runtime.runtime_status,
|
||||
)
|
||||
app = channel._factory.create(channel._health_response, channel._action_response)
|
||||
return TestClient(app)
|
||||
|
||||
|
||||
def test_runtime_runs_worker_routine_and_exposes_status(tmp_path) -> None:
|
||||
config_path = tmp_path / "config.yml"
|
||||
config_path.write_text(
|
||||
@@ -302,15 +341,15 @@ def test_http_control_channel_exposes_health_and_actions() -> None:
|
||||
async def health():
|
||||
return {"status": "ok" if state["started"] else "unhealthy", "state": "idle" if state["started"] else "stopped"}
|
||||
|
||||
async def start_handler() -> str:
|
||||
async def start_handler(_request) -> str:
|
||||
state["started"] = True
|
||||
return "started"
|
||||
|
||||
async def stop_handler() -> str:
|
||||
async def stop_handler(_request) -> str:
|
||||
state["started"] = False
|
||||
return "stopped"
|
||||
|
||||
async def status_handler() -> str:
|
||||
async def status_handler(_request) -> str:
|
||||
return "idle" if state["started"] else "stopped"
|
||||
|
||||
async def scenario() -> None:
|
||||
@@ -339,6 +378,74 @@ def test_http_control_channel_exposes_health_and_actions() -> None:
|
||||
asyncio.run(scenario())
|
||||
|
||||
|
||||
def test_http_control_stop_wait_false_returns_immediately() -> None:
|
||||
started = Event()
|
||||
release = Event()
|
||||
runtime = RuntimeManager()
|
||||
runtime.register_module(BlockingModule(started, release))
|
||||
runtime.start(start_control_plane=False)
|
||||
assert started.wait(timeout=1.0) is True
|
||||
client = _build_control_client(runtime)
|
||||
|
||||
try:
|
||||
response = client.post("/actions/stop?wait=false")
|
||||
payload = response.json()
|
||||
assert response.status_code == 200
|
||||
assert payload["detail"]["timed_out"] is False
|
||||
assert payload["detail"]["state"] == "stopping"
|
||||
|
||||
health_response = client.get("/health")
|
||||
health_payload = health_response.json()
|
||||
assert health_response.status_code == 503
|
||||
assert health_payload["state"] == "stopping"
|
||||
assert health_payload["status"] == "degraded"
|
||||
finally:
|
||||
release.set()
|
||||
client.close()
|
||||
runtime.stop()
|
||||
|
||||
|
||||
def test_http_control_stop_timeout_query_changes_wait_window() -> None:
|
||||
started = Event()
|
||||
release = Event()
|
||||
runtime = RuntimeManager()
|
||||
runtime.ACTION_TIMEOUT_SECONDS = 5.0
|
||||
runtime.register_module(BlockingModule(started, release))
|
||||
runtime.start(start_control_plane=False)
|
||||
assert started.wait(timeout=1.0) is True
|
||||
client = _build_control_client(runtime)
|
||||
|
||||
try:
|
||||
response = client.post("/actions/stop?timeout=0.1")
|
||||
payload = response.json()
|
||||
assert response.status_code == 200
|
||||
assert payload["detail"]["timed_out"] is True
|
||||
assert payload["detail"]["state"] == "stopping"
|
||||
finally:
|
||||
release.set()
|
||||
client.close()
|
||||
runtime.stop()
|
||||
|
||||
|
||||
def test_http_control_stop_force_query_propagates_to_worker() -> None:
|
||||
runtime = RuntimeManager()
|
||||
worker = ForceRecordingWorker()
|
||||
runtime.register_module(WorkerModuleAdapter(worker))
|
||||
runtime.start(start_control_plane=False)
|
||||
client = _build_control_client(runtime)
|
||||
|
||||
try:
|
||||
response = client.post("/actions/stop?force=true")
|
||||
payload = response.json()
|
||||
assert response.status_code == 200
|
||||
assert payload["detail"]["timed_out"] is False
|
||||
assert payload["detail"]["state"] == "stopped"
|
||||
assert worker.stop_flags == [True]
|
||||
finally:
|
||||
client.close()
|
||||
runtime.stop()
|
||||
|
||||
|
||||
def test_public_plba_package_exports_runtime_builder_and_worker_contract(tmp_path) -> None:
|
||||
import plba
|
||||
from plba import ApplicationModule as PublicApplicationModule
|
||||
|
||||
Reference in New Issue
Block a user