first iteration, GunicornBackendServer work for prod mode
This commit is contained in:
parent
1e7a37bcf9
commit
ea06469370
@ -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:
|
||||
|
6
reflex/server/__init__.py
Normal file
6
reflex/server/__init__.py
Normal 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
14
reflex/server/base.py
Normal 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
36
reflex/server/granian.py
Normal 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
1195
reflex/server/gunicorn.py
Normal file
File diff suppressed because it is too large
Load Diff
10
reflex/server/uvicorn.py
Normal file
10
reflex/server/uvicorn.py
Normal file
@ -0,0 +1,10 @@
|
||||
from reflex.server.base import CustomBackendServer
|
||||
|
||||
|
||||
class UvicornBackendServer(CustomBackendServer):
|
||||
|
||||
def run_prod(self):
|
||||
pass
|
||||
|
||||
def run_dev(self):
|
||||
pass
|
@ -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 <--
|
||||
|
Loading…
Reference in New Issue
Block a user