Skip to content

Commit cb8c442

Browse files
committed
fix closed loop
1 parent d9710ff commit cb8c442

File tree

4 files changed

+80
-0
lines changed

4 files changed

+80
-0
lines changed

reflex/app.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2140,3 +2140,12 @@ async def link_token_to_sid(self, sid: str, token: str):
21402140
if new_token:
21412141
# Duplicate detected, emit new token to client
21422142
await self.emit("new_token", new_token, to=sid)
2143+
2144+
async def close(self) -> None:
2145+
"""Close any resources used by the event namespace.
2146+
2147+
This is necessary in testing scenarios to close between asyncio test cases
2148+
to avoid having lingering connections associated with event loops.
2149+
"""
2150+
if hasattr(self, "_token_manager"):
2151+
await self._token_manager.close()

reflex/testing.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -313,6 +313,15 @@ async def _shutdown(*args, **kwargs) -> None:
313313
with contextlib.suppress(ValueError):
314314
await self.app_instance._state_manager.close()
315315

316+
# ensure token manager redis connections are closed
317+
if (
318+
self.app_instance is not None
319+
and hasattr(self.app_instance, "event_namespace")
320+
and self.app_instance.event_namespace is not None
321+
):
322+
with contextlib.suppress(Exception):
323+
await self.app_instance.event_namespace.close()
324+
316325
# socketio shutdown handler
317326
if self.app_instance is not None and self.app_instance.sio is not None:
318327
with contextlib.suppress(TypeError):

reflex/utils/token_manager.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,13 @@ async def disconnect_token(self, token: str, sid: str) -> None:
5252
sid: The Socket.IO session ID.
5353
"""
5454

55+
@abstractmethod
56+
async def close(self) -> None:
57+
"""Close any resources used by the token manager.
58+
59+
Subclasses must implement this method.
60+
"""
61+
5562
@classmethod
5663
def create(cls) -> TokenManager:
5764
"""Factory method to create appropriate TokenManager implementation.
@@ -107,6 +114,13 @@ async def disconnect_token(self, token: str, sid: str) -> None:
107114
self.token_to_sid.pop(token, None)
108115
self.sid_to_token.pop(sid, None)
109116

117+
async def close(self) -> None:
118+
"""Close any resources used by the token manager.
119+
120+
LocalTokenManager has no resources to close.
121+
"""
122+
# No resources to clean up for local manager
123+
110124

111125
class RedisTokenManager(LocalTokenManager):
112126
"""Token manager using Redis for distributed multi-worker support.
@@ -215,3 +229,17 @@ async def disconnect_token(self, token: str, sid: str) -> None:
215229

216230
# Clean up local dicts (always do this)
217231
await super().disconnect_token(token, sid)
232+
233+
async def close(self) -> None:
234+
"""Close Redis connection to prevent closed loop errors in tests.
235+
236+
It is necessary in testing scenarios to close between asyncio test cases
237+
to avoid having lingering redis connections associated with event loops
238+
that will be closed (each test case uses its own event loop).
239+
240+
Note: Connections will be automatically reopened when needed.
241+
"""
242+
try:
243+
await self.redis.aclose(close_connection_pool=True)
244+
except Exception as e:
245+
console.error(f"Redis close error: {e}")

tests/units/utils/test_token_manager.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,15 @@ async def test_disconnect_nonexistent_token(self, manager):
174174
assert len(manager.token_to_sid) == 0
175175
assert len(manager.sid_to_token) == 0
176176

177+
async def test_close_does_nothing(self, manager):
178+
"""Test close method does nothing for LocalTokenManager.
179+
180+
Args:
181+
manager: LocalTokenManager fixture instance.
182+
"""
183+
await manager.close()
184+
# Should not raise any errors
185+
177186

178187
class TestRedisTokenManager:
179188
"""Tests for RedisTokenManager."""
@@ -402,3 +411,28 @@ def test_inheritance_from_local_manager(self, manager):
402411
assert isinstance(manager, LocalTokenManager)
403412
assert hasattr(manager, "token_to_sid")
404413
assert hasattr(manager, "sid_to_token")
414+
415+
async def test_close_calls_redis_aclose(self, manager, mock_redis):
416+
"""Test close method calls Redis aclose with proper parameters.
417+
418+
Args:
419+
manager: RedisTokenManager fixture instance.
420+
mock_redis: Mock Redis client fixture.
421+
"""
422+
await manager.close()
423+
424+
mock_redis.aclose.assert_called_once_with(close_connection_pool=True)
425+
426+
async def test_close_handles_redis_error(self, manager, mock_redis):
427+
"""Test close method handles Redis errors gracefully.
428+
429+
Args:
430+
manager: RedisTokenManager fixture instance.
431+
mock_redis: Mock Redis client fixture.
432+
"""
433+
mock_redis.aclose.side_effect = Exception("Redis close error")
434+
435+
# Should not raise an error
436+
await manager.close()
437+
438+
mock_redis.aclose.assert_called_once_with(close_connection_pool=True)

0 commit comments

Comments
 (0)