From 1ce3569fbfd410bc9fbcc4da32704107a3302476 Mon Sep 17 00:00:00 2001 From: KronosDev-Pro Date: Sun, 12 Jan 2025 05:24:56 +0000 Subject: [PATCH] [FIX] - formatter/linter, prevent failed backend server start --- reflex/config.py | 7 ++++-- reflex/server/base.py | 7 ++---- reflex/server/granian.py | 41 +++++++++++++++----------------- reflex/server/gunicorn.py | 42 +++++++++++++++------------------ reflex/server/uvicorn.py | 35 +++++++++++----------------- reflex/utils/exec.py | 49 +++++++++++++++++++++++++++++---------- 6 files changed, 96 insertions(+), 85 deletions(-) diff --git a/reflex/config.py b/reflex/config.py index b98eab834..3f0b512d0 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -745,8 +745,11 @@ class Config(Base): "gunicorn_max_requests_jitter", ) ): - console.warn( - 'The following reflex configuration fields are obsolete: "timeout", "gunicorn_worker_class", "gunicorn_workers", "gunicorn_max_requests", "gunicorn_max_requests_jitter"\nplease update your configuration.' + console.deprecate( + 'The following reflex configuration fields are obsolete: "timeout", "gunicorn_worker_class", "gunicorn_workers", "gunicorn_max_requests", "gunicorn_max_requests_jitter"\nplease update your configuration.', + reason="Use `config.backend_server_dev` or `config.backend_server_prod` instead in your `rxconfig.py`.", + deprecation_version="0.7.x", + removal_version="x.x.x", ) @property diff --git a/reflex/server/base.py b/reflex/server/base.py index c78c90224..fdd231393 100644 --- a/reflex/server/base.py +++ b/reflex/server/base.py @@ -1,4 +1,5 @@ """The base for CustomBackendServer.""" +# ruff: noqa: RUF009 from __future__ import annotations @@ -297,11 +298,7 @@ class CustomBackendServer: else: need_threads = self.get_max_threads(wait_time_ms, service_time_ms) - return int( - max_available_threads - if need_threads > max_available_threads - else need_threads - ) + return int(min(need_threads, max_available_threads)) def get_fields(self) -> dict[str, Field]: """Return all the fields. diff --git a/reflex/server/granian.py b/reflex/server/granian.py index a87faee93..4fb61f4f2 100644 --- a/reflex/server/granian.py +++ b/reflex/server/granian.py @@ -1,8 +1,8 @@ """The GranianBackendServer.""" +# ruff: noqa: RUF009 from __future__ import annotations -import sys from dataclasses import dataclass from dataclasses import field as dc_field from pathlib import Path @@ -200,7 +200,11 @@ class GranianBackendServer(CustomBackendServer): return self.address, self.port def check_import(self): - """Check package importation.""" + """Check package importation. + + Raises: + ImportError: raise when some required packaging missing. + """ from importlib.util import find_spec errors: list[str] = [] @@ -217,7 +221,7 @@ class GranianBackendServer(CustomBackendServer): if errors: console.error("\n".join(errors)) - sys.exit() + raise ImportError() def setup(self, host: str, port: int, loglevel: LogLevel, env: Env): """Setup. @@ -228,6 +232,7 @@ class GranianBackendServer(CustomBackendServer): loglevel (LogLevel): log level env (Env): prod/dev environment """ + self.check_import() 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 @@ -235,25 +240,17 @@ class GranianBackendServer(CustomBackendServer): 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.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"] + 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 def run_prod(self): """Run in production mode. @@ -272,7 +269,7 @@ class GranianBackendServer(CustomBackendServer): ): command += field.metadata["cli"](value).split(" ") - return command + [self._app_uri] + return [*command, self._app_uri] def run_dev(self): """Run in development mode.""" diff --git a/reflex/server/gunicorn.py b/reflex/server/gunicorn.py index 5e47044cf..63a6ba9ce 100644 --- a/reflex/server/gunicorn.py +++ b/reflex/server/gunicorn.py @@ -1,8 +1,8 @@ """The GunicornBackendServer.""" +# ruff: noqa: RUF009 from __future__ import annotations -import sys from dataclasses import dataclass from typing import Any, Callable, Literal @@ -288,7 +288,11 @@ class GunicornBackendServer(CustomBackendServer): return host, int(port) def check_import(self): - """Check package importation.""" + """Check package importation. + + Raises: + ImportError: raise when some required packaging missing. + """ from importlib.util import find_spec errors: list[str] = [] @@ -305,7 +309,7 @@ class GunicornBackendServer(CustomBackendServer): if errors: console.error("\n".join(errors)) - sys.exit() + raise ImportError() def setup(self, host: str, port: int, loglevel: LogLevel, env: Env): """Setup. @@ -316,27 +320,23 @@ class GunicornBackendServer(CustomBackendServer): loglevel (LogLevel): log level env (Env): prod/dev environment """ + self.check_import() self._app_uri = f"{self.get_app_module()}()" # type: ignore self.loglevel = loglevel.value # type: ignore self.bind = [f"{host}:{port}"] 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_threads := self.get_max_workers()): - self.workers = max_threads + if self.workers == self.get_fields()["workers"].default: + self.workers = self.get_recommended_workers() + else: + if self.workers > (max_threads := self.get_max_workers()): + self.workers = max_threads - 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 - self.preload_app = True - - if env == Env.DEV: - self.reload = True + 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 def run_prod(self) -> list[str]: """Run in production mode. @@ -355,7 +355,7 @@ class GunicornBackendServer(CustomBackendServer): ): command += field.metadata["cli"](value).split(" ") - return command + [self._app_uri] + return [*command, self._app_uri] def run_dev(self): """Run in development mode.""" @@ -404,7 +404,3 @@ class GunicornBackendServer(CustomBackendServer): """Shutdown the backend server.""" if self._app and self._env == Env.DEV: self._app.stop() # type: ignore - - # TODO: complicated because currently `*BackendServer` don't execute the server command, he just create it - # if self._env == Env.PROD: - # pass diff --git a/reflex/server/uvicorn.py b/reflex/server/uvicorn.py index e164fefee..7caf91e0b 100644 --- a/reflex/server/uvicorn.py +++ b/reflex/server/uvicorn.py @@ -1,4 +1,5 @@ """The UvicornBackendServer.""" +# ruff: noqa: RUF009 from __future__ import annotations @@ -6,10 +7,8 @@ from __future__ import annotations import asyncio import os import ssl -import sys from configparser import RawConfigParser from dataclasses import dataclass -from pathlib import Path from typing import IO, Any, Awaitable, Callable from uvicorn import Config, Server @@ -192,7 +191,11 @@ class UvicornBackendServer(CustomBackendServer): return self.host, self.port def check_import(self): - """Check package importation.""" + """Check package importation. + + Raises: + ImportError: raise when some required packaging missing. + """ from importlib.util import find_spec errors: list[str] = [] @@ -211,7 +214,7 @@ class UvicornBackendServer(CustomBackendServer): if errors: console.error("\n".join(errors)) - sys.exit() + raise ImportError() def setup(self, host: str, port: int, loglevel: LogLevel, env: Env): """Setup. @@ -222,24 +225,18 @@ class UvicornBackendServer(CustomBackendServer): loglevel (LogLevel): log level env (Env): prod/dev environment """ + self.check_import() self._app_uri = self.get_app_module(add_extra_api=True) # type: ignore self.log_level = loglevel.value self.host = host self.port = port 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 env == Env.DEV: - from reflex.config import get_config # prevent circular import - - self.reload = True - self.reload_dirs = [str(Path(get_config().app_name))] + 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 def run_prod(self) -> list[str]: """Run in production mode. @@ -258,7 +255,7 @@ class UvicornBackendServer(CustomBackendServer): ): command += field.metadata["cli"](value).split(" ") - return command + [self._app_uri] + return [*command, self._app_uri] def run_dev(self): """Run in development mode.""" @@ -279,7 +276,3 @@ class UvicornBackendServer(CustomBackendServer): """Shutdown the backend server.""" if self._app and self._env == Env.DEV: self._app.shutdown() # type: ignore - - # TODO: hard because currently `*BackendServer` don't execute the server command, he just create it - # if self._env == Env.PROD: - # pass diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 12fa555e6..b30915575 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -10,6 +10,7 @@ import re import subprocess import sys from pathlib import Path +from threading import Barrier, Event from urllib.parse import urljoin import psutil @@ -22,6 +23,8 @@ from reflex.utils.prerequisites import get_web_dir # For uvicorn windows bug fix (#2335) frontend_process = None +barrier = Barrier(2) +failed_start_signal = Event() def detect_package_change(json_file_path: Path) -> str: @@ -61,8 +64,14 @@ def kill(proc_pid: int): process.kill() -def notify_backend(): - """Output a string notifying where the backend is running.""" +def notify_backend(only_backend: bool = False): + """Output a string notifying where the backend is running. + + Args: + only_backend: Whether the frontend is present. + """ + if not only_backend: + barrier.wait() console.print( f"Backend running at: [bold green]http://0.0.0.0:{get_config().backend_port}[/bold green]" ) @@ -110,8 +119,14 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True): console.print( f"App running at: [bold green]{url}[/bold green]{' (Frontend-only mode)' if not backend_present else ''}" ) + if backend_present: - notify_backend() + barrier.wait() + if failed_start_signal.is_set(): + kill(process.pid) + process = None + break + first_run = False else: console.print("New packages detected: Updating app...") @@ -130,7 +145,7 @@ def run_process_and_launch_url(run_command: list[str], backend_present=True): kill(process.pid) process = None break # for line in process.stdout - if process is not None: + if (process is not None) or (failed_start_signal.is_set() and process is None): break # while True @@ -198,12 +213,17 @@ def run_backend( if web_dir.exists(): (web_dir / constants.NOCOMPILE_FILE).touch() - if not frontend_present: - notify_backend() - # Run the backend in development mode. backend_server_dev = config.backend_server_dev - backend_server_dev.setup(host, port, loglevel, Env.DEV) + try: + backend_server_dev.setup(host, port, loglevel, Env.DEV) + except ImportError: + if frontend_present: + failed_start_signal.set() + barrier.wait() # for unlock frontend server + return + + notify_backend(not frontend_present) backend_server_dev.run_dev() @@ -225,12 +245,17 @@ def run_backend_prod( config = get_config() - if not frontend_present: - notify_backend() - # Run the backend in production mode. backend_server_prod = config.backend_server_prod - backend_server_prod.setup(host, port, loglevel, Env.PROD) + try: + backend_server_prod.setup(host, port, loglevel, Env.PROD) + except ImportError: + if frontend_present: + failed_start_signal.set() + barrier.wait() # for unlock frontend server + return + + notify_backend(not frontend_present) processes.new_process( backend_server_prod.run_prod(), run=True,