From b021f5ce6bc4246e512214cf367e088f55a8bebb Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 1 Aug 2026 12:23:55 +0530 Subject: [PATCH 1/4] Fix state manager lock isolation --- reflex/istate/manager/disk.py | 4 ++- reflex/istate/manager/memory.py | 4 ++- reflex/istate/manager/redis.py | 2 +- .../istate/manager/test_manager_locks.py | 36 +++++++++++++++++++ 4 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 tests/units/istate/manager/test_manager_locks.py diff --git a/reflex/istate/manager/disk.py b/reflex/istate/manager/disk.py index 15dcc53f2ed..0fbd544f757 100644 --- a/reflex/istate/manager/disk.py +++ b/reflex/istate/manager/disk.py @@ -41,7 +41,9 @@ class StateManagerDisk(StateManager): states: dict[str, Any] = dataclasses.field(default_factory=dict) # The mutex ensures the dict of mutexes is updated exclusively - _state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock()) + _state_manager_lock: asyncio.Lock = dataclasses.field( + default_factory=asyncio.Lock + ) # The dict of mutexes for each client _states_locks: dict[str, asyncio.Lock] = dataclasses.field( diff --git a/reflex/istate/manager/memory.py b/reflex/istate/manager/memory.py index 07d4dc27926..ea780e9e332 100644 --- a/reflex/istate/manager/memory.py +++ b/reflex/istate/manager/memory.py @@ -28,7 +28,9 @@ class StateManagerMemory(StateManager): states: dict[str, Any] = dataclasses.field(default_factory=dict) # The mutex ensures the dict of mutexes is updated exclusively - _state_manager_lock: asyncio.Lock = dataclasses.field(default=asyncio.Lock()) + _state_manager_lock: asyncio.Lock = dataclasses.field( + default_factory=asyncio.Lock + ) # The dict of mutexes for each client _states_locks: dict[str, asyncio.Lock] = dataclasses.field( diff --git a/reflex/istate/manager/redis.py b/reflex/istate/manager/redis.py index c2dccc50c9b..4785bf37705 100644 --- a/reflex/istate/manager/redis.py +++ b/reflex/istate/manager/redis.py @@ -154,7 +154,7 @@ class StateManagerRedis(StateManager): # The mutex ensures the dict of mutexes is updated exclusively _state_manager_lock: asyncio.Lock = dataclasses.field( - default=asyncio.Lock(), init=False + default_factory=asyncio.Lock, init=False ) # Whether to opportunistically hold locks for fast in-memory access. diff --git a/tests/units/istate/manager/test_manager_locks.py b/tests/units/istate/manager/test_manager_locks.py new file mode 100644 index 00000000000..443a90a6c59 --- /dev/null +++ b/tests/units/istate/manager/test_manager_locks.py @@ -0,0 +1,36 @@ +"""Tests for state manager lock isolation.""" + +import asyncio +from collections.abc import Callable +from typing import Protocol + +import pytest + +from reflex.istate.manager.disk import StateManagerDisk +from reflex.istate.manager.memory import StateManagerMemory +from reflex.istate.manager.redis import StateManagerRedis +from tests.units.mock_redis import mock_redis + + +class StateManagerWithLock(Protocol): + """State manager protocol exposing the internal manager lock.""" + + _state_manager_lock: asyncio.Lock + + +@pytest.mark.parametrize( + "state_manager_factory", + [ + pytest.param(StateManagerMemory, id="memory"), + pytest.param(StateManagerDisk, id="disk"), + pytest.param(lambda: StateManagerRedis(redis=mock_redis()), id="redis"), + ], +) +def test_state_manager_lock_is_instance_local( + state_manager_factory: Callable[[], StateManagerWithLock], +): + """Each state manager instance should own its manager lock.""" + first = state_manager_factory() + second = state_manager_factory() + + assert first._state_manager_lock is not second._state_manager_lock From 00f2687c13a464c9fb86ee3f8747f74a18a50781 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 1 Aug 2026 12:50:10 +0530 Subject: [PATCH 2/4] Fix state manager lock test expectation --- news/6830.bugfix.md | 1 + tests/units/test_state.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/6830.bugfix.md diff --git a/news/6830.bugfix.md b/news/6830.bugfix.md new file mode 100644 index 00000000000..15fa9447458 --- /dev/null +++ b/news/6830.bugfix.md @@ -0,0 +1 @@ +Ensure state manager instances use isolated internal locks instead of sharing one lock across instances. diff --git a/tests/units/test_state.py b/tests/units/test_state.py index 7106287a43d..b66389bc790 100644 --- a/tests/units/test_state.py +++ b/tests/units/test_state.py @@ -1862,7 +1862,7 @@ async def test_state_manager_modify_state( # separate instances should NOT share locks sm2 = type(state_manager)() - assert sm2._state_manager_lock is state_manager._state_manager_lock + assert sm2._state_manager_lock is not state_manager._state_manager_lock assert not sm2._states_locks if state_manager._states_locks: assert sm2._states_locks != state_manager._states_locks From fdd79002ac183523e62716c553efac7ae0dc6d81 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 1 Aug 2026 12:54:03 +0530 Subject: [PATCH 3/4] Format state manager lock fields --- reflex/istate/manager/disk.py | 4 +--- reflex/istate/manager/memory.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/reflex/istate/manager/disk.py b/reflex/istate/manager/disk.py index 0fbd544f757..8091337d739 100644 --- a/reflex/istate/manager/disk.py +++ b/reflex/istate/manager/disk.py @@ -41,9 +41,7 @@ class StateManagerDisk(StateManager): states: dict[str, Any] = dataclasses.field(default_factory=dict) # The mutex ensures the dict of mutexes is updated exclusively - _state_manager_lock: asyncio.Lock = dataclasses.field( - default_factory=asyncio.Lock - ) + _state_manager_lock: asyncio.Lock = dataclasses.field(default_factory=asyncio.Lock) # The dict of mutexes for each client _states_locks: dict[str, asyncio.Lock] = dataclasses.field( diff --git a/reflex/istate/manager/memory.py b/reflex/istate/manager/memory.py index ea780e9e332..7f3d144ed39 100644 --- a/reflex/istate/manager/memory.py +++ b/reflex/istate/manager/memory.py @@ -28,9 +28,7 @@ class StateManagerMemory(StateManager): states: dict[str, Any] = dataclasses.field(default_factory=dict) # The mutex ensures the dict of mutexes is updated exclusively - _state_manager_lock: asyncio.Lock = dataclasses.field( - default_factory=asyncio.Lock - ) + _state_manager_lock: asyncio.Lock = dataclasses.field(default_factory=asyncio.Lock) # The dict of mutexes for each client _states_locks: dict[str, asyncio.Lock] = dataclasses.field( From a8b6febd3e8bc40a1a3730670521be1dd3bc1e80 Mon Sep 17 00:00:00 2001 From: Harsh Thakare Date: Sat, 1 Aug 2026 13:05:03 +0530 Subject: [PATCH 4/4] Isolate disk state manager lock test --- .../istate/manager/test_manager_locks.py | 55 +++++++++++++++++-- 1 file changed, 50 insertions(+), 5 deletions(-) diff --git a/tests/units/istate/manager/test_manager_locks.py b/tests/units/istate/manager/test_manager_locks.py index 443a90a6c59..64ce23c8ffb 100644 --- a/tests/units/istate/manager/test_manager_locks.py +++ b/tests/units/istate/manager/test_manager_locks.py @@ -2,6 +2,7 @@ import asyncio from collections.abc import Callable +from pathlib import Path from typing import Protocol import pytest @@ -9,6 +10,7 @@ from reflex.istate.manager.disk import StateManagerDisk from reflex.istate.manager.memory import StateManagerMemory from reflex.istate.manager.redis import StateManagerRedis +from reflex.utils import prerequisites from tests.units.mock_redis import mock_redis @@ -18,18 +20,61 @@ class StateManagerWithLock(Protocol): _state_manager_lock: asyncio.Lock +def _memory_state_manager_factory( + _: Path, __: pytest.MonkeyPatch +) -> Callable[[], StateManagerMemory]: + """Create in-memory state managers. + + Returns: + A factory for in-memory state managers. + """ + return StateManagerMemory + + +def _disk_state_manager_factory( + states_directory: Path, monkeypatch: pytest.MonkeyPatch +) -> Callable[[], StateManagerDisk]: + """Create disk state managers isolated to a temporary states directory. + + Args: + states_directory: The temporary directory for disk state files. + monkeypatch: Pytest monkeypatch fixture. + + Returns: + A factory for isolated disk state managers. + """ + monkeypatch.setattr(prerequisites, "get_states_dir", lambda: states_directory) + return StateManagerDisk + + +def _redis_state_manager_factory( + _: Path, __: pytest.MonkeyPatch +) -> Callable[[], StateManagerRedis]: + """Create redis state managers backed by mock redis. + + Returns: + A factory for redis state managers. + """ + return lambda: StateManagerRedis(redis=mock_redis()) + + @pytest.mark.parametrize( - "state_manager_factory", + "state_manager_factory_factory", [ - pytest.param(StateManagerMemory, id="memory"), - pytest.param(StateManagerDisk, id="disk"), - pytest.param(lambda: StateManagerRedis(redis=mock_redis()), id="redis"), + pytest.param(_memory_state_manager_factory, id="memory"), + pytest.param(_disk_state_manager_factory, id="disk"), + pytest.param(_redis_state_manager_factory, id="redis"), ], ) def test_state_manager_lock_is_instance_local( - state_manager_factory: Callable[[], StateManagerWithLock], + state_manager_factory_factory: Callable[ + [Path, pytest.MonkeyPatch], Callable[[], StateManagerWithLock] + ], + tmp_path: Path, + monkeypatch: pytest.MonkeyPatch, ): """Each state manager instance should own its manager lock.""" + state_manager_factory = state_manager_factory_factory(tmp_path, monkeypatch) first = state_manager_factory() second = state_manager_factory()