Небольшие доработки по трейсу
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
from app_runtime.control.base import ControlActionSet, ControlChannel
|
||||
from app_runtime.control.base import ControlActionRequest, ControlActionSet, ControlChannel
|
||||
from app_runtime.control.http_channel import HttpControlChannel
|
||||
from app_runtime.control.service import ControlPlaneService
|
||||
|
||||
__all__ = ["ControlActionSet", "ControlChannel", "ControlPlaneService", "HttpControlChannel"]
|
||||
__all__ = ["ControlActionRequest", "ControlActionSet", "ControlChannel", "ControlPlaneService", "HttpControlChannel"]
|
||||
|
||||
@@ -6,7 +6,16 @@ from dataclasses import dataclass
|
||||
|
||||
from app_runtime.core.types import HealthPayload
|
||||
|
||||
ActionHandler = Callable[[], Awaitable[str]]
|
||||
|
||||
@dataclass(slots=True)
|
||||
class ControlActionRequest:
|
||||
wait: bool | None = None
|
||||
timeout: float | None = None
|
||||
force: bool | None = None
|
||||
|
||||
|
||||
ActionResult = str | dict[str, object]
|
||||
ActionHandler = Callable[[ControlActionRequest], Awaitable[ActionResult]]
|
||||
HealthHandler = Callable[[], Awaitable[HealthPayload]]
|
||||
|
||||
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import time
|
||||
from collections.abc import Awaitable, Callable
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app_runtime.control.base import ControlActionRequest
|
||||
from app_runtime.core.types import HealthPayload
|
||||
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class HttpControlAppFactory:
|
||||
def create(
|
||||
self,
|
||||
health_provider: Callable[[], Awaitable[HealthPayload]],
|
||||
action_provider: Callable[[str], Awaitable[JSONResponse]],
|
||||
action_provider: Callable[[str, str, ControlActionRequest], Awaitable[JSONResponse]],
|
||||
) -> FastAPI:
|
||||
app = FastAPI(title="PLBA Control API")
|
||||
|
||||
@@ -32,7 +36,53 @@ class HttpControlAppFactory:
|
||||
|
||||
@app.get("/actions/{action}")
|
||||
@app.post("/actions/{action}")
|
||||
async def action(action: str) -> JSONResponse:
|
||||
return await action_provider(action)
|
||||
async def action(action: str, request: Request) -> JSONResponse:
|
||||
client_source = self._client_source(request)
|
||||
if action in {"start", "stop"}:
|
||||
LOGGER.warning("Control action requested: /actions/%s client=%s", action, client_source)
|
||||
try:
|
||||
action_request = self._action_request(request)
|
||||
except ValueError as exc:
|
||||
return JSONResponse(content={"status": "error", "detail": str(exc)}, status_code=400)
|
||||
return await action_provider(action, client_source, action_request)
|
||||
|
||||
return app
|
||||
|
||||
def _action_request(self, request: Request) -> ControlActionRequest:
|
||||
return ControlActionRequest(
|
||||
wait=self._bool_param(request, "wait"),
|
||||
timeout=self._float_param(request, "timeout"),
|
||||
force=self._bool_param(request, "force"),
|
||||
)
|
||||
|
||||
def _client_source(self, request: Request) -> str:
|
||||
explicit_header = request.headers.get("X-Client-Source", "").strip()
|
||||
if explicit_header:
|
||||
return explicit_header
|
||||
user_agent = request.headers.get("User-Agent", "").strip()
|
||||
if user_agent:
|
||||
return f"user-agent:{user_agent}"
|
||||
return "unknown"
|
||||
|
||||
def _bool_param(self, request: Request, name: str) -> bool | None:
|
||||
raw_value = request.query_params.get(name)
|
||||
if raw_value is None:
|
||||
return None
|
||||
normalized = raw_value.strip().lower()
|
||||
if normalized in {"1", "true", "yes", "on"}:
|
||||
return True
|
||||
if normalized in {"0", "false", "no", "off"}:
|
||||
return False
|
||||
raise ValueError(f"invalid boolean query parameter: {name}={raw_value}")
|
||||
|
||||
def _float_param(self, request: Request, name: str) -> float | None:
|
||||
raw_value = request.query_params.get(name)
|
||||
if raw_value is None:
|
||||
return None
|
||||
try:
|
||||
value = float(raw_value)
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"invalid numeric query parameter: {name}={raw_value}") from exc
|
||||
if value < 0:
|
||||
raise ValueError(f"query parameter must be >= 0: {name}={raw_value}")
|
||||
return value
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app_runtime.control.base import ControlActionSet, ControlChannel
|
||||
from app_runtime.control.base import ControlActionRequest, ControlActionSet, ControlChannel
|
||||
from app_runtime.control.http_app import HttpControlAppFactory
|
||||
from app_runtime.control.http_runner import UvicornThreadRunner
|
||||
|
||||
@@ -33,7 +33,12 @@ class HttpControlChannel(ControlChannel):
|
||||
return {"status": "unhealthy", "detail": "control actions are not configured"}
|
||||
return await asyncio.wait_for(self._actions.health(), timeout=float(self._timeout))
|
||||
|
||||
async def _action_response(self, action: str) -> JSONResponse:
|
||||
async def _action_response(
|
||||
self,
|
||||
action: str,
|
||||
_client_source: str = "unknown",
|
||||
request: ControlActionRequest | None = None,
|
||||
) -> JSONResponse:
|
||||
if self._actions is None:
|
||||
return JSONResponse(content={"status": "error", "detail": f"{action} handler is not configured"}, status_code=404)
|
||||
callbacks = {
|
||||
@@ -44,9 +49,10 @@ class HttpControlChannel(ControlChannel):
|
||||
callback = callbacks.get(action)
|
||||
if callback is None:
|
||||
return JSONResponse(content={"status": "error", "detail": f"unsupported action: {action}"}, status_code=404)
|
||||
action_timeout = max(float(self._timeout), 10.0) if action in {"start", "stop"} else float(self._timeout)
|
||||
action_request = request or ControlActionRequest()
|
||||
action_timeout = self._action_timeout(action, action_request)
|
||||
try:
|
||||
detail = await asyncio.wait_for(callback(), timeout=action_timeout)
|
||||
detail = await asyncio.wait_for(callback(action_request), timeout=action_timeout)
|
||||
except asyncio.TimeoutError:
|
||||
return JSONResponse(
|
||||
content={"status": "accepted", "detail": f"{action} operation is still in progress"},
|
||||
@@ -55,3 +61,9 @@ class HttpControlChannel(ControlChannel):
|
||||
except Exception as exc:
|
||||
return JSONResponse(content={"status": "error", "detail": str(exc)}, status_code=500)
|
||||
return JSONResponse(content={"status": "ok", "detail": detail or f"{action} action accepted"}, status_code=200)
|
||||
|
||||
def _action_timeout(self, action: str, request: ControlActionRequest) -> float:
|
||||
base_timeout = max(float(self._timeout), 10.0) if action in {"start", "stop"} else float(self._timeout)
|
||||
if action != "stop" or request.wait is False or request.timeout is None:
|
||||
return base_timeout
|
||||
return max(base_timeout, float(request.timeout) + 1.0)
|
||||
|
||||
@@ -4,6 +4,7 @@ from time import monotonic, sleep
|
||||
|
||||
from app_runtime.config.providers import FileConfigProvider
|
||||
from app_runtime.contracts.application import ApplicationModule
|
||||
from app_runtime.control.base import ControlActionRequest
|
||||
from app_runtime.control.service import ControlPlaneService
|
||||
from app_runtime.core.configuration import ConfigurationManager
|
||||
from app_runtime.core.registration import ModuleRegistry
|
||||
@@ -87,7 +88,7 @@ class RuntimeManager:
|
||||
async def health_status(self) -> HealthPayload:
|
||||
return self.current_health()
|
||||
|
||||
async def start_runtime(self) -> dict[str, object] | str:
|
||||
async def start_runtime(self, _request: ControlActionRequest) -> dict[str, object] | str:
|
||||
self._refresh_state()
|
||||
if self._started:
|
||||
return "runtime already running"
|
||||
@@ -100,24 +101,29 @@ class RuntimeManager:
|
||||
return self._action_detail("runtime started", timed_out=False)
|
||||
return self._action_detail("runtime start is still in progress", timed_out=True)
|
||||
|
||||
async def stop_runtime(self) -> dict[str, object] | str:
|
||||
async def stop_runtime(self, request: ControlActionRequest) -> dict[str, object] | str:
|
||||
self._refresh_state()
|
||||
if not self._started:
|
||||
if self._state == LifecycleState.STOPPING:
|
||||
return self._action_detail("runtime stop is still in progress", timed_out=True)
|
||||
return "runtime already stopped"
|
||||
|
||||
wait = True if request.wait is None else request.wait
|
||||
timeout = self.ACTION_TIMEOUT_SECONDS if request.timeout is None else float(request.timeout)
|
||||
force = False if request.force is None else request.force
|
||||
self._state = LifecycleState.STOPPING
|
||||
try:
|
||||
self.workers.stop(timeout=self.ACTION_TIMEOUT_SECONDS, force=False)
|
||||
self.workers.stop(timeout=timeout, force=force, wait=wait)
|
||||
except TimeoutError:
|
||||
return self._action_detail("runtime stop is still in progress", timed_out=True)
|
||||
|
||||
self._started = False
|
||||
self._state = LifecycleState.STOPPED
|
||||
return self._action_detail("runtime stopped", timed_out=False)
|
||||
self._refresh_state()
|
||||
if self._state == LifecycleState.STOPPED:
|
||||
self._started = False
|
||||
return self._action_detail("runtime stopped", timed_out=False)
|
||||
return self._action_detail("runtime stop requested", timed_out=False)
|
||||
|
||||
async def runtime_status(self) -> str:
|
||||
async def runtime_status(self, _request: ControlActionRequest) -> str:
|
||||
self._refresh_state()
|
||||
return self._state.value
|
||||
|
||||
|
||||
@@ -18,10 +18,10 @@ class WorkerSupervisor:
|
||||
for worker in self._workers:
|
||||
worker.start()
|
||||
|
||||
def stop(self, timeout: float = 30.0, force: bool = False) -> None:
|
||||
def stop(self, timeout: float = 30.0, force: bool = False, wait: bool = True) -> None:
|
||||
for worker in self._workers:
|
||||
worker.stop(force=force)
|
||||
if force:
|
||||
if not wait:
|
||||
return
|
||||
deadline = monotonic() + timeout
|
||||
while True:
|
||||
|
||||
@@ -133,7 +133,17 @@ class WorkflowRepository:
|
||||
None,
|
||||
"running",
|
||||
self._connection_factory.dumps(snapshot),
|
||||
runtime.get("email_trace_id"),
|
||||
self._resolve_trace_id(runtime),
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _resolve_trace_id(runtime: dict[str, Any]) -> str | None:
|
||||
return (
|
||||
runtime.get("trace_id")
|
||||
or runtime.get("task_trace_id")
|
||||
or runtime.get("order_trace_id")
|
||||
or runtime.get("attachment_trace_id")
|
||||
or runtime.get("email_trace_id")
|
||||
)
|
||||
|
||||
def _use_memory(self) -> bool:
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from plba.bootstrap import create_runtime
|
||||
from plba.config import ConfigFileLoader, FileConfigProvider
|
||||
from plba.control import ControlActionSet, ControlChannel, ControlPlaneService, HttpControlChannel
|
||||
from plba.control import ControlActionRequest, ControlActionSet, ControlChannel, ControlPlaneService, HttpControlChannel
|
||||
from plba.contracts import (
|
||||
ApplicationModule,
|
||||
ConfigProvider,
|
||||
@@ -35,6 +35,7 @@ __all__ = [
|
||||
"ConfigFileLoader",
|
||||
"ConfigProvider",
|
||||
"ConfigurationManager",
|
||||
"ControlActionRequest",
|
||||
"ControlActionSet",
|
||||
"ControlChannel",
|
||||
"ControlPlaneService",
|
||||
|
||||
+2
-1
@@ -1,8 +1,9 @@
|
||||
from app_runtime.control.base import ControlActionSet, ControlChannel
|
||||
from app_runtime.control.base import ControlActionRequest, ControlActionSet, ControlChannel
|
||||
from app_runtime.control.http_channel import HttpControlChannel
|
||||
from app_runtime.control.service import ControlPlaneService
|
||||
|
||||
__all__ = [
|
||||
"ControlActionRequest",
|
||||
"ControlActionSet",
|
||||
"ControlChannel",
|
||||
"ControlPlaneService",
|
||||
|
||||
Reference in New Issue
Block a user