eptm_dashboard/.venv/lib/python3.12/site-packages/granian/server/mt.py

341 lines
11 KiB
Python

import sys
import threading
from collections.abc import Callable
from functools import wraps
from typing import Any
from .._futures import _future_watcher_wrapper, _new_cbscheduler
from .._granian import ASGIWorker, RSGIWorker, WorkerSignal, WorkerSignalSync, WSGIWorker
from .._loops import loops
from .._types import SSLCtx
from ..asgi import LifespanProtocol, _callback_wrapper as _asgi_call_wrap
from ..errors import ConfigurationError, FatalError
from ..rsgi import _callback_wrapper as _rsgi_call_wrap, _callbacks_from_target as _rsgi_cbs_from_target
from ..wsgi import _callback_wrapper as _wsgi_call_wrap
from .common import (
WORKERS_METHODS,
AbstractServer,
AbstractWorker,
HTTP1Settings,
HTTP2Settings,
HTTPModes,
Interfaces,
RuntimeModes,
TaskImpl,
logger,
)
class WorkerThread(AbstractWorker):
_idl = 'TID'
def __init__(self, parent, idx, target, args, sig):
self._sig = sig
super().__init__(parent, idx, target, args)
@staticmethod
def wrap_target(target):
@wraps(target)
def wrapped(worker_id, sig, callback, sock, loop_impl, *args, **kwargs):
loop = loops.get(loop_impl)
return target(worker_id, sig, callback, sock, loop, *args, **kwargs)
return wrapped
def _spawn(self, target, args):
self.inner = threading.Thread(name='granian-worker', target=target, args=args)
self._alive = True
def _id(self):
return self.inner.native_id
def _watcher(self):
self.inner.join()
self._alive = False
if not self.interrupt_by_parent:
logger.error(f'Unexpected exit from worker-{self.idx + 1}')
self.parent.interrupt_children.append(self.idx)
self.parent.main_loop_interrupt.set()
def terminate(self):
self._alive = False
self.interrupt_by_parent = True
self._sig.set()
def is_alive(self):
if not self._alive:
return False
return self.inner.is_alive()
class MTServer(AbstractServer[WorkerThread]):
@staticmethod
@WorkerThread.wrap_target
def _spawn_asgi_worker(
worker_id: int,
shutdown_event: Any,
callback: Any,
sock: Any,
loop: Any,
runtime_mode: RuntimeModes,
runtime_threads: int,
runtime_blocking_threads: int | None,
blocking_threads: int,
blocking_threads_idle_timeout: int,
backpressure: int,
task_impl: TaskImpl,
http_mode: HTTPModes,
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None] | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
metrics: Any,
):
wcallback = _future_watcher_wrapper(_asgi_call_wrap(callback, scope_opts, {}, log_access_fmt))
worker = ASGIWorker(
worker_id,
sock,
None,
runtime_threads,
runtime_blocking_threads,
blocking_threads,
blocking_threads_idle_timeout,
backpressure,
http_mode,
http1_settings,
http2_settings,
websockets,
static_path,
*ssl_ctx,
metrics,
)
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
scheduler = _new_cbscheduler(loop, wcallback, impl_asyncio=task_impl == TaskImpl.asyncio)
serve(scheduler, loop, shutdown_event)
@staticmethod
@WorkerThread.wrap_target
def _spawn_asgi_lifespan_worker(
worker_id: int,
shutdown_event: Any,
callback: Any,
sock: Any,
loop: Any,
runtime_mode: RuntimeModes,
runtime_threads: int,
runtime_blocking_threads: int | None,
blocking_threads: int,
blocking_threads_idle_timeout: int,
backpressure: int,
task_impl: TaskImpl,
http_mode: HTTPModes,
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None] | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
metrics: Any,
):
lifespan_handler = LifespanProtocol(callback)
wcallback = _future_watcher_wrapper(
_asgi_call_wrap(callback, scope_opts, lifespan_handler.state, log_access_fmt)
)
loop.run_until_complete(lifespan_handler.startup())
if lifespan_handler.interrupt:
logger.error('ASGI lifespan startup failed', exc_info=lifespan_handler.exc)
sys.exit(1)
worker = ASGIWorker(
worker_id,
sock,
None,
runtime_threads,
runtime_blocking_threads,
blocking_threads,
blocking_threads_idle_timeout,
backpressure,
http_mode,
http1_settings,
http2_settings,
websockets,
static_path,
*ssl_ctx,
metrics,
)
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
scheduler = _new_cbscheduler(loop, wcallback, impl_asyncio=task_impl == TaskImpl.asyncio)
serve(scheduler, loop, shutdown_event)
loop.run_until_complete(lifespan_handler.shutdown())
@staticmethod
@WorkerThread.wrap_target
def _spawn_rsgi_worker(
worker_id: int,
shutdown_event: Any,
callback: Any,
sock: Any,
loop: Any,
runtime_mode: RuntimeModes,
runtime_threads: int,
runtime_blocking_threads: int | None,
blocking_threads: int,
blocking_threads_idle_timeout: int,
backpressure: int,
task_impl: TaskImpl,
http_mode: HTTPModes,
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None] | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
metrics: Any,
):
callback, callback_init, callback_del = _rsgi_cbs_from_target(callback)
wcallback = _future_watcher_wrapper(_rsgi_call_wrap(callback, log_access_fmt))
callback_init(loop)
worker = RSGIWorker(
worker_id,
sock,
None,
runtime_threads,
runtime_blocking_threads,
blocking_threads,
blocking_threads_idle_timeout,
backpressure,
http_mode,
http1_settings,
http2_settings,
websockets,
static_path,
*ssl_ctx,
metrics,
)
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
scheduler = _new_cbscheduler(loop, wcallback, impl_asyncio=task_impl == TaskImpl.asyncio)
serve(scheduler, loop, shutdown_event)
callback_del(loop)
@staticmethod
@WorkerThread.wrap_target
def _spawn_wsgi_worker(
worker_id: int,
shutdown_event: Any,
callback: Any,
sock: Any,
loop: Any,
runtime_mode: RuntimeModes,
runtime_threads: int,
runtime_blocking_threads: int | None,
blocking_threads: int,
blocking_threads_idle_timeout: int,
backpressure: int,
task_impl: TaskImpl,
http_mode: HTTPModes,
http1_settings: HTTP1Settings | None,
http2_settings: HTTP2Settings | None,
websockets: bool,
static_path: tuple[str, str, str | None] | None,
log_access_fmt: str | None,
ssl_ctx: SSLCtx,
scope_opts: dict[str, Any],
metrics: Any,
):
wcallback = _wsgi_call_wrap(callback, scope_opts, log_access_fmt)
worker = WSGIWorker(
worker_id,
sock,
None,
runtime_threads,
runtime_blocking_threads,
blocking_threads,
blocking_threads_idle_timeout,
backpressure,
http_mode,
http1_settings,
http2_settings,
static_path,
*ssl_ctx,
metrics,
)
serve = getattr(worker, WORKERS_METHODS[runtime_mode][sock.is_uds()])
scheduler = _new_cbscheduler(loop, wcallback, impl_asyncio=task_impl == TaskImpl.asyncio)
serve(scheduler, loop, shutdown_event)
def _spawn_worker(self, idx, target, callback_loader) -> WorkerThread:
sig = WorkerSignalSync(threading.Event()) if self.interface == Interfaces.WSGI else WorkerSignal()
return WorkerThread(
parent=self,
idx=idx,
target=target,
args=(
idx + 1,
sig,
callback_loader,
self._shd,
self.loop,
self.runtime_mode,
self.runtime_threads,
self.runtime_blocking_threads,
self.blocking_threads,
self.blocking_threads_idle_timeout,
self.backpressure,
self.task_impl,
self.http,
self.http1_settings,
self.http2_settings,
self.websockets,
self.static_path,
self.log_access_format if self.log_access else None,
self.ssl_ctx,
{'url_path_prefix': self.url_path_prefix},
(self.metrics_scrape_interval if self.metrics_enabled else None, self._metrics),
),
sig=sig,
)
def _check_gil(self):
try:
assert sys._is_gil_enabled() is False
except Exception:
logger.error('Cannot run a free-threaded Granian build with GIL enabled')
raise FatalError('gil')
def _serve(self, spawn_target, target_loader):
target = target_loader()
self._check_gil()
self.startup(spawn_target, target)
self._serve_loop(spawn_target, target)
self.shutdown()
def _serve_with_reloader(self, spawn_target, target_loader):
raise NotImplementedError
def serve(
self,
spawn_target: Callable[..., None] | None = None,
target_loader: Callable[..., Callable[..., Any]] | None = None,
wrap_loader: bool = True,
):
logger.warning('free-threaded Python support is experimental!')
if self.reload_on_changes:
logger.error('The changes reloader is not supported on the free-threaded build')
raise ConfigurationError('reload')
if self.workers_rss:
logger.error('The resource monitor is not supported on the free-threaded build')
raise ConfigurationError('workers_max_rss')
super().serve(spawn_target, target_loader, wrap_loader)