Небольшие доработки по трейсу

This commit is contained in:
2026-04-26 20:47:13 +03:00
parent 314e6f3c46
commit ed33f6e9cd
16 changed files with 725 additions and 27 deletions
+110 -3
View File
@@ -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