first iteration, GunicornBackendServer work for prod mode

This commit is contained in:
KronosDev-Pro 2024-11-08 02:20:08 +00:00
parent 1e7a37bcf9
commit ea06469370
7 changed files with 1324 additions and 58 deletions

View File

@ -36,7 +36,7 @@ except ModuleNotFoundError:
from reflex_cli.constants.hosting import Hosting from reflex_cli.constants.hosting import Hosting
from reflex import constants from reflex import constants, server
from reflex.base import Base from reflex.base import Base
from reflex.utils import console from reflex.utils import console
@ -649,7 +649,7 @@ class Config(Base):
# Tailwind config. # Tailwind config.
tailwind: Optional[Dict[str, Any]] = {"plugins": ["@tailwindcss/typography"]} tailwind: Optional[Dict[str, Any]] = {"plugins": ["@tailwindcss/typography"]}
# Timeout when launching the gunicorn server. TODO(rename this to backend_timeout?) # Timeout when launching the gunicorn server. TODO(rename this to backend_timeout?); deprecated
timeout: int = 120 timeout: int = 120
# Whether to enable or disable nextJS gzip compression. # Whether to enable or disable nextJS gzip compression.
@ -666,16 +666,16 @@ class Config(Base):
# The hosting service frontend URL. # The hosting service frontend URL.
cp_web_url: str = Hosting.HOSTING_SERVICE_UI cp_web_url: str = Hosting.HOSTING_SERVICE_UI
# The worker class used in production mode # The worker class used in production mode; deprecated
gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker" gunicorn_worker_class: str = "uvicorn.workers.UvicornH11Worker"
# Number of gunicorn workers from user # Number of gunicorn workers from user; deprecated
gunicorn_workers: Optional[int] = None gunicorn_workers: Optional[int] = None
# Number of requests before a worker is restarted # Number of requests before a worker is restarted; deprecated
gunicorn_max_requests: int = 100 gunicorn_max_requests: int = 100
# Variance limit for max requests; gunicorn only # Variance limit for max requests; gunicorn only; deprecated
gunicorn_max_requests_jitter: int = 25 gunicorn_max_requests_jitter: int = 25
# Indicate which type of state manager to use # Indicate which type of state manager to use
@ -696,6 +696,17 @@ class Config(Base):
# Path to file containing key-values pairs to override in the environment; Dotenv format. # Path to file containing key-values pairs to override in the environment; Dotenv format.
env_file: Optional[str] = None env_file: Optional[str] = None
# Custom Backend Server
backend_server_prod: server.CustomBackendServer = server.GunicornBackendServer(
app=f"reflex.app_module_for_backend:{constants.CompileVars.APP}.{constants.CompileVars.API}",
worker_class="uvicorn.workers.UvicornH11Worker", # type: ignore
max_requests=100,
max_requests_jitter=25,
preload_app=True,
timeout=120,
)
backend_server_dev: server.CustomBackendServer = server.UvicornBackendServer()
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
"""Initialize the config values. """Initialize the config values.
@ -706,6 +717,7 @@ class Config(Base):
Raises: Raises:
ConfigError: If some values in the config are invalid. ConfigError: If some values in the config are invalid.
""" """
print("[reflex.config::Config] start")
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
# Update the config from environment variables. # Update the config from environment variables.
@ -725,6 +737,14 @@ class Config(Base):
raise ConfigError( raise ConfigError(
"REDIS_URL is required when using the redis state manager." "REDIS_URL is required when using the redis state manager."
) )
print("[reflex.config::Config] --")
for key in ("timeout", "gunicorn_worker_class", "gunicorn_workers", "gunicorn_max_requests", "gunicorn_max_requests_jitter"):
if isinstance(self.backend_server_prod, server.GunicornBackendServer):
value = self.get_value(key)
if value != self.backend_server_prod.get_fields()[key.replace("gunicorn_", "")].default and value is not None:
setattr(self.backend_server_prod, key.replace("gunicorn_", ""), value)
print("[reflex.config::Config] done")
@property @property
def module(self) -> str: def module(self) -> str:

View File

@ -0,0 +1,6 @@
from .base import CustomBackendServer
from .granian import GranianBackendServer
from .gunicorn import GunicornBackendServer
from .uvicorn import UvicornBackendServer

14
reflex/server/base.py Normal file
View File

@ -0,0 +1,14 @@
from abc import abstractmethod, ABCMeta
from reflex.base import Base
class CustomBackendServer(Base):
@abstractmethod
def run_prod(self):
raise NotImplementedError()
@abstractmethod
def run_dev(self):
raise NotImplementedError()

36
reflex/server/granian.py Normal file
View File

@ -0,0 +1,36 @@
from dataclasses import dataclass
from reflex.server.base import CustomBackendServer
@dataclass
class HTTP1Settings: # https://github.com/emmett-framework/granian/blob/261ceba3fd93bca10300e91d1498bee6df9e3576/granian/http.py#L6
keep_alive: bool = True
max_buffer_size: int = 8192 + 4096 * 100
pipeline_flush: bool = False
@dataclass
class HTTP2Settings: # https://github.com/emmett-framework/granian/blob/261ceba3fd93bca10300e91d1498bee6df9e3576/granian/http.py#L13
adaptive_window: bool = False
initial_connection_window_size: int = 1024 * 1024
initial_stream_window_size: int = 1024 * 1024
keep_alive_interval: int | None = None
keep_alive_timeout: int = 20
max_concurrent_streams: int = 200
max_frame_size: int = 1024 * 16
max_headers_size: int = 16 * 1024 * 1024
max_send_buffer_size: int = 1024 * 400
try:
import watchfiles
except ImportError:
watchfiles = None
class GranianBackendServer(CustomBackendServer):
def run_prod(self):
pass
def run_dev(self):
pass

