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 import constants
from reflex import constants, server
from reflex.base import Base
from reflex.utils import console
@ -649,7 +649,7 @@ class Config(Base):
# Tailwind config.
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
# Whether to enable or disable nextJS gzip compression.
@ -666,16 +666,16 @@ class Config(Base):
# The hosting service frontend URL.
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"
# Number of gunicorn workers from user
# Number of gunicorn workers from user; deprecated
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
# Variance limit for max requests; gunicorn only
# Variance limit for max requests; gunicorn only; deprecated
gunicorn_max_requests_jitter: int = 25
# 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.
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):
"""Initialize the config values.
@ -706,6 +717,7 @@ class Config(Base):
Raises:
ConfigError: If some values in the config are invalid.
"""
print("[reflex.config::Config] start")
super().__init__(*args, **kwargs)
# Update the config from environment variables.
@ -725,6 +737,14 @@ class Config(Base):
raise ConfigError(
"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
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 abc import abstractmethod, ABCMeta
from typing import IO, Any, Literal, Sequence, Type
import hashlib
import json
import os
@ -9,12 +12,18 @@ import platform
import re
import subprocess
import sys
import asyncio
import ssl
from configparser import RawConfigParser
from pathlib import Path
from urllib.parse import urljoin
from pydantic import Field
from dataclasses import dataclass
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.constants.base import LogLevel
from reflex.utils import console, path_ops
@ -194,6 +203,7 @@ def get_app_module():
The app module for the backend.
"""
return f"reflex.app_module_for_backend:{constants.CompileVars.APP}"
### REWORK <--
def get_granian_target():
@ -316,65 +326,36 @@ def run_backend_prod(
loglevel: The log level.
frontend_present: Whether the frontend is present.
"""
print("[reflex.utils.exec::run_backend_prod] start")
if not frontend_present:
notify_backend()
config = get_config()
if should_use_granian():
run_granian_backend_prod(host, port, loglevel)
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):
"""Run the backend in production mode using Uvicorn.
Args:
host: The app host
port: The app port
loglevel: The log level.
"""
from reflex.utils import processes
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
)
print(backend_server_prod.run_prod())
processes.new_process(
backend_server_prod.run_prod(),
run=True,
show_logs=True,
env={
environment.REFLEX_SKIP_COMPILE.name: "true"
}, # skip compile for prod backend
)
print("[reflex.utils.exec::run_backend_prod] done")
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():
"""Show system information if the loglevel is in DEBUG."""
if console._LOG_LEVEL > constants.LogLevel.DEBUG:
@ -540,3 +522,6 @@ def should_skip_compile() -> bool:
removal_version="0.7.0",
)
return environment.REFLEX_SKIP_COMPILE.get()
### REWORK <--