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.
Files changed (51) hide show
  1. redis/__init__.py +1 -1
  2. redis/_parsers/base.py +6 -0
  3. redis/_parsers/helpers.py +64 -6
  4. redis/asyncio/client.py +14 -5
  5. redis/asyncio/cluster.py +5 -1
  6. redis/asyncio/connection.py +19 -1
  7. redis/asyncio/http/__init__.py +0 -0
  8. redis/asyncio/http/http_client.py +265 -0
  9. redis/asyncio/multidb/__init__.py +0 -0
  10. redis/asyncio/multidb/client.py +530 -0
  11. redis/asyncio/multidb/command_executor.py +339 -0
  12. redis/asyncio/multidb/config.py +210 -0
  13. redis/asyncio/multidb/database.py +69 -0
  14. redis/asyncio/multidb/event.py +84 -0
  15. redis/asyncio/multidb/failover.py +125 -0
  16. redis/asyncio/multidb/failure_detector.py +38 -0
  17. redis/asyncio/multidb/healthcheck.py +285 -0
  18. redis/background.py +204 -0
  19. redis/client.py +49 -27
  20. redis/cluster.py +9 -1
  21. redis/commands/core.py +64 -29
  22. redis/commands/json/commands.py +2 -2
  23. redis/commands/search/__init__.py +2 -2
  24. redis/commands/search/aggregation.py +24 -26
  25. redis/commands/search/commands.py +10 -10
  26. redis/commands/search/field.py +2 -2
  27. redis/commands/search/query.py +12 -12
  28. redis/connection.py +1613 -1263
  29. redis/data_structure.py +81 -0
  30. redis/event.py +84 -10
  31. redis/exceptions.py +8 -0
  32. redis/http/__init__.py +0 -0
  33. redis/http/http_client.py +425 -0
  34. redis/maint_notifications.py +18 -7
  35. redis/multidb/__init__.py +0 -0
  36. redis/multidb/circuit.py +144 -0
  37. redis/multidb/client.py +526 -0
  38. redis/multidb/command_executor.py +350 -0
  39. redis/multidb/config.py +207 -0
  40. redis/multidb/database.py +130 -0
  41. redis/multidb/event.py +89 -0
  42. redis/multidb/exception.py +17 -0
  43. redis/multidb/failover.py +125 -0
  44. redis/multidb/failure_detector.py +104 -0
  45. redis/multidb/healthcheck.py +282 -0
  46. redis/retry.py +14 -1
  47. redis/utils.py +34 -0
  48. {redis-7.0.0b2.dist-info → redis-7.0.1.dist-info}/METADATA +17 -4
  49. {redis-7.0.0b2.dist-info → redis-7.0.1.dist-info}/RECORD +51 -25
  50. {redis-7.0.0b2.dist-info → redis-7.0.1.dist-info}/WHEEL +0 -0
  51. {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
+ )
@@ -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