@@ -203,7 +203,9 @@ async def ws_read_loop(self):
203203 log .exception (
204204 f"WebSocketTransport.decode(): Unexpected exception handling channel message: { e } "
205205 )
206- except ConnectionClosedOK :
206+ except (ConnectionClosedOK , GeneratorExit ):
207+ # ConnectionClosedOK: normal websocket closure
208+ # GeneratorExit: coroutine being closed (e.g., during event loop shutdown)
207209 return
208210
209211 def decode_raw_websocket_frame (self , raw : str | bytes ) -> dict :
@@ -229,18 +231,38 @@ def on_read_loop_done(self, task: asyncio.Task):
229231
230232 async def dispose (self ):
231233 self .is_disposed = True
234+
235+ # Cancel tasks but don't await them yet to avoid deadlock
236+ tasks_to_await = []
237+
232238 if self .read_loop :
233239 self .read_loop .cancel ()
240+ tasks_to_await .append (self .read_loop )
234241 if self .ws_connect_task :
235242 self .ws_connect_task .cancel ()
243+ tasks_to_await .append (self .ws_connect_task )
236244 if self .idle_timer :
237245 self .idle_timer .cancel ()
246+
247+ # Schedule cleanup of cancelled tasks in the background to avoid blocking dispose()
248+ # This prevents deadlock when dispose() is called from within these tasks
249+ if tasks_to_await :
250+ asyncio .create_task (self ._cleanup_tasks (tasks_to_await ))
251+
238252 if self .websocket :
239253 try :
240254 await self .websocket .close ()
241255 except asyncio .CancelledError :
242256 return
243257
258+ async def _cleanup_tasks (self , tasks ):
259+ """Wait for cancelled tasks to complete their cleanup."""
260+ for task in tasks :
261+ try :
262+ await task
263+ except Exception :
264+ pass # Ignore all exceptions from cancelled/failed tasks
265+
244266 async def close (self ):
245267 await self .send ({'action' : ProtocolMessageAction .CLOSE })
246268
0 commit comments