1195
reflex/server/gunicorn.py Normal file

File diff suppressed because it is too large Load Diff

10
reflex/server/uvicorn.py Normal file
View File

@ -0,0 +1,10 @@
from reflex.server.base import CustomBackendServer
class UvicornBackendServer(CustomBackendServer):
def run_prod(self):
pass
def run_dev(self):
pass

View File

@ -2,6 +2,9 @@
from __future__ import annotations from __future__ import annotations
from abc import abstractmethod, ABCMeta
from typing import IO, Any, Literal, Sequence, Type
import hashlib import hashlib
import json import json
import os import os
@ -9,12 +12,18 @@ import platform
import re import re
import subprocess import subprocess
import sys import sys
import asyncio
import ssl
from configparser import RawConfigParser
from pathlib import Path from pathlib import Path
from urllib.parse import urljoin from urllib.parse import urljoin
from pydantic import Field
from dataclasses import dataclass
import psutil import psutil
from reflex import constants from reflex import constants, server
from reflex.base import Base
from reflex.config import environment, get_config from reflex.config import environment, get_config
from reflex.constants.base import LogLevel from reflex.constants.base import LogLevel
from reflex.utils import console, path_ops from reflex.utils import console, path_ops
@ -194,6 +203,7 @@ def get_app_module():
The app module for the backend. The app module for the backend.
""" """
return f"reflex.app_module_for_backend:{constants.CompileVars.APP}" return f"reflex.app_module_for_backend:{constants.CompileVars.APP}"
### REWORK <--
def get_granian_target(): def get_granian_target():
@ -316,65 +326,36 @@ def run_backend_prod(
loglevel: The log level. loglevel: The log level.
frontend_present: Whether the frontend is present. frontend_present: Whether the frontend is present.
""" """
print("[reflex.utils.exec::run_backend_prod] start")
if not frontend_present: if not frontend_present:
notify_backend() notify_backend()
config = get_config()
if should_use_granian(): if should_use_granian():
run_granian_backend_prod(host, port, loglevel) run_granian_backend_prod(host, port, loglevel)
else: else:
run_uvicorn_backend_prod(host, port, loglevel) from reflex.utils import processes
backend_server_prod = config.backend_server_prod
if isinstance(backend_server_prod, server.GunicornBackendServer):
backend_server_prod.app = f"{get_app_module()}()"
backend_server_prod.preload_app = True
backend_server_prod.loglevel = loglevel.value # type: ignore
backend_server_prod.bind = [f"{host}:{port}"]
backend_server_prod.threads = _get_backend_workers()
backend_server_prod.workers = _get_backend_workers()
def run_uvicorn_backend_prod(host, port, loglevel): print(backend_server_prod.run_prod())
"""Run the backend in production mode using Uvicorn. processes.new_process(
backend_server_prod.run_prod(),
Args: run=True,
host: The app host show_logs=True,
port: The app port env={
loglevel: The log level. environment.REFLEX_SKIP_COMPILE.name: "true"
""" }, # skip compile for prod backend
from reflex.utils import processes )
print("[reflex.utils.exec::run_backend_prod] done")
config = get_config()
app_module = get_app_module()
RUN_BACKEND_PROD = f"gunicorn --worker-class {config.gunicorn_worker_class} --max-requests {config.gunicorn_max_requests} --max-requests-jitter {config.gunicorn_max_requests_jitter} --preload --timeout {config.timeout} --log-level critical".split()
RUN_BACKEND_PROD_WINDOWS = f"uvicorn --limit-max-requests {config.gunicorn_max_requests} --timeout-keep-alive {config.timeout}".split()
command = (
[
*RUN_BACKEND_PROD_WINDOWS,
"--host",
host,
"--port",
str(port),
app_module,
]
if constants.IS_WINDOWS
else [
*RUN_BACKEND_PROD,
"--bind",
f"{host}:{port}",
"--threads",
str(_get_backend_workers()),
f"{app_module}()",
]
)
command += [
"--log-level",
loglevel.value,
"--workers",
str(_get_backend_workers()),
]
processes.new_process(
command,
run=True,
show_logs=True,
env={
environment.REFLEX_SKIP_COMPILE.name: "true"
}, # skip compile for prod backend
)
def run_granian_backend_prod(host, port, loglevel): def run_granian_backend_prod(host, port, loglevel):
@ -418,6 +399,7 @@ def run_granian_backend_prod(host, port, loglevel):
) )
### REWORK-->
def output_system_info(): def output_system_info():
"""Show system information if the loglevel is in DEBUG.""" """Show system information if the loglevel is in DEBUG."""
if console._LOG_LEVEL > constants.LogLevel.DEBUG: if console._LOG_LEVEL > constants.LogLevel.DEBUG:
@ -540,3 +522,6 @@ def should_skip_compile() -> bool:
removal_version="0.7.0", removal_version="0.7.0",
) )
return environment.REFLEX_SKIP_COMPILE.get() return environment.REFLEX_SKIP_COMPILE.get()
### REWORK <--