Единый http сервис на одном порту
This commit is contained in:
@@ -0,0 +1,46 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app_runtime.control.base import ControlActionRequest, ControlActionSet
|
||||
|
||||
|
||||
class ControlActionResponder:
|
||||
def __init__(self, actions: ControlActionSet, timeout: int) -> None:
|
||||
self._actions = actions
|
||||
self._timeout = timeout
|
||||
|
||||
async def respond(
|
||||
self,
|
||||
action: str,
|
||||
_client_source: str = "unknown",
|
||||
request: ControlActionRequest | None = None,
|
||||
) -> JSONResponse:
|
||||
callbacks = {
|
||||
"start": self._actions.start,
|
||||
"stop": self._actions.stop,
|
||||
"status": self._actions.status,
|
||||
}
|
||||
callback = callbacks.get(action)
|
||||
if callback is None:
|
||||
return JSONResponse(content={"status": "error", "detail": f"unsupported action: {action}"}, status_code=404)
|
||||
action_request = request or ControlActionRequest()
|
||||
action_timeout = self._action_timeout(action, action_request)
|
||||
try:
|
||||
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"},
|
||||
status_code=202,
|
||||
)
|
||||
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 @@ import asyncio
|
||||
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
from app_runtime.control.action_responder import ControlActionResponder
|
||||
from app_runtime.control.base import ControlActionRequest, ControlActionSet, ControlChannel, TraceQueryRequest
|
||||
from app_runtime.contracts.trace import TraceLogView
|
||||
from app_runtime.control.http_app import HttpControlAppFactory
|
||||
@@ -16,9 +17,11 @@ class HttpControlChannel(ControlChannel):
|
||||
self._runner = UvicornThreadRunner(host, port, timeout)
|
||||
self._factory = HttpControlAppFactory()
|
||||
self._actions: ControlActionSet | None = None
|
||||
self._action_responder: ControlActionResponder | None = None
|
||||
|
||||
async def start(self, actions: ControlActionSet) -> None:
|
||||
self._actions = actions
|
||||
self._action_responder = ControlActionResponder(actions, self._timeout)
|
||||
app = self._factory.create(self._health_response, self._action_response, self._trace_response)
|
||||
await self._runner.start(app)
|
||||
|
||||
@@ -40,34 +43,14 @@ class HttpControlChannel(ControlChannel):
|
||||
_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 = {
|
||||
"start": self._actions.start,
|
||||
"stop": self._actions.stop,
|
||||
"status": self._actions.status,
|
||||
}
|
||||
callback = callbacks.get(action)
|
||||
if callback is None:
|
||||
return JSONResponse(content={"status": "error", "detail": f"unsupported action: {action}"}, status_code=404)
|
||||
action_request = request or ControlActionRequest()
|
||||
action_timeout = self._action_timeout(action, action_request)
|
||||
try:
|
||||
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"},
|
||||
status_code=202,
|
||||
)
|
||||
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)
|
||||
if self._action_responder is None:
|
||||
if self._actions is None:
|
||||
return JSONResponse(
|
||||
content={"status": "error", "detail": f"{action} handler is not configured"},
|
||||
status_code=404,
|
||||
)
|
||||
self._action_responder = ControlActionResponder(self._actions, self._timeout)
|
||||
return await self._action_responder.respond(action, _client_source, request)
|
||||
|
||||
async def _trace_response(self, trace_id: str, request: TraceQueryRequest) -> TraceLogView:
|
||||
if self._actions is None or self._actions.trace_lookup is None:
|
||||
|
||||
@@ -2,6 +2,7 @@ from app_runtime.http.base import ApplicationHttpChannel, HttpRouteRegistrar
|
||||
from app_runtime.http.http_app import HttpApplicationAppFactory
|
||||
from app_runtime.http.http_channel import HttpApplicationChannel
|
||||
from app_runtime.http.service import ApplicationHttpService
|
||||
from app_runtime.http.unified_service import UnifiedHttpService
|
||||
|
||||
__all__ = [
|
||||
"ApplicationHttpChannel",
|
||||
@@ -9,4 +10,5 @@ __all__ = [
|
||||
"HttpApplicationAppFactory",
|
||||
"HttpApplicationChannel",
|
||||
"HttpRouteRegistrar",
|
||||
"UnifiedHttpService",
|
||||
]
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from app_runtime.control.action_responder import ControlActionResponder
|
||||
from app_runtime.control.base import ControlActionSet
|
||||
from app_runtime.control.http_app import HttpControlAppFactory
|
||||
from app_runtime.http.base import ApplicationHttpChannel, HttpRouteRegistrar
|
||||
from app_runtime.http.http_app import HttpApplicationAppFactory
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from app_runtime.core.runtime import RuntimeManager
|
||||
|
||||
|
||||
class UnifiedHttpService:
|
||||
"""Publish control and application routes through the same HTTP channel."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
app_factory: HttpApplicationAppFactory | None = None,
|
||||
control_app_factory: HttpControlAppFactory | None = None,
|
||||
control_timeout: int = 5,
|
||||
) -> None:
|
||||
self._channels: list[ApplicationHttpChannel] = []
|
||||
self._registrars: list[HttpRouteRegistrar] = []
|
||||
self._app_factory = app_factory or HttpApplicationAppFactory()
|
||||
self._control_app_factory = control_app_factory or HttpControlAppFactory()
|
||||
self._control_timeout = control_timeout
|
||||
|
||||
def register_channel(self, channel: ApplicationHttpChannel) -> None:
|
||||
self._channels.append(channel)
|
||||
|
||||
def register_routes(self, registrar: HttpRouteRegistrar) -> None:
|
||||
self._registrars.append(registrar)
|
||||
|
||||
def start(self, runtime: RuntimeManager) -> None:
|
||||
if not self._channels:
|
||||
return
|
||||
asyncio.run(self._start_async(runtime))
|
||||
|
||||
def stop(self) -> None:
|
||||
if not self._channels:
|
||||
return
|
||||
asyncio.run(self._stop_async())
|
||||
|
||||
async def _start_async(self, runtime: RuntimeManager) -> None:
|
||||
app = self._app_factory.create(self._registrars, runtime.services)
|
||||
actions = ControlActionSet(
|
||||
health=runtime.health_status,
|
||||
start=runtime.start_runtime,
|
||||
stop=runtime.stop_runtime,
|
||||
status=runtime.runtime_status,
|
||||
trace_lookup=runtime.trace_logs,
|
||||
)
|
||||
action_responder = ControlActionResponder(actions, self._control_timeout)
|
||||
control_app = self._control_app_factory.create(
|
||||
actions.health,
|
||||
action_responder.respond,
|
||||
actions.trace_lookup,
|
||||
)
|
||||
app.router.routes.extend(control_app.router.routes)
|
||||
for channel in self._channels:
|
||||
await channel.start(app)
|
||||
|
||||
async def _stop_async(self) -> None:
|
||||
for channel in reversed(self._channels):
|
||||
await channel.stop()
|
||||
@@ -21,6 +21,7 @@ from plba.http import (
|
||||
HttpApplicationAppFactory,
|
||||
HttpApplicationChannel,
|
||||
HttpRouteRegistrar,
|
||||
UnifiedHttpService,
|
||||
)
|
||||
from plba.logging import LogManager
|
||||
from plba.queue import InMemoryTaskQueue
|
||||
@@ -56,6 +57,7 @@ __all__ = [
|
||||
"HttpApplicationChannel",
|
||||
"HttpRouteRegistrar",
|
||||
"HttpControlChannel",
|
||||
"UnifiedHttpService",
|
||||
"InMemoryTaskQueue",
|
||||
"LogManager",
|
||||
"MySqlTraceTransport",
|
||||
|
||||
@@ -2,6 +2,7 @@ from app_runtime.http.base import ApplicationHttpChannel, HttpRouteRegistrar
|
||||
from app_runtime.http.http_app import HttpApplicationAppFactory
|
||||
from app_runtime.http.http_channel import HttpApplicationChannel
|
||||
from app_runtime.http.service import ApplicationHttpService
|
||||
from app_runtime.http.unified_service import UnifiedHttpService
|
||||
|
||||
__all__ = [
|
||||
"ApplicationHttpChannel",
|
||||
@@ -9,4 +10,5 @@ __all__ = [
|
||||
"HttpApplicationAppFactory",
|
||||
"HttpApplicationChannel",
|
||||
"HttpRouteRegistrar",
|
||||
"UnifiedHttpService",
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user