Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/redis/event.py: 48%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1import asyncio
2import threading
3from abc import ABC, abstractmethod
4from enum import Enum
5from typing import TYPE_CHECKING, Dict, List, Optional, Type, Union
7from redis.auth.token import TokenInterface
8from redis.credentials import CredentialProvider, StreamingCredentialProvider
9from redis.observability.recorder import (
10 init_connection_count,
11 register_pools_connection_count,
12)
13from redis.utils import check_protocol_version, deprecated_function
15if TYPE_CHECKING:
16 from redis.maint_notifications import (
17 MaintenanceNotification,
18 MaintenanceState,
19 MaintNotificationsConfig,
20 )
23class EventListenerInterface(ABC):
24 """
25 Represents a listener for given event object.
26 """
28 @abstractmethod
29 def listen(self, event: object):
30 pass
33class AsyncEventListenerInterface(ABC):
34 """
35 Represents an async listener for given event object.
36 """
38 @abstractmethod
39 async def listen(self, event: object):
40 pass
43class EventDispatcherInterface(ABC):
44 """
45 Represents a dispatcher that dispatches events to listeners
46 associated with given event.
47 """
49 @abstractmethod
50 def dispatch(self, event: object):
51 pass
53 @abstractmethod
54 async def dispatch_async(self, event: object):
55 pass
57 @abstractmethod
58 def register_listeners(
59 self,
60 mappings: Dict[
61 Type[object],
62 List[Union[EventListenerInterface, AsyncEventListenerInterface]],
63 ],
64 ):
65 """Register additional listeners."""
66 pass
68 @abstractmethod
69 def unregister_listeners(
70 self,
71 mappings: Dict[
72 Type[object],
73 List[Union[EventListenerInterface, AsyncEventListenerInterface]],
74 ],
75 ):
76 """Remove previously registered listeners by identity."""
77 pass
80class EventException(Exception):
81 """
82 Exception wrapper that adds an event object into exception context.
83 """
85 def __init__(self, exception: Exception, event: object):
86 self.exception = exception
87 self.event = event
88 super().__init__(exception)
91class EventDispatcher(EventDispatcherInterface):
92 # TODO: Make dispatcher to accept external mappings.
93 def __init__(
94 self,
95 event_listeners: Optional[
96 Dict[Type[object], List[EventListenerInterface]]
97 ] = None,
98 ):
99 """
100 Dispatcher that dispatches events to listeners associated with given event.
101 """
102 self._event_listeners_mapping: Dict[
103 Type[object], List[EventListenerInterface]
104 ] = {
105 AfterConnectionReleasedEvent: [
106 ReAuthConnectionListener(),
107 ],
108 AfterPooledConnectionsInstantiationEvent: [
109 RegisterReAuthForPooledConnections(),
110 ],
111 AfterSingleConnectionInstantiationEvent: [
112 RegisterReAuthForSingleConnection()
113 ],
114 AfterPubSubConnectionInstantiationEvent: [RegisterReAuthForPubSub()],
115 AfterAsyncClusterInstantiationEvent: [RegisterReAuthForAsyncClusterNodes()],
116 AsyncAfterConnectionReleasedEvent: [
117 AsyncReAuthConnectionListener(),
118 ],
119 }
121 # Reentrant so a finalizer/listener that runs on the same thread
122 # while the lock is held (e.g. a weakref.finalize callback fired
123 # from cyclic GC during an allocation inside register_listeners /
124 # unregister_listeners) can re-enter without deadlocking.
125 self._lock = threading.RLock()
126 self._async_lock = None
128 if event_listeners:
129 self.register_listeners(event_listeners)
131 def dispatch(self, event: object):
132 # Snapshot listeners under the lock, then release it before invoking
133 # them. Holding the lock across listener execution would turn any
134 # listener that calls register_listeners / unregister_listeners /
135 # dispatch back into the dispatcher into a deadlock.
136 with self._lock:
137 listeners = list(self._event_listeners_mapping.get(type(event), []))
138 for listener in listeners:
139 listener.listen(event)
141 async def dispatch_async(self, event: object):
142 if self._async_lock is None:
143 self._async_lock = asyncio.Lock()
145 # Snapshot listeners under the lock, then release it before awaiting
146 # them. See the note in dispatch(); the same rationale applies here
147 # for dispatch_async re-entry from within a listener.
148 async with self._async_lock:
149 listeners = list(self._event_listeners_mapping.get(type(event), []))
150 for listener in listeners:
151 await listener.listen(event)
153 def register_listeners(
154 self,
155 mappings: Dict[
156 Type[object],
157 List[Union[EventListenerInterface, AsyncEventListenerInterface]],
158 ],
159 ):
160 with self._lock:
161 for event_type in mappings:
162 if event_type in self._event_listeners_mapping:
163 self._event_listeners_mapping[event_type] = list(
164 set(
165 self._event_listeners_mapping[event_type]
166 + mappings[event_type]
167 )
168 )
169 else:
170 self._event_listeners_mapping[event_type] = mappings[event_type]
172 def unregister_listeners(
173 self,
174 mappings: Dict[
175 Type[object],
176 List[Union[EventListenerInterface, AsyncEventListenerInterface]],
177 ],
178 ):
179 with self._lock:
180 for event_type, to_remove in mappings.items():
181 current = self._event_listeners_mapping.get(event_type)
182 if not current:
183 continue
184 # Remove by identity to match register semantics and to avoid
185 # reliance on listener __eq__ implementations.
186 self._event_listeners_mapping[event_type] = [
187 listener
188 for listener in current
189 if all(listener is not target for target in to_remove)
190 ]
193class AfterConnectionReleasedEvent:
194 """
195 Event that will be fired before each command execution.
196 """
198 def __init__(self, connection):
199 self._connection = connection
201 @property
202 def connection(self):
203 return self._connection
206class AsyncAfterConnectionReleasedEvent(AfterConnectionReleasedEvent):
207 pass
210class AfterSlotsCacheRefreshEvent:
211 """
212 Event fired after NodesManager's slots cache is refreshed, either via a
213 full re-initialization or a MOVED-driven slot re-mapping. Signal-only;
214 carries no payload. Listeners typically reconcile per-node bookkeeping
215 (e.g. ClusterPubSub shard subscriptions).
216 """
218 pass
221class AsyncAfterSlotsCacheRefreshEvent(AfterSlotsCacheRefreshEvent):
222 pass
225class MaintenanceEvent:
226 """
227 Base class of the events a maintenance notification pushed by the server
228 produces while it relaxes timeouts: on a connection pool for a node handoff
229 (``MaintenanceState.MOVING``), or on a single connection for a shard
230 migration or failover (``MaintenanceState.MAINTENANCE``).
232 Dispatched synchronously, by both the sync and the async client, to the
233 ``event_dispatcher`` of the ``MaintNotificationsConfig`` the handler acted
234 with; listeners implement ``EventListenerInterface``.
236 :param connection_pool: The pool whose connections are affected. ``None``
237 when the notification was handled by a connection without a pool
238 handler, such as an OSS cluster node connection.
239 :param connection: The connection that received the notification. ``None``
240 when a pool-level handler had no connection to attribute it to.
241 :param state: The ``MaintenanceState`` the notification puts its source in.
242 :param notification: The ``MaintenanceNotification`` that was handled.
243 :param config: The ``MaintNotificationsConfig`` the handler acted with.
244 """
246 def __init__(
247 self,
248 connection_pool: Optional[object],
249 connection: Optional[object],
250 state: "MaintenanceState",
251 notification: Optional["MaintenanceNotification"],
252 config: "MaintNotificationsConfig",
253 ):
254 self._connection_pool = connection_pool
255 self._connection = connection
256 self._state = state
257 self._notification = notification
258 self._config = config
260 @property
261 def connection_pool(self) -> Optional[object]:
262 return self._connection_pool
264 @property
265 def connection(self) -> Optional[object]:
266 return self._connection
268 @property
269 def state(self) -> "MaintenanceState":
270 return self._state
272 @property
273 def notification(self) -> Optional["MaintenanceNotification"]:
274 return self._notification
276 @property
277 def config(self) -> "MaintNotificationsConfig":
278 return self._config
281class MaintenanceStartedEvent(MaintenanceEvent):
282 """
283 Event fired when a maintenance notification starts relaxing the timeouts of
284 its source. Paired with a ``MaintenanceCompletedEvent`` carrying the same
285 ``state`` and source.
286 """
288 pass
291class MaintenanceCompletedEvent(MaintenanceEvent):
292 """
293 Event fired when the timeout relaxation announced by a
294 ``MaintenanceStartedEvent`` is reverted: the completion notification
295 arrived, the handoff TTL expired, or the connection was closed while
296 still under maintenance. Also fired, with ``MaintenanceState.MAINTENANCE``,
297 when a handoff takes over a connection under maintenance: the connection
298 stays relaxed, but under the handoff's ``MOVING`` pair from then on. In
299 the last two cases ``notification`` is ``None``.
301 Not fired when a newer handoff superseded the one that completed; the
302 source stays relaxed until the newer one completes.
303 """
305 pass
308class ClientType(Enum):
309 SYNC = ("sync",)
310 ASYNC = ("async",)
313class AfterPooledConnectionsInstantiationEvent:
314 """
315 Event that will be fired after pooled connection instances was created.
316 """
318 def __init__(
319 self,
320 connection_pools: List,
321 client_type: ClientType,
322 credential_provider: Optional[CredentialProvider] = None,
323 ):
324 self._connection_pools = connection_pools
325 self._client_type = client_type
326 self._credential_provider = credential_provider
328 @property
329 def connection_pools(self):
330 return self._connection_pools
332 @property
333 def client_type(self) -> ClientType:
334 return self._client_type
336 @property
337 def credential_provider(self) -> Union[CredentialProvider, None]:
338 return self._credential_provider
341class AfterSingleConnectionInstantiationEvent:
342 """
343 Event that will be fired after single connection instances was created.
345 :param connection_lock: For sync client thread-lock should be provided,
346 for async asyncio.Lock
347 """
349 def __init__(
350 self,
351 connection,
352 client_type: ClientType,
353 connection_lock: Union[threading.RLock, asyncio.Lock],
354 ):
355 self._connection = connection
356 self._client_type = client_type
357 self._connection_lock = connection_lock
359 @property
360 def connection(self):
361 return self._connection
363 @property
364 def client_type(self) -> ClientType:
365 return self._client_type
367 @property
368 def connection_lock(self) -> Union[threading.RLock, asyncio.Lock]:
369 return self._connection_lock
372class AfterPubSubConnectionInstantiationEvent:
373 def __init__(
374 self,
375 pubsub_connection,
376 connection_pool,
377 client_type: ClientType,
378 connection_lock: Union[threading.RLock, asyncio.Lock],
379 ):
380 self._pubsub_connection = pubsub_connection
381 self._connection_pool = connection_pool
382 self._client_type = client_type
383 self._connection_lock = connection_lock
385 @property
386 def pubsub_connection(self):
387 return self._pubsub_connection
389 @property
390 def connection_pool(self):
391 return self._connection_pool
393 @property
394 def client_type(self) -> ClientType:
395 return self._client_type
397 @property
398 def connection_lock(self) -> Union[threading.RLock, asyncio.Lock]:
399 return self._connection_lock
402class AfterAsyncClusterInstantiationEvent:
403 """
404 Event that will be fired after async cluster instance was created.
406 Async cluster doesn't use connection pools,
407 instead ClusterNode object manages connections.
408 """
410 def __init__(
411 self,
412 nodes: dict,
413 credential_provider: Optional[CredentialProvider] = None,
414 ):
415 self._nodes = nodes
416 self._credential_provider = credential_provider
418 @property
419 def nodes(self) -> dict:
420 return self._nodes
422 @property
423 def credential_provider(self) -> Union[CredentialProvider, None]:
424 return self._credential_provider
427class OnCommandsFailEvent:
428 """
429 Event fired whenever a command fails during the execution.
430 """
432 def __init__(
433 self,
434 commands: tuple,
435 exception: Exception,
436 ):
437 self._commands = commands
438 self._exception = exception
440 @property
441 def commands(self) -> tuple:
442 return self._commands
444 @property
445 def exception(self) -> Exception:
446 return self._exception
449class AsyncOnCommandsFailEvent(OnCommandsFailEvent):
450 pass
453class ReAuthConnectionListener(EventListenerInterface):
454 """
455 Listener that performs re-authentication of given connection.
456 """
458 def listen(self, event: AfterConnectionReleasedEvent):
459 event.connection.re_auth()
462class AsyncReAuthConnectionListener(AsyncEventListenerInterface):
463 """
464 Async listener that performs re-authentication of given connection.
465 """
467 async def listen(self, event: AsyncAfterConnectionReleasedEvent):
468 await event.connection.re_auth()
471class RegisterReAuthForPooledConnections(EventListenerInterface):
472 """
473 Listener that registers a re-authentication callback for pooled connections.
474 Required by :class:`StreamingCredentialProvider`.
475 """
477 def __init__(self):
478 self._event = None
480 def listen(self, event: AfterPooledConnectionsInstantiationEvent):
481 if isinstance(event.credential_provider, StreamingCredentialProvider):
482 self._event = event
484 if event.client_type == ClientType.SYNC:
485 event.credential_provider.on_next(self._re_auth)
486 event.credential_provider.on_error(self._raise_on_error)
487 else:
488 event.credential_provider.on_next(self._re_auth_async)
489 event.credential_provider.on_error(self._raise_on_error_async)
491 def _re_auth(self, token):
492 for pool in self._event.connection_pools:
493 pool.re_auth_callback(token)
495 async def _re_auth_async(self, token):
496 for pool in self._event.connection_pools:
497 await pool.re_auth_callback(token)
499 def _raise_on_error(self, error: Exception):
500 raise EventException(error, self._event)
502 async def _raise_on_error_async(self, error: Exception):
503 raise EventException(error, self._event)
506class RegisterReAuthForSingleConnection(EventListenerInterface):
507 """
508 Listener that registers a re-authentication callback for single connection.
509 Required by :class:`StreamingCredentialProvider`.
510 """
512 def __init__(self):
513 self._event = None
515 def listen(self, event: AfterSingleConnectionInstantiationEvent):
516 if isinstance(
517 event.connection.credential_provider, StreamingCredentialProvider
518 ):
519 self._event = event
521 if event.client_type == ClientType.SYNC:
522 event.connection.credential_provider.on_next(self._re_auth)
523 event.connection.credential_provider.on_error(self._raise_on_error)
524 else:
525 event.connection.credential_provider.on_next(self._re_auth_async)
526 event.connection.credential_provider.on_error(
527 self._raise_on_error_async
528 )
530 def _re_auth(self, token):
531 with self._event.connection_lock:
532 self._event.connection.send_command(
533 "AUTH", token.try_get("oid"), token.get_value()
534 )
535 self._event.connection.read_response()
537 async def _re_auth_async(self, token):
538 async with self._event.connection_lock:
539 await self._event.connection.send_command(
540 "AUTH", token.try_get("oid"), token.get_value()
541 )
542 await self._event.connection.read_response()
544 def _raise_on_error(self, error: Exception):
545 raise EventException(error, self._event)
547 async def _raise_on_error_async(self, error: Exception):
548 raise EventException(error, self._event)
551class RegisterReAuthForAsyncClusterNodes(EventListenerInterface):
552 def __init__(self):
553 self._event = None
555 def listen(self, event: AfterAsyncClusterInstantiationEvent):
556 if isinstance(event.credential_provider, StreamingCredentialProvider):
557 self._event = event
558 event.credential_provider.on_next(self._re_auth)
559 event.credential_provider.on_error(self._raise_on_error)
561 async def _re_auth(self, token: TokenInterface):
562 for key in self._event.nodes:
563 await self._event.nodes[key].re_auth_callback(token)
565 async def _raise_on_error(self, error: Exception):
566 raise EventException(error, self._event)
569class RegisterReAuthForPubSub(EventListenerInterface):
570 def __init__(self):
571 self._connection = None
572 self._connection_pool = None
573 self._client_type = None
574 self._connection_lock = None
575 self._event = None
577 def listen(self, event: AfterPubSubConnectionInstantiationEvent):
578 if isinstance(
579 event.pubsub_connection.credential_provider, StreamingCredentialProvider
580 ) and check_protocol_version(event.pubsub_connection.get_protocol(), 3):
581 self._event = event
582 self._connection = event.pubsub_connection
583 self._connection_pool = event.connection_pool
584 self._client_type = event.client_type
585 self._connection_lock = event.connection_lock
587 if self._client_type == ClientType.SYNC:
588 self._connection.credential_provider.on_next(self._re_auth)
589 self._connection.credential_provider.on_error(self._raise_on_error)
590 else:
591 self._connection.credential_provider.on_next(self._re_auth_async)
592 self._connection.credential_provider.on_error(
593 self._raise_on_error_async
594 )
596 def _re_auth(self, token: TokenInterface):
597 with self._connection_lock:
598 self._connection.send_command(
599 "AUTH", token.try_get("oid"), token.get_value()
600 )
601 self._connection.read_response()
603 self._connection_pool.re_auth_callback(token)
605 async def _re_auth_async(self, token: TokenInterface):
606 async with self._connection_lock:
607 await self._connection.send_command(
608 "AUTH", token.try_get("oid"), token.get_value()
609 )
610 await self._connection.read_response()
612 await self._connection_pool.re_auth_callback(token)
614 def _raise_on_error(self, error: Exception):
615 raise EventException(error, self._event)
617 async def _raise_on_error_async(self, error: Exception):
618 raise EventException(error, self._event)
621class InitializeConnectionCountObservability(EventListenerInterface):
622 """
623 Listener that initializes connection count observability.
624 """
626 @deprecated_function(
627 reason="Connection count is now tracked via record_connection_count(). "
628 "This functionality will be removed in the next major version",
629 version="7.4.0",
630 )
631 def listen(self, event: AfterPooledConnectionsInstantiationEvent):
632 # Initialize gauge only once, subsequent calls won't have an affect.
633 # Note: init_connection_count() and register_pools_connection_count()
634 # are deprecated and will emit their own warnings.
635 init_connection_count()
637 # Register pools for connection count observability.
638 register_pools_connection_count(event.connection_pools)