@@ -44,13 +44,13 @@ def sync(self, timeout: float | None = None) -> None:
4444 _future_result (fut , timeout )
4545
4646 def pause (self ) -> None :
47- self ._pub .pause ( )
47+ self ._loop . call_soon_threadsafe ( self . _pub .pause )
4848
4949 def resume (self ) -> None :
50- self ._pub .resume ( )
50+ self ._loop . call_soon_threadsafe ( self . _pub .resume )
5151
5252 def close (self ) -> None :
53- self ._pub .close ( )
53+ self ._loop . call_soon_threadsafe ( self . _pub .close )
5454
5555 def wait_closed (self , timeout : float | None = None ) -> None :
5656 fut = asyncio .run_coroutine_threadsafe (self ._pub .wait_closed (), self ._loop )
@@ -101,7 +101,7 @@ def recv_zero_copy(self, timeout: float | None = None) -> _SyncZeroCopy:
101101 return _SyncZeroCopy (self ._sub , self ._loop , timeout )
102102
103103 def close (self ) -> None :
104- self ._sub .close ( )
104+ self ._loop . call_soon_threadsafe ( self . _sub .close )
105105
106106 def wait_closed (self , timeout : float | None = None ) -> None :
107107 fut = asyncio .run_coroutine_threadsafe (self ._sub .wait_closed (), self ._loop )
@@ -134,6 +134,14 @@ def graph_address(self) -> AddressType | None:
134134 return self ._graph_context .graph_address
135135
136136 def __enter__ (self ) -> "SyncContext" :
137+
138+ # SyncContext instances are single-use: they cannot be re-entered after shutdown.
139+ if self ._closed :
140+ raise RuntimeError (
141+ "SyncContext instances cannot be reused after shutdown; "
142+ "create a new SyncContext instead."
143+ )
144+
137145 if self ._loop_cm is not None :
138146 return self
139147
@@ -368,6 +376,22 @@ async def _recv_any(
368376 entries : Iterable [tuple [SyncSubscriber , Callable [[Any ], None ], bool ]],
369377 timeout : float | None ,
370378) -> tuple [tuple [SyncSubscriber , Callable [[Any ], None ], bool ], Any , Any ] | None :
379+ async def _cleanup_result (result : Any ) -> None :
380+ if isinstance (result , BaseException ):
381+ return
382+ try :
383+ _ , cm , _ = result
384+ except Exception :
385+ return
386+ try :
387+ await cm .__aexit__ (None , None , None )
388+ except CacheMiss :
389+ logger .warning (
390+ "Cache miss while releasing message; publisher likely exited."
391+ )
392+ except Exception :
393+ logger .exception ("Failed while releasing message backpressure" )
394+
371395 async def _recv_entry (
372396 entry : tuple [SyncSubscriber , Callable [[Any ], None ], bool ]
373397 ) -> tuple [tuple [SyncSubscriber , Callable [[Any ], None ], bool ], Any , Any ]:
@@ -392,19 +416,17 @@ async def _recv_entry(
392416 task .cancel ()
393417 except RuntimeError :
394418 pass
395- await asyncio .gather (* pending , return_exceptions = True )
419+ pending_results = await asyncio .gather (
420+ * pending , return_exceptions = True
421+ )
422+ for result in pending_results :
423+ await _cleanup_result (result )
396424 return None
397425
398- for task in pending :
399- try :
400- task .cancel ()
401- except RuntimeError :
402- pass
403- await asyncio .gather (* pending , return_exceptions = True )
404-
426+ winner_result = None
405427 for task in done :
406428 try :
407- return task .result ()
429+ result = task .result ()
408430 except CacheMiss :
409431 # Likely stale notification after publisher exit; keep waiting.
410432 continue
@@ -413,6 +435,24 @@ async def _recv_entry(
413435 except Exception :
414436 logger .exception ("Sync subscription receive failed" )
415437 continue
438+ if winner_result is None :
439+ winner_result = result
440+ else :
441+ await _cleanup_result (result )
442+
443+ for task in pending :
444+ try :
445+ task .cancel ()
446+ except RuntimeError :
447+ pass
448+ pending_results = await asyncio .gather (
449+ * pending , return_exceptions = True
450+ )
451+ for result in pending_results :
452+ await _cleanup_result (result )
453+
454+ if winner_result is not None :
455+ return winner_result
416456
417457 # Only CacheMiss/cancelled/error occurred; continue within timeout window.
418458 if deadline is not None and loop .time () >= deadline :
0 commit comments