@@ -250,6 +250,17 @@ def listen(
250250 thread .start ()
251251 return listener
252252
253+ def listen_sync (
254+ self , channel : str , handler : "SyncEventHandler" , * , poll_interval : float | None = None , auto_ack : bool = True
255+ ) -> SyncEventListener :
256+ """Start a sync listener thread.
257+
258+ This is an alias of :meth:`listen`, provided for API symmetry with
259+ :meth:`listen_async`.
260+ """
261+
262+ return self .listen (channel , handler , poll_interval = poll_interval , auto_ack = auto_ack )
263+
253264 def stop_listener (self , listener_id : str ) -> None :
254265 """Stop a running sync listener."""
255266
@@ -259,6 +270,15 @@ def stop_listener(self, listener_id: str) -> None:
259270 listener .stop ()
260271 self ._runtime .increment_metric ("events.listener.stop" )
261272
273+ def stop_listener_sync (self , listener_id : str ) -> None :
274+ """Stop a running sync listener.
275+
276+ This is an alias of :meth:`stop_listener`, provided for API symmetry
277+ with :meth:`stop_listener_async`.
278+ """
279+
280+ self .stop_listener (listener_id )
281+
262282 def _run_sync_listener (
263283 self ,
264284 listener_id : str ,
@@ -391,6 +411,51 @@ def ack_sync(self, event_id: str) -> None:
391411 raise
392412 self ._end_event_span (span , result = "acked" )
393413
414+ async def nack_async (self , event_id : str ) -> None :
415+ """Return an event to the queue for redelivery asynchronously.
416+
417+ Resets the event status to pending, clearing the lease and allowing
418+ it to be picked up by another consumer.
419+ """
420+ if not self ._is_async :
421+ msg = "nack_async requires an async configuration"
422+ raise ImproperConfigurationError (msg )
423+ nack_method = getattr (self ._backend , "nack_async" , None )
424+ if nack_method is None :
425+ msg = "Current events backend does not support nack"
426+ raise ImproperConfigurationError (msg )
427+ span = self ._start_event_span ("nack" , mode = "async" )
428+ try :
429+ await nack_method (event_id )
430+ except Exception as error :
431+ self ._end_event_span (span , error = error )
432+ raise
433+ self ._end_event_span (span , result = "nacked" )
434+
435+ def nack_sync (self , event_id : str ) -> None :
436+ """Return an event to the queue for redelivery synchronously.
437+
438+ Resets the event status to pending, clearing the lease and allowing
439+ it to be picked up by another consumer.
440+ """
441+ if self ._is_async :
442+ if self ._should_bridge_sync_calls ():
443+ self ._bridge_sync_call (self .nack_async , event_id )
444+ return
445+ msg = "nack_sync requires a sync configuration"
446+ raise ImproperConfigurationError (msg )
447+ nack_method = getattr (self ._backend , "nack_sync" , None )
448+ if nack_method is None :
449+ msg = "Current events backend does not support nack"
450+ raise ImproperConfigurationError (msg )
451+ span = self ._start_event_span ("nack" , mode = "sync" )
452+ try :
453+ nack_method (event_id )
454+ except Exception as error :
455+ self ._end_event_span (span , error = error )
456+ raise
457+ self ._end_event_span (span , result = "nacked" )
458+
394459 # Loading helpers -----------------------------------------------------------
395460
396461 @staticmethod
@@ -520,23 +585,18 @@ def _normalize_channel_name(channel: str) -> str:
520585 raise ImproperConfigurationError (str (error )) from error
521586
522587 def _start_event_span (self , operation : str , channel : "str | None" = None , mode : str = "sync" ) -> Any :
523- span_manager = getattr (self ._runtime , "span_manager" , None )
524- if span_manager is None or not getattr (span_manager , "is_enabled" , False ):
588+ if not getattr (self ._runtime .span_manager , "is_enabled" , False ):
525589 return None
526590 attributes : dict [str , Any ] = {
527591 "sqlspec.events.operation" : operation ,
528592 "sqlspec.events.backend" : self ._backend_name ,
529593 "sqlspec.events.mode" : mode ,
530- "sqlspec.config" : type (self ._config ).__name__ ,
531594 }
532595 if self ._adapter_name :
533596 attributes ["sqlspec.events.adapter" ] = self ._adapter_name
534- bind_key = getattr (self ._config , "bind_key" , None )
535- if bind_key :
536- attributes ["sqlspec.bind_key" ] = bind_key
537597 if channel :
538598 attributes ["sqlspec.events.channel" ] = channel
539- return span_manager . start_span (f"sqlspec.events.{ operation } " , attributes )
599+ return self . _runtime . start_span (f"sqlspec.events.{ operation } " , attributes = attributes )
540600
541601 def _end_event_span (self , span : Any , * , error : "Exception | None" = None , result : "str | None" = None ) -> None :
542602 if span is None :
@@ -545,4 +605,55 @@ def _end_event_span(self, span: Any, *, error: "Exception | None" = None, result
545605 setter = getattr (span , "set_attribute" , None )
546606 if setter is not None :
547607 setter ("sqlspec.events.result" , result )
548- self ._runtime .span_manager .end_span (span , error = error )
608+ self ._runtime .end_span (span , error = error )
609+
610+ # Shutdown helpers ------------------------------------------------------------
611+
612+ async def shutdown_async (self ) -> None :
613+ """Shutdown the event channel and release backend resources.
614+
615+ Stops all async listeners and calls the backend's shutdown_async method
616+ to release any held connections (e.g., LISTEN/NOTIFY listener connections).
617+
618+ Must be called before closing the database pool when using native backends.
619+ """
620+ span = self ._start_event_span ("shutdown" , mode = "async" )
621+ try :
622+ for listener_id in list (self ._listeners_async ):
623+ await self .stop_listener_async (listener_id )
624+
625+ backend_shutdown = getattr (self ._backend , "shutdown_async" , None )
626+ if backend_shutdown is not None and callable (backend_shutdown ):
627+ result = backend_shutdown ()
628+ if result is not None :
629+ await result # type: ignore[misc]
630+ except Exception as error :
631+ self ._end_event_span (span , error = error )
632+ raise
633+ self ._end_event_span (span , result = "shutdown" )
634+ self ._runtime .increment_metric ("events.shutdown" )
635+
636+ def shutdown_sync (self ) -> None :
637+ """Shutdown the event channel and release backend resources.
638+
639+ Stops all sync listeners. For async backends with sync bridging,
640+ use shutdown_async instead.
641+ """
642+ if self ._is_async :
643+ if self ._should_bridge_sync_calls ():
644+ self ._bridge_sync_call (self .shutdown_async )
645+ return
646+ msg = "shutdown_sync requires a sync configuration"
647+ raise ImproperConfigurationError (msg )
648+ span = self ._start_event_span ("shutdown" , mode = "sync" )
649+ try :
650+ for listener_id in list (self ._listeners_sync ):
651+ self .stop_listener (listener_id )
652+ backend_shutdown = getattr (self ._backend , "shutdown_sync" , None )
653+ if backend_shutdown is not None and callable (backend_shutdown ):
654+ backend_shutdown ()
655+ except Exception as error :
656+ self ._end_event_span (span , error = error )
657+ raise
658+ self ._end_event_span (span , result = "shutdown" )
659+ self ._runtime .increment_metric ("events.shutdown" )
0 commit comments