reflex/reflex/server/granian.py
2025-01-11 21:34:11 +00:00

332 lines
12 KiB
Python

"""The GranianBackendServer."""
from __future__ import annotations
import sys
from dataclasses import dataclass
from dataclasses import field as dc_field
from pathlib import Path
from typing import Any, Literal
from reflex.constants.base import Env, LogLevel
from reflex.server.base import CliType, CustomBackendServer, field_
from reflex.utils import console
@dataclass
class HTTP1Settings:
"""Granian HTTP1Settings."""
# https://github.com/emmett-framework/granian/blob/261ceba3fd93bca10300e91d1498bee6df9e3576/granian/http.py#L6
keep_alive: bool = dc_field(default=True)
max_buffer_size: int = dc_field(default=8192 + 4096 * 100)
pipeline_flush: bool = dc_field(default=False)
@dataclass
class HTTP2Settings: # https://github.com/emmett-framework/granian/blob/261ceba3fd93bca10300e91d1498bee6df9e3576/granian/http.py#L13
"""Granian HTTP2Settings."""
adaptive_window: bool = dc_field(default=False)
initial_connection_window_size: int = dc_field(default=1024 * 1024)
initial_stream_window_size: int = dc_field(default=1024 * 1024)
keep_alive_interval: int | None = dc_field(default=None)
keep_alive_timeout: int = dc_field(default=20)
max_concurrent_streams: int = dc_field(default=200)
max_frame_size: int = dc_field(default=1024 * 16)
max_headers_size: int = dc_field(default=16 * 1024 * 1024)
max_send_buffer_size: int = dc_field(default=1024 * 400)
@dataclass
class GranianBackendServer(CustomBackendServer):
"""Granian backendServer.
https://github.com/emmett-framework/granian/blob/fc11808ed177362fcd9359a455a733065ddbc505/granian/cli.py#L52 (until Granian has the proper documentation)
"""
address: str = field_(
default="127.0.0.1", metadata_cli=CliType.default("--host {value}")
)
port: int = field_(default=8000, metadata_cli=CliType.default("--port {value}"))
interface: Literal["asgi", "asginl", "rsgi", "wsgi"] = field_(
default="rsgi", metadata_cli=CliType.default("--interface {value}")
)
workers: int = field_(default=0, metadata_cli=CliType.default("--workers {value}"))
threads: int = field_(default=0, metadata_cli=CliType.default("--threads {value}"))
blocking_threads: int | None = field_(
default=None, metadata_cli=CliType.default("--blocking-threads {value}")
)
threading_mode: Literal["runtime", "workers"] = field_(
default="workers", metadata_cli=CliType.default("--threading-mode {value}")
)
loop: Literal["auto", "asyncio", "uvloop"] = field_(
default="auto", metadata_cli=CliType.default("--loop {value}")
)
loop_opt: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}opt"),
)
http: Literal["auto", "1", "2"] = field_(
default="auto", metadata_cli=CliType.default("--http {value}")
)
websockets: bool = field_(
default=True, metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}ws")
)
backlog: int = field_(
default=1024, metadata_cli=CliType.default("--backlog {value}")
)
backpressure: int | None = field_(
default=None, metadata_cli=CliType.default("--backpressure {value}")
)
http1_keep_alive: bool = field_(
default=True, metadata_cli=CliType.default("--http1-keep-alive {value}")
)
http1_max_buffer_size: int = field_(
default=417792, metadata_cli=CliType.default("--http1-max-buffer-size {value}")
)
http1_pipeline_flush: bool = field_(
default=False, metadata_cli=CliType.default("--http1-pipeline-flush {value}")
)
http2_adaptive_window: bool = field_(
default=False, metadata_cli=CliType.default("--http2-adaptive-window {value}")
)
http2_initial_connection_window_size: int = field_(
default=1048576,
metadata_cli=CliType.default("--http2-initial-connection-window-size {value}"),
)
http2_initial_stream_window_size: int = field_(
default=1048576,
metadata_cli=CliType.default("--http2-initial-stream-window-size {value}"),
)
http2_keep_alive_interval: int | None = field_(
default=None,
metadata_cli=CliType.default("--http2-keep-alive-interval {value}"),
)
http2_keep_alive_timeout: int = field_(
default=20, metadata_cli=CliType.default("--http2-keep-alive-timeout {value}")
)
http2_max_concurrent_streams: int = field_(
default=200,
metadata_cli=CliType.default("--http2-max-concurrent-streams {value}"),
)
http2_max_frame_size: int = field_(
default=16384, metadata_cli=CliType.default("--http2-max-frame-size {value}")
)
http2_max_headers_size: int = field_(
default=16777216,
metadata_cli=CliType.default("--http2-max-headers-size {value}"),
)
http2_max_send_buffer_size: int = field_(
default=409600,
metadata_cli=CliType.default("--http2-max-send-buffer-size {value}"),
)
log_enabled: bool = field_(
default=True,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}log"),
)
log_level: Literal["critical", "error", "warning", "warn", "info", "debug"] = (
field_(default="info", metadata_cli=CliType.default("--log-level {value}"))
)
log_dictconfig: dict[str, Any] | None = field_(default=None, metadata_cli=None)
log_access: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}log-access"),
)
log_access_format: str | None = field_(
default=None, metadata_cli=CliType.default("--access-log-fmt {value}")
)
ssl_cert: Path | None = field_(
default=None, metadata_cli=CliType.default("--ssl-certificate {value}")
)
ssl_key: Path | None = field_(
default=None, metadata_cli=CliType.default("--ssl-keyfile {value}")
)
ssl_key_password: str | None = field_(
default=None, metadata_cli=CliType.default("--ssl-keyfile-password {value}")
)
url_path_prefix: str | None = field_(
default=None, metadata_cli=CliType.default("--url-path-prefix {value}")
)
respawn_failed_workers: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle(
"--{toggle_kw}{toggle_sep}respawn-failed-workers"
),
)
respawn_interval: float = field_(
default=3.5, metadata_cli=CliType.default("--respawn-interval {value}")
)
workers_lifetime: int | None = field_(
default=None, metadata_cli=CliType.default("--workers-lifetime {value}")
)
factory: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}factory"),
)
reload: bool = field_(
default=False,
metadata_cli=CliType.boolean_toggle("--{toggle_kw}{toggle_sep}reload"),
)
reload_paths: list[Path] | None = field_(
default=None, metadata_cli=CliType.multiple("--reload-paths {value}")
)
reload_ignore_dirs: list[str] | None = field_(
default=None, metadata_cli=CliType.multiple("--reload-ignore-dirs {value}")
)
reload_ignore_patterns: list[str] | None = field_(
default=None, metadata_cli=CliType.multiple("--reload-ignore-patterns {value}")
)
reload_ignore_paths: list[Path] | None = field_(
default=None, metadata_cli=CliType.multiple("--reload-ignore-paths {value}")
)
reload_filter: object | None = field_( # type: ignore
default=None, metadata_cli=None
)
process_name: str | None = field_(
default=None, metadata_cli=CliType.default("--process-name {value}")
)
pid_file: Path | None = field_(
default=None, metadata_cli=CliType.default("--pid-file {value}")
)
def get_backend_bind(self) -> tuple[str, int]:
"""Return the backend host and port.
Returns:
tuple[str, int]: The host address and port.
"""
return self.address, self.port
def check_import(self):
"""Check package importation."""
from importlib.util import find_spec
errors: list[str] = []
if find_spec("granian") is None:
errors.append(
'The `granian` package is required to run `GranianBackendServer`. Run `pip install "granian>=1.6.0"`.'
)
if find_spec("watchfiles") is None and self.reload:
errors.append(
'Using `--reload` in `GranianBackendServer` requires the `watchfiles` extra. Run `pip install "watchfiles~=0.21"`.'
)
if errors:
console.error("\n".join(errors))
sys.exit()
def setup(self, host: str, port: int, loglevel: LogLevel, env: Env):
"""Setup.
Args:
host (str): host address
port (int): port address
loglevel (LogLevel): log level
env (Env): prod/dev environment
"""
self._app_uri = self.get_app_module(for_granian_target=True, add_extra_api=True) # type: ignore
self.log_level = loglevel.value # type: ignore
self.address = host
self.port = port
self.interface = "asgi" # NOTE: prevent obvious error
self._env = env # type: ignore
if env == Env.PROD:
if self.workers == self.get_fields()["workers"].default:
self.workers = self.get_recommended_workers()
else:
if self.workers > (max_workers := self.get_max_workers()):
self.workers = max_workers
if self.threads == self.get_fields()["threads"].default:
self.threads = self.get_recommended_threads()
else:
if self.threads > (max_threads := self.get_max_threads()):
self.threads = max_threads
if env == Env.DEV:
from reflex.config import get_config # prevent circular import
self.reload = True
self.reload_paths = [Path(get_config().app_name)]
self.reload_ignore_dirs = [".web"]
def run_prod(self):
"""Run in production mode.
Returns:
list[str]: Command ready to be executed
"""
self.check_import()
command = ["granian"]
for key, field in self.get_fields().items():
if (
field.metadata["exclude"] is False
and field.metadata["cli"]
and not self.is_default_value(key, (value := getattr(self, key)))
):
command += field.metadata["cli"](value).split(" ")
return command + [self._app_uri]
def run_dev(self):
"""Run in development mode."""
self.check_import()
from granian import Granian # type: ignore
exclude_keys = (
"http1_keep_alive",
"http1_max_buffer_size",
"http1_pipeline_flush",
"http2_adaptive_window",
"http2_initial_connection_window_size",
"http2_initial_stream_window_size",
"http2_keep_alive_interval",
"http2_keep_alive_timeout",
"http2_max_concurrent_streams",
"http2_max_frame_size",
"http2_max_headers_size",
"http2_max_send_buffer_size",
)
self._app = Granian( # type: ignore
**{
**{
key: value
for key, value in self.get_values().items()
if (
key not in exclude_keys
and not self.is_default_value(key, value)
)
},
"target": self._app_uri,
"http1_settings": HTTP1Settings(
self.http1_keep_alive,
self.http1_max_buffer_size,
self.http1_pipeline_flush,
),
"http2_settings": HTTP2Settings(
self.http2_adaptive_window,
self.http2_initial_connection_window_size,
self.http2_initial_stream_window_size,
self.http2_keep_alive_interval,
self.http2_keep_alive_timeout,
self.http2_max_concurrent_streams,
self.http2_max_frame_size,
self.http2_max_headers_size,
self.http2_max_send_buffer_size,
),
}
)
self._app.serve()
async def shutdown(self):
"""Shutdown the backend server."""
if self._app and self._env == Env.DEV:
self._app.shutdown()