2323import logging
2424import random
2525import time
26- from typing import TYPE_CHECKING , Any , Callable , Literal , Optional , Union
26+ from typing import Any , Callable , Literal , Optional , TYPE_CHECKING , Union
2727
2828# Third-Party
2929from cpex .framework import ObservabilityProvider , TenantPluginManager
@@ -102,9 +102,10 @@ class _TeamBindingChangeMsg(BaseModel):
102102
103103def _get_invalidation_hmac_key () -> Optional [bytes ]:
104104 """Return the HMAC signing key for pub/sub messages, or None if unconfigured."""
105- from mcpgateway .config import settings # pylint: disable=import-outside-toplevel
106105 from pydantic import SecretStr # pylint: disable=import-outside-toplevel
107106
107+ from mcpgateway .config import settings # pylint: disable=import-outside-toplevel
108+
108109 secret = settings .jwt_secret_key
109110 if not secret :
110111 return None
@@ -491,21 +492,22 @@ async def _plugin_invalidation_listener() -> None:
491492 consecutive_failures = 0
492493
493494 while True :
495+ pubsub = None
494496 try :
495497 client = await _redis ()
496498 if not client :
497499 _logger .debug ("Plugin invalidation listener: Redis unavailable, retrying in 10s" )
498500 await asyncio .sleep (10 )
499501 continue
500502
501- async with client .pubsub () as pubsub :
502- await pubsub .subscribe (_REDIS_INVALIDATION_CHANNEL )
503- _logger .info ("Plugin invalidation listener subscribed to %s" , _REDIS_INVALIDATION_CHANNEL )
504- backoff = 1.0
505- consecutive_failures = 0
503+ pubsub = client .pubsub ()
504+ await pubsub .subscribe (_REDIS_INVALIDATION_CHANNEL )
505+ _logger .info ("Plugin invalidation listener subscribed to %s" , _REDIS_INVALIDATION_CHANNEL )
506+ backoff = 1.0
507+ consecutive_failures = 0
506508
507- async for message in pubsub .listen ():
508- await _handle_invalidation_message (message )
509+ async for message in pubsub .listen ():
510+ await _handle_invalidation_message (message )
509511
510512 except asyncio .CancelledError :
511513 _logger .info ("Plugin invalidation listener cancelled" )
@@ -524,6 +526,14 @@ async def _plugin_invalidation_listener() -> None:
524526 )
525527 await asyncio .sleep (delay )
526528 backoff = min (max_backoff , backoff * 2 )
529+ finally :
530+ # Always release the pubsub connection back to the pool.
531+ # Using wait_for shields aclose() from an in-flight CancelledError:
532+ if pubsub is not None :
533+ try :
534+ await asyncio .wait_for (pubsub .aclose (), timeout = 2.0 )
535+ except Exception :
536+ pass
527537
528538
529539async def start_plugin_invalidation_listener () -> None :
0 commit comments