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