Skip to content

Commit 253ce0b

Browse files
committed
fix: hanging in wait_for_sync when channel DETACHED/FAILED
1 parent 052b009 commit 253ce0b

File tree

2 files changed

+44
-0
lines changed

2 files changed

+44
-0
lines changed

ably/realtime/presencemap.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -333,7 +333,17 @@ def clear(self) -> None:
333333
Clear all members and reset sync state.
334334
335335
Used when channel enters DETACHED or FAILED state (RTP5a).
336+
Invokes any pending sync callbacks before clearing to ensure
337+
waiting Futures are resolved and callers are not left blocked.
336338
"""
339+
# Notify any callbacks waiting for sync to complete
340+
# This ensures Futures created by _wait_for_sync() are resolved
341+
for callback in self._sync_complete_callbacks:
342+
try:
343+
callback()
344+
except Exception as e:
345+
self._logger.error(f"Error in sync complete callback during clear: {e}")
346+
337347
self._map.clear()
338348
self._residual_members = None
339349
self._sync_in_progress = False

test/ably/realtime/presencemap_test.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,3 +736,37 @@ def test_start_sync_multiple_times(self):
736736
# Call start_sync again - should not reset residual
737737
self.presence_map.start_sync()
738738
assert self.presence_map._residual_members is initial_residual
739+
740+
def test_clear_invokes_sync_callbacks(self):
741+
"""
742+
Test that clear() invokes pending sync callbacks to prevent hanging.
743+
744+
This ensures that if get() is waiting for sync and the channel
745+
transitions to DETACHED/FAILED, the waiting Future is resolved
746+
and the caller is not left blocked.
747+
"""
748+
msg1 = PresenceMessage(
749+
id='conn1:0:0',
750+
connection_id='conn1',
751+
client_id='client1',
752+
action=PresenceAction.PRESENT
753+
)
754+
755+
self.presence_map.put(msg1)
756+
self.presence_map.start_sync()
757+
758+
# Register a callback as if _wait_for_sync() was called
759+
callback_invoked = False
760+
761+
def sync_callback():
762+
nonlocal callback_invoked
763+
callback_invoked = True
764+
765+
self.presence_map.wait_sync(sync_callback)
766+
767+
# Clear should invoke the callback
768+
self.presence_map.clear()
769+
770+
assert callback_invoked, "clear() should invoke pending sync callbacks"
771+
assert not self.presence_map.sync_in_progress
772+
assert len(self.presence_map.values()) == 0

0 commit comments

Comments
 (0)