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_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:
|
||||||
|
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 __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 <--
|
||||||
|
Loading…
Reference in New Issue
Block a user