diff --git a/reflex/app.py b/reflex/app.py index 6e66257b4..c2abb7a19 100644 --- a/reflex/app.py +++ b/reflex/app.py @@ -580,6 +580,22 @@ class App(MiddlewareMixin, LifespanMixin): """ if not self.api: raise ValueError("The app has not been initialized.") + + # For py3.9 compatibility when redis is used, we MUST add any decorator pages + # before compiling the app in a thread to avoid event loop error (REF-2172). + self._apply_decorated_pages() + + compile_future = concurrent.futures.ThreadPoolExecutor(max_workers=1).submit( + self._compile + ) + compile_future.add_done_callback( + # Force background compile errors to print eagerly + lambda f: f.result() + ) + # Wait for the compile to finish in prod mode to ensure all optional endpoints are mounted. + if is_prod_mode(): + compile_future.result() + return self.api def _add_default_endpoints(self): diff --git a/reflex/app_module_for_backend.py b/reflex/app_module_for_backend.py deleted file mode 100644 index 28be30410..000000000 --- a/reflex/app_module_for_backend.py +++ /dev/null @@ -1,33 +0,0 @@ -"""Shims the real reflex app module for running backend server (uvicorn or gunicorn). -Only the app attribute is explicitly exposed. -""" - -from concurrent.futures import ThreadPoolExecutor - -from reflex import constants -from reflex.utils.exec import is_prod_mode -from reflex.utils.prerequisites import get_and_validate_app - -if constants.CompileVars.APP != "app": - raise AssertionError("unexpected variable name for 'app'") - -app, app_module = get_and_validate_app(reload=False) -# For py3.9 compatibility when redis is used, we MUST add any decorator pages -# before compiling the app in a thread to avoid event loop error (REF-2172). -app._apply_decorated_pages() -compile_future = ThreadPoolExecutor(max_workers=1).submit(app._compile) -compile_future.add_done_callback( - # Force background compile errors to print eagerly - lambda f: f.result() -) -# Wait for the compile to finish in prod mode to ensure all optional endpoints are mounted. -if is_prod_mode(): - compile_future.result() - -# ensure only "app" is exposed. -del app_module -del compile_future -del get_and_validate_app -del is_prod_mode -del constants -del ThreadPoolExecutor diff --git a/reflex/reflex.py b/reflex/reflex.py index 878b32d76..0322d74a7 100644 --- a/reflex/reflex.py +++ b/reflex/reflex.py @@ -15,6 +15,7 @@ from reflex.config import environment, get_config from reflex.custom_components.custom_components import custom_components_cli from reflex.state import reset_disk_state_manager from reflex.utils import console, telemetry +from reflex.utils.exec import should_use_granian # Disable typer+rich integration for help panels typer.core.rich = None # pyright: ignore [reportPrivateImportUsage] @@ -192,7 +193,9 @@ def _run( if frontend: # Get the app module. - prerequisites.get_compiled_app() + if not should_use_granian(): + # Granian fails if the app is already imported. + prerequisites.get_compiled_app() # Warn if schema is not up to date. prerequisites.check_schema_up_to_date() diff --git a/reflex/utils/exec.py b/reflex/utils/exec.py index 5474ae82a..16cdc2d2a 100644 --- a/reflex/utils/exec.py +++ b/reflex/utils/exec.py @@ -196,22 +196,9 @@ def get_app_module(): Returns: The app module for the backend. """ - return f"reflex.app_module_for_backend:{constants.CompileVars.APP}" + config = get_config() - -def get_granian_target(): - """Get the Granian target for the backend. - - Returns: - The Granian target for the backend. - """ - import reflex - - app_module_path = Path(reflex.__file__).parent / "app_module_for_backend.py" - - return ( - f"{app_module_path!s}:{constants.CompileVars.APP}.{constants.CompileVars.API}" - ) + return f"{config.module}:{constants.CompileVars.APP}" def run_backend( @@ -229,9 +216,6 @@ def run_backend( frontend_present: Whether the frontend is present. """ web_dir = get_web_dir() - # Create a .nocompile file to skip compile for backend. - if web_dir.exists(): - (web_dir / constants.NOCOMPILE_FILE).touch() if not frontend_present: notify_backend() @@ -240,6 +224,9 @@ def run_backend( if should_use_granian(): run_granian_backend(host, port, loglevel) else: + # Create a .nocompile file to skip compile for backend. + if web_dir.exists(): + (web_dir / constants.NOCOMPILE_FILE).touch() run_uvicorn_backend(host, port, loglevel) @@ -313,7 +300,8 @@ def run_uvicorn_backend(host: str, port: int, loglevel: LogLevel): import uvicorn uvicorn.run( - app=f"{get_app_module()}.{constants.CompileVars.API}", + app=f"{get_app_module()}", + factory=True, host=host, port=port, log_level=loglevel.value, @@ -339,7 +327,8 @@ def run_granian_backend(host: str, port: int, loglevel: LogLevel): from granian.log import LogLevels # pyright: ignore [reportMissingImports] Granian( - target=get_granian_target(), + target=get_app_module(), + factory=True, address=host, port=port, interface=Interfaces.ASGI, @@ -417,6 +406,7 @@ def run_uvicorn_backend_prod(host: str, port: int, loglevel: LogLevel): *("--host", host), *("--port", str(port)), *("--workers", str(_get_backend_workers())), + "--factory", app_module, ] if constants.IS_WINDOWS @@ -482,7 +472,8 @@ def run_granian_backend_prod(host: str, port: int, loglevel: LogLevel): str(port), "--interface", str(Interfaces.ASGI), - get_granian_target(), + "--factory", + get_app_module(), ] processes.new_process( command, diff --git a/tests/units/test_page.py b/tests/units/test_page.py index e1dd70905..8c9847cba 100644 --- a/tests/units/test_page.py +++ b/tests/units/test_page.py @@ -7,6 +7,7 @@ def test_page_decorator(): def foo_(): return text("foo") + DECORATED_PAGES.clear() assert len(DECORATED_PAGES) == 0 decorated_foo_ = page()(foo_) assert decorated_foo_ == foo_