redis 6.3.0__py3-none-any.whl → 7.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. redis/__init__.py +1 -2
  2. redis/_parsers/base.py +193 -8
  3. redis/_parsers/helpers.py +64 -6
  4. redis/_parsers/hiredis.py +16 -10
  5. redis/_parsers/resp3.py +11 -5
  6. redis/asyncio/client.py +65 -8
  7. redis/asyncio/cluster.py +57 -14
  8. redis/asyncio/connection.py +62 -2
  9. redis/asyncio/http/__init__.py +0 -0
  10. redis/asyncio/http/http_client.py +265 -0
  11. redis/asyncio/multidb/__init__.py +0 -0
  12. redis/asyncio/multidb/client.py +530 -0
  13. redis/asyncio/multidb/command_executor.py +339 -0
  14. redis/asyncio/multidb/config.py +210 -0
  15. redis/asyncio/multidb/database.py +69 -0
  16. redis/asyncio/multidb/event.py +84 -0
  17. redis/asyncio/multidb/failover.py +125 -0
  18. redis/asyncio/multidb/failure_detector.py +38 -0
  19. redis/asyncio/multidb/healthcheck.py +285 -0
  20. redis/background.py +204 -0
  21. redis/cache.py +1 -0
  22. redis/client.py +99 -22
  23. redis/cluster.py +14 -3
  24. redis/commands/core.py +348 -313
  25. redis/commands/helpers.py +0 -20
  26. redis/commands/json/_util.py +4 -2
  27. redis/commands/json/commands.py +2 -2
  28. redis/commands/search/__init__.py +2 -2
  29. redis/commands/search/aggregation.py +28 -30
  30. redis/commands/search/commands.py +13 -13
  31. redis/commands/search/field.py +2 -2
  32. redis/commands/search/query.py +23 -23
  33. redis/commands/vectorset/__init__.py +1 -1
  34. redis/commands/vectorset/commands.py +50 -25
  35. redis/commands/vectorset/utils.py +40 -4
  36. redis/connection.py +1258 -90
  37. redis/data_structure.py +81 -0
  38. redis/event.py +88 -14
  39. redis/exceptions.py +8 -0
  40. redis/http/__init__.py +0 -0
  41. redis/http/http_client.py +425 -0
  42. redis/maint_notifications.py +810 -0
  43. redis/multidb/__init__.py +0 -0
  44. redis/multidb/circuit.py +144 -0
  45. redis/multidb/client.py +526 -0
  46. redis/multidb/command_executor.py +350 -0
  47. redis/multidb/config.py +207 -0
  48. redis/multidb/database.py +130 -0
  49. redis/multidb/event.py +89 -0
  50. redis/multidb/exception.py +17 -0
  51. redis/multidb/failover.py +125 -0
  52. redis/multidb/failure_detector.py +104 -0
  53. redis/multidb/healthcheck.py +282 -0
  54. redis/retry.py +14 -1
  55. redis/utils.py +34 -0
  56. {redis-6.3.0.dist-info → redis-7.0.0.dist-info}/METADATA +7 -4
  57. redis-7.0.0.dist-info/RECORD +105 -0
  58. redis-6.3.0.dist-info/RECORD +0 -78
  59. {redis-6.3.0.dist-info → redis-7.0.0.dist-info}/WHEEL +0 -0
  60. {redis-6.3.0.dist-info → redis-7.0.0.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,339 @@
1
+ from abc import abstractmethod
2
+ from asyncio import iscoroutinefunction
3
+ from datetime import datetime
4
+ from typing import Any, Awaitable, Callable, List, Optional, Union
5
+
6
+ from redis.asyncio import RedisCluster
7
+ from redis.asyncio.client import Pipeline, PubSub
8
+ from redis.asyncio.multidb.database import AsyncDatabase, Database, Databases
9
+ from redis.asyncio.multidb.event import (
10
+ AsyncActiveDatabaseChanged,
11
+ CloseConnectionOnActiveDatabaseChanged,
12
+ RegisterCommandFailure,
13
+ ResubscribeOnActiveDatabaseChanged,
14
+ )
15
+ from redis.asyncio.multidb.failover import (
16
+ DEFAULT_FAILOVER_ATTEMPTS,
17
+ DEFAULT_FAILOVER_DELAY,
18
+ AsyncFailoverStrategy,
19
+ DefaultFailoverStrategyExecutor,
20
+ FailoverStrategyExecutor,
21
+ )
22
+ from redis.asyncio.multidb.failure_detector import AsyncFailureDetector
23
+ from redis.asyncio.retry import Retry
24
+ from redis.event import AsyncOnCommandsFailEvent, EventDispatcherInterface
25
+ from redis.multidb.circuit import State as CBState
26
+ from redis.multidb.command_executor import BaseCommandExecutor, CommandExecutor
27
+ from redis.multidb.config import DEFAULT_AUTO_FALLBACK_INTERVAL
28
+ from redis.typing import KeyT
29
+
30
+
31
+ class AsyncCommandExecutor(CommandExecutor):
32
+ @property
33
+ @abstractmethod
34
+ def databases(self) -> Databases:
35
+ """Returns a list of databases."""
36
+ pass
37
+
38
+ @property
39
+ @abstractmethod
40
+ def failure_detectors(self) -> List[AsyncFailureDetector]:
41
+ """Returns a list of failure detectors."""
42
+ pass
43
+
44
+ @abstractmethod
45
+ def add_failure_detector(self, failure_detector: AsyncFailureDetector) -> None:
46
+ """Adds a new failure detector to the list of failure detectors."""
47
+ pass
48
+
49
+ @property
50
+ @abstractmethod
51
+ def active_database(self) -> Optional[AsyncDatabase]:
52
+ """Returns currently active database."""
53
+ pass
54
+
55
+ @abstractmethod
56
+ async def set_active_database(self, database: AsyncDatabase) -> None:
57
+ """Sets the currently active database."""
58
+ pass
59
+
60
+ @property
61
+ @abstractmethod
62
+ def active_pubsub(self) -> Optional[PubSub]:
63
+ """Returns currently active pubsub."""
64
+ pass
65
+
66
+ @active_pubsub.setter
67
+ @abstractmethod
68
+ def active_pubsub(self, pubsub: PubSub) -> None:
69
+ """Sets currently active pubsub."""
70
+ pass
71
+
72
+ @property
73
+ @abstractmethod
74
+ def failover_strategy_executor(self) -> FailoverStrategyExecutor:
75
+ """Returns failover strategy executor."""
76
+ pass
77
+
78
+ @property
79
+ @abstractmethod
80
+ def command_retry(self) -> Retry:
81
+ """Returns command retry object."""
82
+ pass
83
+
84
+ @abstractmethod
85
+ async def pubsub(self, **kwargs):
86
+ """Initializes a PubSub object on a currently active database"""
87
+ pass
88
+
89
+ @abstractmethod
90
+ async def execute_command(self, *args, **options):
91
+ """Executes a command and returns the result."""
92
+ pass
93
+
94
+ @abstractmethod
95
+ async def execute_pipeline(self, command_stack: tuple):
96
+ """Executes a stack of commands in pipeline."""
97
+ pass
98
+
99
+ @abstractmethod
100
+ async def execute_transaction(
101
+ self, transaction: Callable[[Pipeline], None], *watches, **options
102
+ ):
103
+ """Executes a transaction block wrapped in callback."""
104
+ pass
105
+
106
+ @abstractmethod
107
+ async def execute_pubsub_method(self, method_name: str, *args, **kwargs):
108
+ """Executes a given method on active pub/sub."""
109
+ pass
110
+
111
+ @abstractmethod
112
+ async def execute_pubsub_run(self, sleep_time: float, **kwargs) -> Any:
113
+ """Executes pub/sub run in a thread."""
114
+ pass
115
+
116
+
117
+ class DefaultCommandExecutor(BaseCommandExecutor, AsyncCommandExecutor):
118
+ def __init__(
119
+ self,
120
+ failure_detectors: List[AsyncFailureDetector],
121
+ databases: Databases,
122
+ command_retry: Retry,
123
+ failover_strategy: AsyncFailoverStrategy,
124
+ event_dispatcher: EventDispatcherInterface,
125
+ failover_attempts: int = DEFAULT_FAILOVER_ATTEMPTS,
126
+ failover_delay: float = DEFAULT_FAILOVER_DELAY,
127
+ auto_fallback_interval: float = DEFAULT_AUTO_FALLBACK_INTERVAL,
128
+ ):
129
+ """
130
+ Initialize the DefaultCommandExecutor instance.
131
+
132
+ Args:
133
+ failure_detectors: List of failure detector instances to monitor database health
134
+ databases: Collection of available databases to execute commands on
135
+ command_retry: Retry policy for failed command execution
136
+ failover_strategy: Strategy for handling database failover
137
+ event_dispatcher: Interface for dispatching events
138
+ failover_attempts: Number of failover attempts
139
+ failover_delay: Delay between failover attempts
140
+ auto_fallback_interval: Time interval in seconds between attempts to fall back to a primary database
141
+ """
142
+ super().__init__(auto_fallback_interval)
143
+
144
+ for fd in failure_detectors:
145
+ fd.set_command_executor(command_executor=self)
146
+
147
+ self._databases = databases
148
+ self._failure_detectors = failure_detectors
149
+ self._command_retry = command_retry
150
+ self._failover_strategy_executor = DefaultFailoverStrategyExecutor(
151
+ failover_strategy, failover_attempts, failover_delay
152
+ )
153
+ self._event_dispatcher = event_dispatcher
154
+ self._active_database: Optional[Database] = None
155
+ self._active_pubsub: Optional[PubSub] = None
156
+ self._active_pubsub_kwargs = {}
157
+ self._setup_event_dispatcher()
158
+ self._schedule_next_fallback()
159
+
160
+ @property
161
+ def databases(self) -> Databases:
162
+ return self._databases
163
+
164
+ @property
165
+ def failure_detectors(self) -> List[AsyncFailureDetector]:
166
+ return self._failure_detectors
167
+
168
+ def add_failure_detector(self, failure_detector: AsyncFailureDetector) -> None:
169
+ self._failure_detectors.append(failure_detector)
170
+
171
+ @property
172
+ def active_database(self) -> Optional[AsyncDatabase]:
173
+ return self._active_database
174
+
175
+ async def set_active_database(self, database: AsyncDatabase) -> None:
176
+ old_active = self._active_database
177
+ self._active_database = database
178
+
179
+ if old_active is not None and old_active is not database:
180
+ await self._event_dispatcher.dispatch_async(
181
+ AsyncActiveDatabaseChanged(
182
+ old_active,
183
+ self._active_database,
184
+ self,
185
+ **self._active_pubsub_kwargs,
186
+ )
187
+ )
188
+
189
+ @property
190
+ def active_pubsub(self) -> Optional[PubSub]:
191
+ return self._active_pubsub
192
+
193
+ @active_pubsub.setter
194
+ def active_pubsub(self, pubsub: PubSub) -> None:
195
+ self._active_pubsub = pubsub
196
+
197
+ @property
198
+ def failover_strategy_executor(self) -> FailoverStrategyExecutor:
199
+ return self._failover_strategy_executor
200
+
201
+ @property
202
+ def command_retry(self) -> Retry:
203
+ return self._command_retry
204
+
205
+ def pubsub(self, **kwargs):
206
+ if self._active_pubsub is None:
207
+ if isinstance(self._active_database.client, RedisCluster):
208
+ raise ValueError("PubSub is not supported for RedisCluster")
209
+
210
+ self._active_pubsub = self._active_database.client.pubsub(**kwargs)
211
+ self._active_pubsub_kwargs = kwargs
212
+
213
+ async def execute_command(self, *args, **options):
214
+ async def callback():
215
+ response = await self._active_database.client.execute_command(
216
+ *args, **options
217
+ )
218
+ await self._register_command_execution(args)
219
+ return response
220
+
221
+ return await self._execute_with_failure_detection(callback, args)
222
+
223
+ async def execute_pipeline(self, command_stack: tuple):
224
+ async def callback():
225
+ async with self._active_database.client.pipeline() as pipe:
226
+ for command, options in command_stack:
227
+ pipe.execute_command(*command, **options)
228
+
229
+ response = await pipe.execute()
230
+ await self._register_command_execution(command_stack)
231
+ return response
232
+
233
+ return await self._execute_with_failure_detection(callback, command_stack)
234
+
235
+ async def execute_transaction(
236
+ self,
237
+ func: Callable[["Pipeline"], Union[Any, Awaitable[Any]]],
238
+ *watches: KeyT,
239
+ shard_hint: Optional[str] = None,
240
+ value_from_callable: bool = False,
241
+ watch_delay: Optional[float] = None,
242
+ ):
243
+ async def callback():
244
+ response = await self._active_database.client.transaction(
245
+ func,
246
+ *watches,
247
+ shard_hint=shard_hint,
248
+ value_from_callable=value_from_callable,
249
+ watch_delay=watch_delay,
250
+ )
251
+ await self._register_command_execution(())
252
+ return response
253
+
254
+ return await self._execute_with_failure_detection(callback)
255
+
256
+ async def execute_pubsub_method(self, method_name: str, *args, **kwargs):
257
+ async def callback():
258
+ method = getattr(self.active_pubsub, method_name)
259
+ if iscoroutinefunction(method):
260
+ response = await method(*args, **kwargs)
261
+ else:
262
+ response = method(*args, **kwargs)
263
+
264
+ await self._register_command_execution(args)
265
+ return response
266
+
267
+ return await self._execute_with_failure_detection(callback, *args)
268
+
269
+ async def execute_pubsub_run(
270
+ self, sleep_time: float, exception_handler=None, pubsub=None
271
+ ) -> Any:
272
+ async def callback():
273
+ return await self._active_pubsub.run(
274
+ poll_timeout=sleep_time,
275
+ exception_handler=exception_handler,
276
+ pubsub=pubsub,
277
+ )
278
+
279
+ return await self._execute_with_failure_detection(callback)
280
+
281
+ async def _execute_with_failure_detection(
282
+ self, callback: Callable, cmds: tuple = ()
283
+ ):
284
+ """
285
+ Execute a commands execution callback with failure detection.
286
+ """
287
+
288
+ async def wrapper():
289
+ # On each retry we need to check active database as it might change.
290
+ await self._check_active_database()
291
+ return await callback()
292
+
293
+ return await self._command_retry.call_with_retry(
294
+ lambda: wrapper(),
295
+ lambda error: self._on_command_fail(error, *cmds),
296
+ )
297
+
298
+ async def _check_active_database(self):
299
+ """
300
+ Checks if active a database needs to be updated.
301
+ """
302
+ if (
303
+ self._active_database is None
304
+ or self._active_database.circuit.state != CBState.CLOSED
305
+ or (
306
+ self._auto_fallback_interval != DEFAULT_AUTO_FALLBACK_INTERVAL
307
+ and self._next_fallback_attempt <= datetime.now()
308
+ )
309
+ ):
310
+ await self.set_active_database(
311
+ await self._failover_strategy_executor.execute()
312
+ )
313
+ self._schedule_next_fallback()
314
+
315
+ async def _on_command_fail(self, error, *args):
316
+ await self._event_dispatcher.dispatch_async(
317
+ AsyncOnCommandsFailEvent(args, error)
318
+ )
319
+
320
+ async def _register_command_execution(self, cmd: tuple):
321
+ for detector in self._failure_detectors:
322
+ await detector.register_command_execution(cmd)
323
+
324
+ def _setup_event_dispatcher(self):
325
+ """
326
+ Registers necessary listeners.
327
+ """
328
+ failure_listener = RegisterCommandFailure(self._failure_detectors)
329
+ resubscribe_listener = ResubscribeOnActiveDatabaseChanged()
330
+ close_connection_listener = CloseConnectionOnActiveDatabaseChanged()
331
+ self._event_dispatcher.register_listeners(
332
+ {
333
+ AsyncOnCommandsFailEvent: [failure_listener],
334
+ AsyncActiveDatabaseChanged: [
335
+ close_connection_listener,
336
+ resubscribe_listener,
337
+ ],
338
+ }
339
+ )
@@ -0,0 +1,210 @@
1
+ from dataclasses import dataclass, field
2
+ from typing import List, Optional, Type, Union
3
+
4
+ import pybreaker
5
+
6
+ from redis.asyncio import ConnectionPool, Redis, RedisCluster
7
+ from redis.asyncio.multidb.database import Database, Databases
8
+ from redis.asyncio.multidb.failover import (
9
+ DEFAULT_FAILOVER_ATTEMPTS,
10
+ DEFAULT_FAILOVER_DELAY,
11
+ AsyncFailoverStrategy,
12
+ WeightBasedFailoverStrategy,
13
+ )
14
+ from redis.asyncio.multidb.failure_detector import (
15
+ AsyncFailureDetector,
16
+ FailureDetectorAsyncWrapper,
17
+ )
18
+ from redis.asyncio.multidb.healthcheck import (
19
+ DEFAULT_HEALTH_CHECK_DELAY,
20
+ DEFAULT_HEALTH_CHECK_INTERVAL,
21
+ DEFAULT_HEALTH_CHECK_POLICY,
22
+ DEFAULT_HEALTH_CHECK_PROBES,
23
+ HealthCheck,
24
+ HealthCheckPolicies,
25
+ PingHealthCheck,
26
+ )
27
+ from redis.asyncio.retry import Retry
28
+ from redis.backoff import ExponentialWithJitterBackoff, NoBackoff
29
+ from redis.data_structure import WeightedList
30
+ from redis.event import EventDispatcher, EventDispatcherInterface
31
+ from redis.multidb.circuit import (
32
+ DEFAULT_GRACE_PERIOD,
33
+ CircuitBreaker,
34
+ PBCircuitBreakerAdapter,
35
+ )
36
+ from redis.multidb.failure_detector import (
37
+ DEFAULT_FAILURE_RATE_THRESHOLD,
38
+ DEFAULT_FAILURES_DETECTION_WINDOW,
39
+ DEFAULT_MIN_NUM_FAILURES,
40
+ CommandFailureDetector,
41
+ )
42
+
43
+ DEFAULT_AUTO_FALLBACK_INTERVAL = 120
44
+
45
+
46
+ def default_event_dispatcher() -> EventDispatcherInterface:
47
+ return EventDispatcher()
48
+
49
+
50
+ @dataclass
51
+ class DatabaseConfig:
52
+ """
53
+ Dataclass representing the configuration for a database connection.
54
+
55
+ This class is used to store configuration settings for a database connection,
56
+ including client options, connection sourcing details, circuit breaker settings,
57
+ and cluster-specific properties. It provides a structure for defining these
58
+ attributes and allows for the creation of customized configurations for various
59
+ database setups.
60
+
61
+ Attributes:
62
+ weight (float): Weight of the database to define the active one.
63
+ client_kwargs (dict): Additional parameters for the database client connection.
64
+ from_url (Optional[str]): Redis URL way of connecting to the database.
65
+ from_pool (Optional[ConnectionPool]): A pre-configured connection pool to use.
66
+ circuit (Optional[CircuitBreaker]): Custom circuit breaker implementation.
67
+ grace_period (float): Grace period after which we need to check if the circuit could be closed again.
68
+ health_check_url (Optional[str]): URL for health checks. Cluster FQDN is typically used
69
+ on public Redis Enterprise endpoints.
70
+
71
+ Methods:
72
+ default_circuit_breaker:
73
+ Generates and returns a default CircuitBreaker instance adapted for use.
74
+ """
75
+
76
+ weight: float = 1.0
77
+ client_kwargs: dict = field(default_factory=dict)
78
+ from_url: Optional[str] = None
79
+ from_pool: Optional[ConnectionPool] = None
80
+ circuit: Optional[CircuitBreaker] = None
81
+ grace_period: float = DEFAULT_GRACE_PERIOD
82
+ health_check_url: Optional[str] = None
83
+
84
+ def default_circuit_breaker(self) -> CircuitBreaker:
85
+ circuit_breaker = pybreaker.CircuitBreaker(reset_timeout=self.grace_period)
86
+ return PBCircuitBreakerAdapter(circuit_breaker)
87
+
88
+
89
+ @dataclass
90
+ class MultiDbConfig:
91
+ """
92
+ Configuration class for managing multiple database connections in a resilient and fail-safe manner.
93
+
94
+ Attributes:
95
+ databases_config: A list of database configurations.
96
+ client_class: The client class used to manage database connections.
97
+ command_retry: Retry strategy for executing database commands.
98
+ failure_detectors: Optional list of additional failure detectors for monitoring database failures.
99
+ min_num_failures: Minimal count of failures required for failover
100
+ failure_rate_threshold: Percentage of failures required for failover
101
+ failures_detection_window: Time interval for tracking database failures.
102
+ health_checks: Optional list of additional health checks performed on databases.
103
+ health_check_interval: Time interval for executing health checks.
104
+ health_check_probes: Number of attempts to evaluate the health of a database.
105
+ health_check_delay: Delay between health check attempts.
106
+ failover_strategy: Optional strategy for handling database failover scenarios.
107
+ failover_attempts: Number of retries allowed for failover operations.
108
+ failover_delay: Delay between failover attempts.
109
+ auto_fallback_interval: Time interval to trigger automatic fallback.
110
+ event_dispatcher: Interface for dispatching events related to database operations.
111
+
112
+ Methods:
113
+ databases:
114
+ Retrieves a collection of database clients managed by weighted configurations.
115
+ Initializes database clients based on the provided configuration and removes
116
+ redundant retry objects for lower-level clients to rely on global retry logic.
117
+
118
+ default_failure_detectors:
119
+ Returns the default list of failure detectors used to monitor database failures.
120
+
121
+ default_health_checks:
122
+ Returns the default list of health checks used to monitor database health
123
+ with specific retry and backoff strategies.
124
+
125
+ default_failover_strategy:
126
+ Provides the default failover strategy used for handling failover scenarios
127
+ with defined retry and backoff configurations.
128
+ """
129
+
130
+ databases_config: List[DatabaseConfig]
131
+ client_class: Type[Union[Redis, RedisCluster]] = Redis
132
+ command_retry: Retry = Retry(
133
+ backoff=ExponentialWithJitterBackoff(base=1, cap=10), retries=3
134
+ )
135
+ failure_detectors: Optional[List[AsyncFailureDetector]] = None
136
+ min_num_failures: int = DEFAULT_MIN_NUM_FAILURES
137
+ failure_rate_threshold: float = DEFAULT_FAILURE_RATE_THRESHOLD
138
+ failures_detection_window: float = DEFAULT_FAILURES_DETECTION_WINDOW
139
+ health_checks: Optional[List[HealthCheck]] = None
140
+ health_check_interval: float = DEFAULT_HEALTH_CHECK_INTERVAL
141
+ health_check_probes: int = DEFAULT_HEALTH_CHECK_PROBES
142
+ health_check_delay: float = DEFAULT_HEALTH_CHECK_DELAY
143
+ health_check_policy: HealthCheckPolicies = DEFAULT_HEALTH_CHECK_POLICY
144
+ failover_strategy: Optional[AsyncFailoverStrategy] = None
145
+ failover_attempts: int = DEFAULT_FAILOVER_ATTEMPTS
146
+ failover_delay: float = DEFAULT_FAILOVER_DELAY
147
+ auto_fallback_interval: float = DEFAULT_AUTO_FALLBACK_INTERVAL
148
+ event_dispatcher: EventDispatcherInterface = field(
149
+ default_factory=default_event_dispatcher
150
+ )
151
+
152
+ def databases(self) -> Databases:
153
+ databases = WeightedList()
154
+
155
+ for database_config in self.databases_config:
156
+ # The retry object is not used in the lower level clients, so we can safely remove it.
157
+ # We rely on command_retry in terms of global retries.
158
+ database_config.client_kwargs.update(
159
+ {"retry": Retry(retries=0, backoff=NoBackoff())}
160
+ )
161
+
162
+ if database_config.from_url:
163
+ client = self.client_class.from_url(
164
+ database_config.from_url, **database_config.client_kwargs
165
+ )
166
+ elif database_config.from_pool:
167
+ database_config.from_pool.set_retry(
168
+ Retry(retries=0, backoff=NoBackoff())
169
+ )
170
+ client = self.client_class.from_pool(
171
+ connection_pool=database_config.from_pool
172
+ )
173
+ else:
174
+ client = self.client_class(**database_config.client_kwargs)
175
+
176
+ circuit = (
177
+ database_config.default_circuit_breaker()
178
+ if database_config.circuit is None
179
+ else database_config.circuit
180
+ )
181
+ databases.add(
182
+ Database(
183
+ client=client,
184
+ circuit=circuit,
185
+ weight=database_config.weight,
186
+ health_check_url=database_config.health_check_url,
187
+ ),
188
+ database_config.weight,
189
+ )
190
+
191
+ return databases
192
+
193
+ def default_failure_detectors(self) -> List[AsyncFailureDetector]:
194
+ return [
195
+ FailureDetectorAsyncWrapper(
196
+ CommandFailureDetector(
197
+ min_num_failures=self.min_num_failures,
198
+ failure_rate_threshold=self.failure_rate_threshold,
199
+ failure_detection_window=self.failures_detection_window,
200
+ )
201
+ ),
202
+ ]
203
+
204
+ def default_health_checks(self) -> List[HealthCheck]:
205
+ return [
206
+ PingHealthCheck(),
207
+ ]
208
+
209
+ def default_failover_strategy(self) -> AsyncFailoverStrategy:
210
+ return WeightBasedFailoverStrategy()
@@ -0,0 +1,69 @@
1
+ from abc import abstractmethod
2
+ from typing import Optional, Union
3
+
4
+ from redis.asyncio import Redis, RedisCluster
5
+ from redis.data_structure import WeightedList
6
+ from redis.multidb.circuit import CircuitBreaker
7
+ from redis.multidb.database import AbstractDatabase, BaseDatabase
8
+ from redis.typing import Number
9
+
10
+
11
+ class AsyncDatabase(AbstractDatabase):
12
+ """Database with an underlying asynchronous redis client."""
13
+
14
+ @property
15
+ @abstractmethod
16
+ def client(self) -> Union[Redis, RedisCluster]:
17
+ """The underlying redis client."""
18
+ pass
19
+
20
+ @client.setter
21
+ @abstractmethod
22
+ def client(self, client: Union[Redis, RedisCluster]):
23
+ """Set the underlying redis client."""
24
+ pass
25
+
26
+ @property
27
+ @abstractmethod
28
+ def circuit(self) -> CircuitBreaker:
29
+ """Circuit breaker for the current database."""
30
+ pass
31
+
32
+ @circuit.setter
33
+ @abstractmethod
34
+ def circuit(self, circuit: CircuitBreaker):
35
+ """Set the circuit breaker for the current database."""
36
+ pass
37
+
38
+
39
+ Databases = WeightedList[tuple[AsyncDatabase, Number]]
40
+
41
+
42
+ class Database(BaseDatabase, AsyncDatabase):
43
+ def __init__(
44
+ self,
45
+ client: Union[Redis, RedisCluster],
46
+ circuit: CircuitBreaker,
47
+ weight: float,
48
+ health_check_url: Optional[str] = None,
49
+ ):
50
+ self._client = client
51
+ self._cb = circuit
52
+ self._cb.database = self
53
+ super().__init__(weight, health_check_url)
54
+
55
+ @property
56
+ def client(self) -> Union[Redis, RedisCluster]:
57
+ return self._client
58
+
59
+ @client.setter
60
+ def client(self, client: Union[Redis, RedisCluster]):
61
+ self._client = client
62
+
63
+ @property
64
+ def circuit(self) -> CircuitBreaker:
65
+ return self._cb
66
+
67
+ @circuit.setter
68
+ def circuit(self, circuit: CircuitBreaker):
69
+ self._cb = circuit
@@ -0,0 +1,84 @@
1
+ from typing import List
2
+
3
+ from redis.asyncio import Redis
4
+ from redis.asyncio.multidb.database import AsyncDatabase
5
+ from redis.asyncio.multidb.failure_detector import AsyncFailureDetector
6
+ from redis.event import AsyncEventListenerInterface, AsyncOnCommandsFailEvent
7
+
8
+
9
+ class AsyncActiveDatabaseChanged:
10
+ """
11
+ Event fired when an async active database has been changed.
12
+ """
13
+
14
+ def __init__(
15
+ self,
16
+ old_database: AsyncDatabase,
17
+ new_database: AsyncDatabase,
18
+ command_executor,
19
+ **kwargs,
20
+ ):
21
+ self._old_database = old_database
22
+ self._new_database = new_database
23
+ self._command_executor = command_executor
24
+ self._kwargs = kwargs
25
+
26
+ @property
27
+ def old_database(self) -> AsyncDatabase:
28
+ return self._old_database
29
+
30
+ @property
31
+ def new_database(self) -> AsyncDatabase:
32
+ return self._new_database
33
+
34
+ @property
35
+ def command_executor(self):
36
+ return self._command_executor
37
+
38
+ @property
39
+ def kwargs(self):
40
+ return self._kwargs
41
+
42
+
43
+ class ResubscribeOnActiveDatabaseChanged(AsyncEventListenerInterface):
44
+ """
45
+ Re-subscribe the currently active pub / sub to a new active database.
46
+ """
47
+
48
+ async def listen(self, event: AsyncActiveDatabaseChanged):
49
+ old_pubsub = event.command_executor.active_pubsub
50
+
51
+ if old_pubsub is not None:
52
+ # Re-assign old channels and patterns so they will be automatically subscribed on connection.
53
+ new_pubsub = event.new_database.client.pubsub(**event.kwargs)
54
+ new_pubsub.channels = old_pubsub.channels
55
+ new_pubsub.patterns = old_pubsub.patterns
56
+ await new_pubsub.on_connect(None)
57
+ event.command_executor.active_pubsub = new_pubsub
58
+ await old_pubsub.aclose()
59
+
60
+
61
+ class CloseConnectionOnActiveDatabaseChanged(AsyncEventListenerInterface):
62
+ """
63
+ Close connection to the old active database.
64
+ """
65
+
66
+ async def listen(self, event: AsyncActiveDatabaseChanged):
67
+ await event.old_database.client.aclose()
68
+
69
+ if isinstance(event.old_database.client, Redis):
70
+ await event.old_database.client.connection_pool.update_active_connections_for_reconnect()
71
+ await event.old_database.client.connection_pool.disconnect()
72
+
73
+
74
+ class RegisterCommandFailure(AsyncEventListenerInterface):
75
+ """
76
+ Event listener that registers command failures and passing it to the failure detectors.
77
+ """
78
+
79
+ def __init__(self, failure_detectors: List[AsyncFailureDetector]):
80
+ self._failure_detectors = failure_detectors
81
+
82
+ async def listen(self, event: AsyncOnCommandsFailEvent) -> None:
83
+ for failure_detector in self._failure_detectors:
84
+ await failure_detector.register_failure(event.exception, event.commands)