From d1cda3a792f4d463eb106a6cf3cc5df78b04275f Mon Sep 17 00:00:00 2001 From: Lendemor Date: Thu, 24 Jul 2025 19:04:06 +0200 Subject: [PATCH 01/11] allow leak and block detection on handlers --- pyproject.toml | 1 + reflex/config.py | 15 +++++ reflex/monitoring/__init__.py | 15 +++++ reflex/monitoring/pyleak.py | 109 ++++++++++++++++++++++++++++++++++ reflex/state.py | 9 ++- 5 files changed, 147 insertions(+), 2 deletions(-) create mode 100644 reflex/monitoring/__init__.py create mode 100644 reflex/monitoring/pyleak.py diff --git a/pyproject.toml b/pyproject.toml index 2a8f5b5d710..099bfbf41c9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ "platformdirs >=4.3.7,<5.0", "psutil >=7.0.0,<8.0; sys_platform == 'win32'", "pydantic >=1.10.21,<3.0", + "pyleak >=0.1.14,<1.0", "python-multipart >=0.0.20,<1.0", "python-socketio >=5.12.0,<6.0", "redis >=5.2.1,<7.0", diff --git a/reflex/config.py b/reflex/config.py index b4970c7515e..3009fbcf3b5 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -29,6 +29,9 @@ from reflex.utils import console from reflex.utils.exceptions import ConfigError +if TYPE_CHECKING: + from pyleak.base import LeakAction + @dataclasses.dataclass(kw_only=True) class DBConfig: @@ -186,6 +189,18 @@ class BaseConfig: # Telemetry opt-in. telemetry_enabled: bool = True + # PyLeak monitoring configuration for detecting event loop blocking and resource leaks. + enable_pyleak_monitoring: bool = False + + # Threshold in seconds for detecting event loop blocking operations. + pyleak_blocking_threshold: float = 0.001 + + # Grace period in seconds for thread leak detection cleanup. + pyleak_thread_grace_period: float = 0.2 + + # Action to take when PyLeak detects issues + pyleak_action: "LeakAction | None" = None + # The bun path bun_path: ExistingPath = constants.Bun.DEFAULT_PATH diff --git a/reflex/monitoring/__init__.py b/reflex/monitoring/__init__.py new file mode 100644 index 00000000000..27f025e3335 --- /dev/null +++ b/reflex/monitoring/__init__.py @@ -0,0 +1,15 @@ +"""Monitoring utilities for Reflex applications.""" + +from reflex.monitoring.pyleak import ( + is_pyleak_enabled, + monitor_async, + monitor_leaks, + monitor_sync, +) + +__all__ = [ + "is_pyleak_enabled", + "monitor_async", + "monitor_leaks", + "monitor_sync", +] diff --git a/reflex/monitoring/pyleak.py b/reflex/monitoring/pyleak.py new file mode 100644 index 00000000000..071a6240f16 --- /dev/null +++ b/reflex/monitoring/pyleak.py @@ -0,0 +1,109 @@ +"""PyLeak integration for monitoring event loop blocking and resource leaks in Reflex applications.""" + +import contextlib +from collections.abc import Callable + +from pyleak import no_event_loop_blocking, no_task_leaks, no_thread_leaks +from pyleak.base import LeakAction + +from reflex.config import get_config + + +def is_pyleak_enabled() -> bool: + """Check if PyLeak monitoring is enabled and available. + + Returns: + True if PyLeak monitoring is enabled in config. + """ + config = get_config() + return config.enable_pyleak_monitoring + + +@contextlib.contextmanager +def monitor_sync(): + """Sync context manager for PyLeak monitoring. + + Yields: + None: Context for monitoring sync operations. + """ + if not is_pyleak_enabled(): + yield + return + + config = get_config() + action = config.pyleak_action or LeakAction.WARN + + with contextlib.ExitStack() as stack: + stack.enter_context( + no_thread_leaks( + action=action, + grace_period=config.pyleak_thread_grace_period, + ) + ) + stack.enter_context( + no_event_loop_blocking( + action=action, + threshold=config.pyleak_blocking_threshold, + ) + ) + yield + + +@contextlib.asynccontextmanager +async def monitor_async(): + """Async context manager for PyLeak monitoring. + + Yields: + None: Context for monitoring async operations. + """ + if not is_pyleak_enabled(): + yield + return + + config = get_config() + action = config.pyleak_action or LeakAction.WARN + + async with contextlib.AsyncExitStack() as stack: + stack.enter_context( + no_thread_leaks( + action=action, + grace_period=config.pyleak_thread_grace_period, + ) + ) + stack.enter_context( + no_event_loop_blocking( + action=action, + threshold=config.pyleak_blocking_threshold, + ) + ) + await stack.enter_async_context(no_task_leaks(action=action)) + yield + + +def monitor_leaks(): + """Framework decorator using the monitoring module's context manager. + + Returns: + Decorator function that applies PyLeak monitoring to sync/async functions. + """ + import asyncio + import functools + + def decorator(func: Callable): + if asyncio.iscoroutinefunction(func): + + @functools.wraps(func) + async def async_wrapper(*args, **kwargs): + async with monitor_async(): + return await func(*args, **kwargs) + + return async_wrapper + + @functools.wraps(func) + def sync_wrapper(*args, **kwargs): + with monitor_sync(): + return func(*args, **kwargs) + + return sync_wrapper # pyright: ignore[reportReturnType] + + return decorator diff --git a/reflex/state.py b/reflex/state.py index 692f71cb6ca..573d2ee51d4 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -43,6 +43,7 @@ from reflex.istate.proxy import MutableProxy, StateProxy from reflex.istate.storage import ClientStorageBase from reflex.model import Model +from reflex.monitoring import is_pyleak_enabled, monitor_leaks from reflex.utils import console, format, prerequisites, types from reflex.utils.exceptions import ( ComputedVarShadowsBaseVarsError, @@ -1784,7 +1785,11 @@ async def _process_event( from reflex.utils import telemetry # Get the function to process the event. - fn = functools.partial(handler.fn, state) + if is_pyleak_enabled(): + console.debug(f"Monitoring leaks for handler: {handler.fn.__qualname__}") + fn = functools.partial(monitor_leaks()(handler.fn), state) + else: + fn = functools.partial(handler.fn, state) try: type_hints = typing.get_type_hints(handler.fn) @@ -1869,7 +1874,7 @@ async def _process_event( # Handle regular event chains. else: - yield await state._as_state_update(handler, events, final=True) + yield await state._as_state_update(handler, events, final=True) # pyright: ignore[reportArgumentType] # If an error occurs, throw a window alert. except Exception as ex: From 25ffc2a8970d46a5c1f8d53da1bb3824ca9fe068 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Thu, 24 Jul 2025 19:34:13 +0200 Subject: [PATCH 02/11] optional deps and simpler file tree --- pyproject.toml | 8 ++++- .../{monitoring/pyleak.py => monitoring.py} | 31 ++++++++++++------- reflex/monitoring/__init__.py | 15 --------- 3 files changed, 27 insertions(+), 27 deletions(-) rename reflex/{monitoring/pyleak.py => monitoring.py} (70%) delete mode 100644 reflex/monitoring/__init__.py diff --git a/pyproject.toml b/pyproject.toml index 099bfbf41c9..45a7e4c9897 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,7 +29,6 @@ dependencies = [ "platformdirs >=4.3.7,<5.0", "psutil >=7.0.0,<8.0; sys_platform == 'win32'", "pydantic >=1.10.21,<3.0", - "pyleak >=0.1.14,<1.0", "python-multipart >=0.0.20,<1.0", "python-socketio >=5.12.0,<6.0", "redis >=5.2.1,<7.0", @@ -40,6 +39,12 @@ dependencies = [ "typing_extensions >=4.13.0", "wrapt >=1.17.0,<2.0", ] + +[project.optional-dependencies] +monitoring = [ + "pyleak >=0.1.14,<1.0", +] + classifiers = [ "Development Status :: 4 - Beta", "License :: OSI Approved :: Apache Software License", @@ -75,6 +80,7 @@ dev = [ "pre-commit", "psutil", "psycopg[binary]", + "pyleak >=0.1.14,<1.0", "pyright", "pytest-asyncio", "pytest-benchmark", diff --git a/reflex/monitoring/pyleak.py b/reflex/monitoring.py similarity index 70% rename from reflex/monitoring/pyleak.py rename to reflex/monitoring.py index 071a6240f16..36993f87749 100644 --- a/reflex/monitoring/pyleak.py +++ b/reflex/monitoring.py @@ -3,18 +3,27 @@ import contextlib from collections.abc import Callable -from pyleak import no_event_loop_blocking, no_task_leaks, no_thread_leaks -from pyleak.base import LeakAction - from reflex.config import get_config +try: + from pyleak import no_event_loop_blocking, no_task_leaks, no_thread_leaks + from pyleak.base import LeakAction + + PYLEAK_AVAILABLE = True +except ImportError: + PYLEAK_AVAILABLE = False + no_event_loop_blocking = no_task_leaks = no_thread_leaks = None # pyright: ignore[reportAssignmentType] + LeakAction = None # pyright: ignore[reportAssignmentType] + def is_pyleak_enabled() -> bool: """Check if PyLeak monitoring is enabled and available. Returns: - True if PyLeak monitoring is enabled in config. + True if PyLeak monitoring is enabled in config and PyLeak is available. """ + if not PYLEAK_AVAILABLE: + return False config = get_config() return config.enable_pyleak_monitoring @@ -31,17 +40,17 @@ def monitor_sync(): return config = get_config() - action = config.pyleak_action or LeakAction.WARN + action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess] with contextlib.ExitStack() as stack: stack.enter_context( - no_thread_leaks( + no_thread_leaks( # pyright: ignore[reportOptionalCall] action=action, grace_period=config.pyleak_thread_grace_period, ) ) stack.enter_context( - no_event_loop_blocking( + no_event_loop_blocking( # pyright: ignore[reportOptionalCall] action=action, threshold=config.pyleak_blocking_threshold, ) @@ -61,22 +70,22 @@ async def monitor_async(): return config = get_config() - action = config.pyleak_action or LeakAction.WARN + action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess] async with contextlib.AsyncExitStack() as stack: stack.enter_context( - no_thread_leaks( + no_thread_leaks( # pyright: ignore[reportOptionalCall] action=action, grace_period=config.pyleak_thread_grace_period, ) ) stack.enter_context( - no_event_loop_blocking( + no_event_loop_blocking( # pyright: ignore[reportOptionalCall] action=action, threshold=config.pyleak_blocking_threshold, ) ) - await stack.enter_async_context(no_task_leaks(action=action)) + await stack.enter_async_context(no_task_leaks(action=action)) # pyright: ignore[reportOptionalCall] yield diff --git a/reflex/monitoring/__init__.py b/reflex/monitoring/__init__.py deleted file mode 100644 index 27f025e3335..00000000000 --- a/reflex/monitoring/__init__.py +++ /dev/null @@ -1,15 +0,0 @@ -"""Monitoring utilities for Reflex applications.""" - -from reflex.monitoring.pyleak import ( - is_pyleak_enabled, - monitor_async, - monitor_leaks, - monitor_sync, -) - -__all__ = [ - "is_pyleak_enabled", - "monitor_async", - "monitor_leaks", - "monitor_sync", -] From 7f9e74927353201d17418e7e72252ab15b7b76a4 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Thu, 24 Jul 2025 19:35:30 +0200 Subject: [PATCH 03/11] avoid repeating imports --- reflex/monitoring.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/reflex/monitoring.py b/reflex/monitoring.py index 36993f87749..e4c2d5124fd 100644 --- a/reflex/monitoring.py +++ b/reflex/monitoring.py @@ -1,6 +1,8 @@ """PyLeak integration for monitoring event loop blocking and resource leaks in Reflex applications.""" +import asyncio import contextlib +import functools from collections.abc import Callable from reflex.config import get_config @@ -95,8 +97,6 @@ def monitor_leaks(): Returns: Decorator function that applies PyLeak monitoring to sync/async functions. """ - import asyncio - import functools def decorator(func: Callable): if asyncio.iscoroutinefunction(func): From cd8967be67823c16750ac9cab2bc421d4a5b35ad Mon Sep 17 00:00:00 2001 From: Lendemor Date: Thu, 24 Jul 2025 19:47:58 +0200 Subject: [PATCH 04/11] fix --- pyproject.toml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 45a7e4c9897..489156bc90a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -40,11 +40,6 @@ dependencies = [ "wrapt >=1.17.0,<2.0", ] -[project.optional-dependencies] -monitoring = [ - "pyleak >=0.1.14,<1.0", -] - classifiers = [ "Development Status :: 4 - Beta", "License :: OSI Approved :: Apache Software License", @@ -55,6 +50,10 @@ classifiers = [ "Programming Language :: Python :: 3.13", ] +[project.optional-dependencies] +monitoring = [ + "pyleak >=0.1.14,<1.0", +] [project.urls] homepage = "https://reflex.dev" From 410cd28a41549017951b45386cc139673b98333a Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Thu, 24 Jul 2025 16:28:55 -0700 Subject: [PATCH 05/11] Handle async gen and regular gen Add overloads to `monitor_leaks` to avoid pyright ignores Remove extra layer of callable in `monitor_leaks` --- reflex/monitoring.py | 69 +++++++++++++++++++++++++++++++++++--------- reflex/state.py | 4 +-- 2 files changed, 58 insertions(+), 15 deletions(-) diff --git a/reflex/monitoring.py b/reflex/monitoring.py index e4c2d5124fd..2c5f8b9dad8 100644 --- a/reflex/monitoring.py +++ b/reflex/monitoring.py @@ -3,7 +3,9 @@ import asyncio import contextlib import functools -from collections.abc import Callable +import inspect +from collections.abc import AsyncGenerator, Awaitable, Callable, Generator +from typing import TypeVar, overload from reflex.config import get_config @@ -91,28 +93,69 @@ async def monitor_async(): yield -def monitor_leaks(): +YieldType = TypeVar("YieldType") +SendType = TypeVar("SendType") +ReturnType = TypeVar("ReturnType") + + +@overload +def monitor_leaks( + func: Callable[..., AsyncGenerator[YieldType, ReturnType]], +) -> Callable[..., AsyncGenerator[YieldType, ReturnType]]: ... + + +@overload +def monitor_leaks( + func: Callable[..., Generator[YieldType, SendType, ReturnType]], +) -> Callable[..., Generator[YieldType, SendType, ReturnType]]: ... + + +@overload +def monitor_leaks( + func: Callable[..., Awaitable[ReturnType]], +) -> Callable[..., Awaitable[ReturnType]]: ... + + +def monitor_leaks(func: Callable) -> Callable: """Framework decorator using the monitoring module's context manager. + Args: + func: The function to be monitored for leaks. + Returns: Decorator function that applies PyLeak monitoring to sync/async functions. """ + if inspect.isasyncgenfunction(func): - def decorator(func: Callable): - if asyncio.iscoroutinefunction(func): + @functools.wraps(func) + async def async_gen_wrapper(*args, **kwargs): + async with monitor_async(): + async for item in func(*args, **kwargs): + yield item - @functools.wraps(func) - async def async_wrapper(*args, **kwargs): - async with monitor_async(): - return await func(*args, **kwargs) + return async_gen_wrapper - return async_wrapper + if asyncio.iscoroutinefunction(func): @functools.wraps(func) - def sync_wrapper(*args, **kwargs): + async def async_wrapper(*args, **kwargs): + async with monitor_async(): + return await func(*args, **kwargs) + + return async_wrapper + + if inspect.isgeneratorfunction(func): + + @functools.wraps(func) + def gen_wrapper(*args, **kwargs): with monitor_sync(): - return func(*args, **kwargs) + yield from func(*args, **kwargs) + + return gen_wrapper - return sync_wrapper # pyright: ignore[reportReturnType] + @functools.wraps(func) + def sync_wrapper(*args, **kwargs): + with monitor_sync(): + return func(*args, **kwargs) - return decorator + return sync_wrapper # pyright: ignore[reportReturnType] diff --git a/reflex/state.py b/reflex/state.py index 573d2ee51d4..8526ceeb249 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -1787,7 +1787,7 @@ async def _process_event( # Get the function to process the event. if is_pyleak_enabled(): console.debug(f"Monitoring leaks for handler: {handler.fn.__qualname__}") - fn = functools.partial(monitor_leaks()(handler.fn), state) + fn = functools.partial(monitor_leaks(handler.fn), state) else: fn = functools.partial(handler.fn, state) @@ -1874,7 +1874,7 @@ async def _process_event( # Handle regular event chains. else: - yield await state._as_state_update(handler, events, final=True) # pyright: ignore[reportArgumentType] + yield await state._as_state_update(handler, events, final=True) # If an error occurs, throw a window alert. except Exception as ex: From 7930ee24dc5f07b296337f14387360c02e317ce4 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 1 Aug 2025 12:38:16 -0700 Subject: [PATCH 06/11] temporarily disable thread leak detection --- reflex/monitoring.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/reflex/monitoring.py b/reflex/monitoring.py index 2c5f8b9dad8..2064a3a9b49 100644 --- a/reflex/monitoring.py +++ b/reflex/monitoring.py @@ -47,12 +47,13 @@ def monitor_sync(): action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess] with contextlib.ExitStack() as stack: - stack.enter_context( - no_thread_leaks( # pyright: ignore[reportOptionalCall] - action=action, - grace_period=config.pyleak_thread_grace_period, - ) - ) + # Thread leak detection has issues with background tasks + # stack.enter_context( + # no_thread_leaks( # pyright: ignore[reportOptionalCall] + # action=action, + # grace_period=config.pyleak_thread_grace_period, + # ) + # ) stack.enter_context( no_event_loop_blocking( # pyright: ignore[reportOptionalCall] action=action, @@ -77,12 +78,12 @@ async def monitor_async(): action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess] async with contextlib.AsyncExitStack() as stack: - stack.enter_context( - no_thread_leaks( # pyright: ignore[reportOptionalCall] - action=action, - grace_period=config.pyleak_thread_grace_period, - ) - ) + # stack.enter_context( + # no_thread_leaks( # pyright: ignore[reportOptionalCall] + # action=action, + # grace_period=config.pyleak_thread_grace_period, + # ) + # ) stack.enter_context( no_event_loop_blocking( # pyright: ignore[reportOptionalCall] action=action, From dcf2c9ad4182ceb7bbde5c16b0f9f409bb085e82 Mon Sep 17 00:00:00 2001 From: Masen Furer Date: Fri, 1 Aug 2025 13:51:47 -0700 Subject: [PATCH 07/11] temporarily disable task leak detection --- reflex/monitoring.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/monitoring.py b/reflex/monitoring.py index 2064a3a9b49..b5c0533f2e1 100644 --- a/reflex/monitoring.py +++ b/reflex/monitoring.py @@ -90,7 +90,7 @@ async def monitor_async(): threshold=config.pyleak_blocking_threshold, ) ) - await stack.enter_async_context(no_task_leaks(action=action)) # pyright: ignore[reportOptionalCall] + #await stack.enter_async_context(no_task_leaks(action=action)) # pyright: ignore[reportOptionalCall] yield From 01ed8013cc9ebadd44878a6828e0b9eaa6c6637a Mon Sep 17 00:00:00 2001 From: Lendemor Date: Tue, 5 Aug 2025 18:48:33 +0200 Subject: [PATCH 08/11] move monitoring to utils folder and fix precommit --- reflex/state.py | 2 +- reflex/{ => utils}/monitoring.py | 21 +++++++-------------- 2 files changed, 8 insertions(+), 15 deletions(-) rename reflex/{ => utils}/monitoring.py (88%) diff --git a/reflex/state.py b/reflex/state.py index 8526ceeb249..ead5a91eec4 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -43,7 +43,6 @@ from reflex.istate.proxy import MutableProxy, StateProxy from reflex.istate.storage import ClientStorageBase from reflex.model import Model -from reflex.monitoring import is_pyleak_enabled, monitor_leaks from reflex.utils import console, format, prerequisites, types from reflex.utils.exceptions import ( ComputedVarShadowsBaseVarsError, @@ -61,6 +60,7 @@ ) from reflex.utils.exceptions import ImmutableStateError as ImmutableStateError from reflex.utils.exec import is_testing_env +from reflex.utils.monitoring import is_pyleak_enabled, monitor_leaks from reflex.utils.types import _isinstance, is_union, value_inside_optional from reflex.vars import Field, VarData, field from reflex.vars.base import ( diff --git a/reflex/monitoring.py b/reflex/utils/monitoring.py similarity index 88% rename from reflex/monitoring.py rename to reflex/utils/monitoring.py index b5c0533f2e1..5ef42e895ad 100644 --- a/reflex/monitoring.py +++ b/reflex/utils/monitoring.py @@ -47,13 +47,7 @@ def monitor_sync(): action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess] with contextlib.ExitStack() as stack: - # Thread leak detection has issues with background tasks - # stack.enter_context( - # no_thread_leaks( # pyright: ignore[reportOptionalCall] - # action=action, - # grace_period=config.pyleak_thread_grace_period, - # ) - # ) + # Thread leak detection has issues with background tasks (no_thread_leaks) stack.enter_context( no_event_loop_blocking( # pyright: ignore[reportOptionalCall] action=action, @@ -78,19 +72,18 @@ async def monitor_async(): action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess] async with contextlib.AsyncExitStack() as stack: - # stack.enter_context( - # no_thread_leaks( # pyright: ignore[reportOptionalCall] - # action=action, - # grace_period=config.pyleak_thread_grace_period, - # ) - # ) + # Thread leak detection has issues with background tasks (no_thread_leaks) + # Re-add thread leak later. + + # Block detection for event loops stack.enter_context( no_event_loop_blocking( # pyright: ignore[reportOptionalCall] action=action, threshold=config.pyleak_blocking_threshold, ) ) - #await stack.enter_async_context(no_task_leaks(action=action)) # pyright: ignore[reportOptionalCall] + # Task leak detection has issues with background tasks (no_task_leaks) + yield From af7d2b8a5b0c6898aadc9f07e5ce5920a8e1e946 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Tue, 5 Aug 2025 18:49:54 +0200 Subject: [PATCH 09/11] adjust default threshold --- reflex/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/reflex/config.py b/reflex/config.py index 3009fbcf3b5..8b961a6b88b 100644 --- a/reflex/config.py +++ b/reflex/config.py @@ -193,7 +193,7 @@ class BaseConfig: enable_pyleak_monitoring: bool = False # Threshold in seconds for detecting event loop blocking operations. - pyleak_blocking_threshold: float = 0.001 + pyleak_blocking_threshold: float = 0.1 # Grace period in seconds for thread leak detection cleanup. pyleak_thread_grace_period: float = 0.2 From 85f22450ea74cb8602cb042986027196b69ff261 Mon Sep 17 00:00:00 2001 From: Lendemor Date: Tue, 5 Aug 2025 19:13:13 +0200 Subject: [PATCH 10/11] rename decorator --- reflex/state.py | 4 ++-- reflex/utils/monitoring.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/reflex/state.py b/reflex/state.py index ead5a91eec4..a980933df08 100644 --- a/reflex/state.py +++ b/reflex/state.py @@ -60,7 +60,7 @@ ) from reflex.utils.exceptions import ImmutableStateError as ImmutableStateError from reflex.utils.exec import is_testing_env -from reflex.utils.monitoring import is_pyleak_enabled, monitor_leaks +from reflex.utils.monitoring import is_pyleak_enabled, monitor_loopblocks from reflex.utils.types import _isinstance, is_union, value_inside_optional from reflex.vars import Field, VarData, field from reflex.vars.base import ( @@ -1787,7 +1787,7 @@ async def _process_event( # Get the function to process the event. if is_pyleak_enabled(): console.debug(f"Monitoring leaks for handler: {handler.fn.__qualname__}") - fn = functools.partial(monitor_leaks(handler.fn), state) + fn = functools.partial(monitor_loopblocks(handler.fn), state) else: fn = functools.partial(handler.fn, state) diff --git a/reflex/utils/monitoring.py b/reflex/utils/monitoring.py index 5ef42e895ad..392c56ca527 100644 --- a/reflex/utils/monitoring.py +++ b/reflex/utils/monitoring.py @@ -93,24 +93,24 @@ async def monitor_async(): @overload -def monitor_leaks( +def monitor_loopblocks( func: Callable[..., AsyncGenerator[YieldType, ReturnType]], ) -> Callable[..., AsyncGenerator[YieldType, ReturnType]]: ... @overload -def monitor_leaks( +def monitor_loopblocks( func: Callable[..., Generator[YieldType, SendType, ReturnType]], ) -> Callable[..., Generator[YieldType, SendType, ReturnType]]: ... @overload -def monitor_leaks( +def monitor_loopblocks( func: Callable[..., Awaitable[ReturnType]], ) -> Callable[..., Awaitable[ReturnType]]: ... -def monitor_leaks(func: Callable) -> Callable: +def monitor_loopblocks(func: Callable) -> Callable: """Framework decorator using the monitoring module's context manager. Args: From ccaba49f04f26e1d65fd70f38f19812005410a6f Mon Sep 17 00:00:00 2001 From: Lendemor Date: Wed, 6 Aug 2025 21:24:43 +0200 Subject: [PATCH 11/11] prevent nested monitoring --- reflex/utils/monitoring.py | 65 ++++++++++++++++++++++++++------------ 1 file changed, 45 insertions(+), 20 deletions(-) diff --git a/reflex/utils/monitoring.py b/reflex/utils/monitoring.py index 392c56ca527..7b3ce88c764 100644 --- a/reflex/utils/monitoring.py +++ b/reflex/utils/monitoring.py @@ -4,6 +4,7 @@ import contextlib import functools import inspect +import threading from collections.abc import AsyncGenerator, Awaitable, Callable, Generator from typing import TypeVar, overload @@ -20,6 +21,10 @@ LeakAction = None # pyright: ignore[reportAssignmentType] +# Thread-local storage to track if monitoring is already active +_thread_local = threading.local() + + def is_pyleak_enabled() -> bool: """Check if PyLeak monitoring is enabled and available. @@ -43,18 +48,28 @@ def monitor_sync(): yield return + # Check if monitoring is already active in this thread + if getattr(_thread_local, "monitoring_active", False): + yield + return + config = get_config() action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess] - with contextlib.ExitStack() as stack: - # Thread leak detection has issues with background tasks (no_thread_leaks) - stack.enter_context( - no_event_loop_blocking( # pyright: ignore[reportOptionalCall] - action=action, - threshold=config.pyleak_blocking_threshold, + # Mark monitoring as active + _thread_local.monitoring_active = True + try: + with contextlib.ExitStack() as stack: + # Thread leak detection has issues with background tasks (no_thread_leaks) + stack.enter_context( + no_event_loop_blocking( # pyright: ignore[reportOptionalCall] + action=action, + threshold=config.pyleak_blocking_threshold, + ) ) - ) - yield + yield + finally: + _thread_local.monitoring_active = False @contextlib.asynccontextmanager @@ -68,23 +83,33 @@ async def monitor_async(): yield return + # Check if monitoring is already active in this thread + if getattr(_thread_local, "monitoring_active", False): + yield + return + config = get_config() action = config.pyleak_action or LeakAction.WARN # pyright: ignore[reportOptionalMemberAccess] - async with contextlib.AsyncExitStack() as stack: - # Thread leak detection has issues with background tasks (no_thread_leaks) - # Re-add thread leak later. - - # Block detection for event loops - stack.enter_context( - no_event_loop_blocking( # pyright: ignore[reportOptionalCall] - action=action, - threshold=config.pyleak_blocking_threshold, + # Mark monitoring as active + _thread_local.monitoring_active = True + try: + async with contextlib.AsyncExitStack() as stack: + # Thread leak detection has issues with background tasks (no_thread_leaks) + # Re-add thread leak later. + + # Block detection for event loops + stack.enter_context( + no_event_loop_blocking( # pyright: ignore[reportOptionalCall] + action=action, + threshold=config.pyleak_blocking_threshold, + ) ) - ) - # Task leak detection has issues with background tasks (no_task_leaks) + # Task leak detection has issues with background tasks (no_task_leaks) - yield + yield + finally: + _thread_local.monitoring_active = False YieldType = TypeVar("YieldType")