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,530 @@
1
+ import asyncio
2
+ import logging
3
+ from typing import Any, Awaitable, Callable, Coroutine, List, Optional, Union
4
+
5
+ from redis.asyncio.client import PubSubHandler
6
+ from redis.asyncio.multidb.command_executor import DefaultCommandExecutor
7
+ from redis.asyncio.multidb.config import DEFAULT_GRACE_PERIOD, MultiDbConfig
8
+ from redis.asyncio.multidb.database import AsyncDatabase, Databases
9
+ from redis.asyncio.multidb.failure_detector import AsyncFailureDetector
10
+ from redis.asyncio.multidb.healthcheck import HealthCheck, HealthCheckPolicy
11
+ from redis.background import BackgroundScheduler
12
+ from redis.commands import AsyncCoreCommands, AsyncRedisModuleCommands
13
+ from redis.multidb.circuit import CircuitBreaker
14
+ from redis.multidb.circuit import State as CBState
15
+ from redis.multidb.exception import NoValidDatabaseException, UnhealthyDatabaseException
16
+ from redis.typing import ChannelT, EncodableT, KeyT
17
+ from redis.utils import experimental
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+
22
+ @experimental
23
+ class MultiDBClient(AsyncRedisModuleCommands, AsyncCoreCommands):
24
+ """
25
+ Client that operates on multiple logical Redis databases.
26
+ Should be used in Active-Active database setups.
27
+ """
28
+
29
+ def __init__(self, config: MultiDbConfig):
30
+ self._databases = config.databases()
31
+ self._health_checks = config.default_health_checks()
32
+
33
+ if config.health_checks is not None:
34
+ self._health_checks.extend(config.health_checks)
35
+
36
+ self._health_check_interval = config.health_check_interval
37
+ self._health_check_policy: HealthCheckPolicy = config.health_check_policy.value(
38
+ config.health_check_probes, config.health_check_delay
39
+ )
40
+ self._failure_detectors = config.default_failure_detectors()
41
+
42
+ if config.failure_detectors is not None:
43
+ self._failure_detectors.extend(config.failure_detectors)
44
+
45
+ self._failover_strategy = (
46
+ config.default_failover_strategy()
47
+ if config.failover_strategy is None
48
+ else config.failover_strategy
49
+ )
50
+ self._failover_strategy.set_databases(self._databases)
51
+ self._auto_fallback_interval = config.auto_fallback_interval
52
+ self._event_dispatcher = config.event_dispatcher
53
+ self._command_retry = config.command_retry
54
+ self._command_retry.update_supported_errors([ConnectionRefusedError])
55
+ self.command_executor = DefaultCommandExecutor(
56
+ failure_detectors=self._failure_detectors,
57
+ databases=self._databases,
58
+ command_retry=self._command_retry,
59
+ failover_strategy=self._failover_strategy,
60
+ failover_attempts=config.failover_attempts,
61
+ failover_delay=config.failover_delay,
62
+ event_dispatcher=self._event_dispatcher,
63
+ auto_fallback_interval=self._auto_fallback_interval,
64
+ )
65
+ self.initialized = False
66
+ self._hc_lock = asyncio.Lock()
67
+ self._bg_scheduler = BackgroundScheduler()
68
+ self._config = config
69
+ self._recurring_hc_task = None
70
+ self._hc_tasks = []
71
+ self._half_open_state_task = None
72
+
73
+ async def __aenter__(self: "MultiDBClient") -> "MultiDBClient":
74
+ if not self.initialized:
75
+ await self.initialize()
76
+ return self
77
+
78
+ async def __aexit__(self, exc_type, exc_value, traceback):
79
+ if self._recurring_hc_task:
80
+ self._recurring_hc_task.cancel()
81
+ if self._half_open_state_task:
82
+ self._half_open_state_task.cancel()
83
+ for hc_task in self._hc_tasks:
84
+ hc_task.cancel()
85
+
86
+ async def initialize(self):
87
+ """
88
+ Perform initialization of databases to define their initial state.
89
+ """
90
+
91
+ async def raise_exception_on_failed_hc(error):
92
+ raise error
93
+
94
+ # Initial databases check to define initial state
95
+ await self._check_databases_health(on_error=raise_exception_on_failed_hc)
96
+
97
+ # Starts recurring health checks on the background.
98
+ self._recurring_hc_task = asyncio.create_task(
99
+ self._bg_scheduler.run_recurring_async(
100
+ self._health_check_interval,
101
+ self._check_databases_health,
102
+ )
103
+ )
104
+
105
+ is_active_db_found = False
106
+
107
+ for database, weight in self._databases:
108
+ # Set on state changed callback for each circuit.
109
+ database.circuit.on_state_changed(self._on_circuit_state_change_callback)
110
+
111
+ # Set states according to a weights and circuit state
112
+ if database.circuit.state == CBState.CLOSED and not is_active_db_found:
113
+ await self.command_executor.set_active_database(database)
114
+ is_active_db_found = True
115
+
116
+ if not is_active_db_found:
117
+ raise NoValidDatabaseException(
118
+ "Initial connection failed - no active database found"
119
+ )
120
+
121
+ self.initialized = True
122
+
123
+ def get_databases(self) -> Databases:
124
+ """
125
+ Returns a sorted (by weight) list of all databases.
126
+ """
127
+ return self._databases
128
+
129
+ async def set_active_database(self, database: AsyncDatabase) -> None:
130
+ """
131
+ Promote one of the existing databases to become an active.
132
+ """
133
+ exists = None
134
+
135
+ for existing_db, _ in self._databases:
136
+ if existing_db == database:
137
+ exists = True
138
+ break
139
+
140
+ if not exists:
141
+ raise ValueError("Given database is not a member of database list")
142
+
143
+ await self._check_db_health(database)
144
+
145
+ if database.circuit.state == CBState.CLOSED:
146
+ highest_weighted_db, _ = self._databases.get_top_n(1)[0]
147
+ await self.command_executor.set_active_database(database)
148
+ return
149
+
150
+ raise NoValidDatabaseException(
151
+ "Cannot set active database, database is unhealthy"
152
+ )
153
+
154
+ async def add_database(self, database: AsyncDatabase):
155
+ """
156
+ Adds a new database to the database list.
157
+ """
158
+ for existing_db, _ in self._databases:
159
+ if existing_db == database:
160
+ raise ValueError("Given database already exists")
161
+
162
+ await self._check_db_health(database)
163
+
164
+ highest_weighted_db, highest_weight = self._databases.get_top_n(1)[0]
165
+ self._databases.add(database, database.weight)
166
+ await self._change_active_database(database, highest_weighted_db)
167
+
168
+ async def _change_active_database(
169
+ self, new_database: AsyncDatabase, highest_weight_database: AsyncDatabase
170
+ ):
171
+ if (
172
+ new_database.weight > highest_weight_database.weight
173
+ and new_database.circuit.state == CBState.CLOSED
174
+ ):
175
+ await self.command_executor.set_active_database(new_database)
176
+
177
+ async def remove_database(self, database: AsyncDatabase):
178
+ """
179
+ Removes a database from the database list.
180
+ """
181
+ weight = self._databases.remove(database)
182
+ highest_weighted_db, highest_weight = self._databases.get_top_n(1)[0]
183
+
184
+ if (
185
+ highest_weight <= weight
186
+ and highest_weighted_db.circuit.state == CBState.CLOSED
187
+ ):
188
+ await self.command_executor.set_active_database(highest_weighted_db)
189
+
190
+ async def update_database_weight(self, database: AsyncDatabase, weight: float):
191
+ """
192
+ Updates a database from the database list.
193
+ """
194
+ exists = None
195
+
196
+ for existing_db, _ in self._databases:
197
+ if existing_db == database:
198
+ exists = True
199
+ break
200
+
201
+ if not exists:
202
+ raise ValueError("Given database is not a member of database list")
203
+
204
+ highest_weighted_db, highest_weight = self._databases.get_top_n(1)[0]
205
+ self._databases.update_weight(database, weight)
206
+ database.weight = weight
207
+ await self._change_active_database(database, highest_weighted_db)
208
+
209
+ def add_failure_detector(self, failure_detector: AsyncFailureDetector):
210
+ """
211
+ Adds a new failure detector to the database.
212
+ """
213
+ self._failure_detectors.append(failure_detector)
214
+
215
+ async def add_health_check(self, healthcheck: HealthCheck):
216
+ """
217
+ Adds a new health check to the database.
218
+ """
219
+ async with self._hc_lock:
220
+ self._health_checks.append(healthcheck)
221
+
222
+ async def execute_command(self, *args, **options):
223
+ """
224
+ Executes a single command and return its result.
225
+ """
226
+ if not self.initialized:
227
+ await self.initialize()
228
+
229
+ return await self.command_executor.execute_command(*args, **options)
230
+
231
+ def pipeline(self):
232
+ """
233
+ Enters into pipeline mode of the client.
234
+ """
235
+ return Pipeline(self)
236
+
237
+ async def transaction(
238
+ self,
239
+ func: Callable[["Pipeline"], Union[Any, Awaitable[Any]]],
240
+ *watches: KeyT,
241
+ shard_hint: Optional[str] = None,
242
+ value_from_callable: bool = False,
243
+ watch_delay: Optional[float] = None,
244
+ ):
245
+ """
246
+ Executes callable as transaction.
247
+ """
248
+ if not self.initialized:
249
+ await self.initialize()
250
+
251
+ return await self.command_executor.execute_transaction(
252
+ func,
253
+ *watches,
254
+ shard_hint=shard_hint,
255
+ value_from_callable=value_from_callable,
256
+ watch_delay=watch_delay,
257
+ )
258
+
259
+ async def pubsub(self, **kwargs):
260
+ """
261
+ Return a Publish/Subscribe object. With this object, you can
262
+ subscribe to channels and listen for messages that get published to
263
+ them.
264
+ """
265
+ if not self.initialized:
266
+ await self.initialize()
267
+
268
+ return PubSub(self, **kwargs)
269
+
270
+ async def _check_databases_health(
271
+ self,
272
+ on_error: Optional[Callable[[Exception], Coroutine[Any, Any, None]]] = None,
273
+ ):
274
+ """
275
+ Runs health checks as a recurring task.
276
+ Runs health checks against all databases.
277
+ """
278
+ try:
279
+ self._hc_tasks = [
280
+ asyncio.create_task(self._check_db_health(database))
281
+ for database, _ in self._databases
282
+ ]
283
+ results = await asyncio.wait_for(
284
+ asyncio.gather(
285
+ *self._hc_tasks,
286
+ return_exceptions=True,
287
+ ),
288
+ timeout=self._health_check_interval,
289
+ )
290
+ except asyncio.TimeoutError:
291
+ raise asyncio.TimeoutError(
292
+ "Health check execution exceeds health_check_interval"
293
+ )
294
+
295
+ for result in results:
296
+ if isinstance(result, UnhealthyDatabaseException):
297
+ unhealthy_db = result.database
298
+ unhealthy_db.circuit.state = CBState.OPEN
299
+
300
+ logger.exception(
301
+ "Health check failed, due to exception",
302
+ exc_info=result.original_exception,
303
+ )
304
+
305
+ if on_error:
306
+ on_error(result.original_exception)
307
+
308
+ async def _check_db_health(self, database: AsyncDatabase) -> bool:
309
+ """
310
+ Runs health checks on the given database until first failure.
311
+ """
312
+ # Health check will setup circuit state
313
+ is_healthy = await self._health_check_policy.execute(
314
+ self._health_checks, database
315
+ )
316
+
317
+ if not is_healthy:
318
+ if database.circuit.state != CBState.OPEN:
319
+ database.circuit.state = CBState.OPEN
320
+ return is_healthy
321
+ elif is_healthy and database.circuit.state != CBState.CLOSED:
322
+ database.circuit.state = CBState.CLOSED
323
+
324
+ return is_healthy
325
+
326
+ def _on_circuit_state_change_callback(
327
+ self, circuit: CircuitBreaker, old_state: CBState, new_state: CBState
328
+ ):
329
+ loop = asyncio.get_running_loop()
330
+
331
+ if new_state == CBState.HALF_OPEN:
332
+ self._half_open_state_task = asyncio.create_task(
333
+ self._check_db_health(circuit.database)
334
+ )
335
+ return
336
+
337
+ if old_state == CBState.CLOSED and new_state == CBState.OPEN:
338
+ loop.call_later(DEFAULT_GRACE_PERIOD, _half_open_circuit, circuit)
339
+
340
+ async def aclose(self):
341
+ if self.command_executor.active_database:
342
+ await self.command_executor.active_database.client.aclose()
343
+
344
+
345
+ def _half_open_circuit(circuit: CircuitBreaker):
346
+ circuit.state = CBState.HALF_OPEN
347
+
348
+
349
+ class Pipeline(AsyncRedisModuleCommands, AsyncCoreCommands):
350
+ """
351
+ Pipeline implementation for multiple logical Redis databases.
352
+ """
353
+
354
+ def __init__(self, client: MultiDBClient):
355
+ self._command_stack = []
356
+ self._client = client
357
+
358
+ async def __aenter__(self: "Pipeline") -> "Pipeline":
359
+ return self
360
+
361
+ async def __aexit__(self, exc_type, exc_value, traceback):
362
+ await self.reset()
363
+ await self._client.__aexit__(exc_type, exc_value, traceback)
364
+
365
+ def __await__(self):
366
+ return self._async_self().__await__()
367
+
368
+ async def _async_self(self):
369
+ return self
370
+
371
+ def __len__(self) -> int:
372
+ return len(self._command_stack)
373
+
374
+ def __bool__(self) -> bool:
375
+ """Pipeline instances should always evaluate to True"""
376
+ return True
377
+
378
+ async def reset(self) -> None:
379
+ self._command_stack = []
380
+
381
+ async def aclose(self) -> None:
382
+ """Close the pipeline"""
383
+ await self.reset()
384
+
385
+ def pipeline_execute_command(self, *args, **options) -> "Pipeline":
386
+ """
387
+ Stage a command to be executed when execute() is next called
388
+
389
+ Returns the current Pipeline object back so commands can be
390
+ chained together, such as:
391
+
392
+ pipe = pipe.set('foo', 'bar').incr('baz').decr('bang')
393
+
394
+ At some other point, you can then run: pipe.execute(),
395
+ which will execute all commands queued in the pipe.
396
+ """
397
+ self._command_stack.append((args, options))
398
+ return self
399
+
400
+ def execute_command(self, *args, **kwargs):
401
+ """Adds a command to the stack"""
402
+ return self.pipeline_execute_command(*args, **kwargs)
403
+
404
+ async def execute(self) -> List[Any]:
405
+ """Execute all the commands in the current pipeline"""
406
+ if not self._client.initialized:
407
+ await self._client.initialize()
408
+
409
+ try:
410
+ return await self._client.command_executor.execute_pipeline(
411
+ tuple(self._command_stack)
412
+ )
413
+ finally:
414
+ await self.reset()
415
+
416
+
417
+ class PubSub:
418
+ """
419
+ PubSub object for multi database client.
420
+ """
421
+
422
+ def __init__(self, client: MultiDBClient, **kwargs):
423
+ """Initialize the PubSub object for a multi-database client.
424
+
425
+ Args:
426
+ client: MultiDBClient instance to use for pub/sub operations
427
+ **kwargs: Additional keyword arguments to pass to the underlying pubsub implementation
428
+ """
429
+
430
+ self._client = client
431
+ self._client.command_executor.pubsub(**kwargs)
432
+
433
+ async def __aenter__(self) -> "PubSub":
434
+ return self
435
+
436
+ async def __aexit__(self, exc_type, exc_value, traceback) -> None:
437
+ await self.aclose()
438
+
439
+ async def aclose(self):
440
+ return await self._client.command_executor.execute_pubsub_method("aclose")
441
+
442
+ @property
443
+ def subscribed(self) -> bool:
444
+ return self._client.command_executor.active_pubsub.subscribed
445
+
446
+ async def execute_command(self, *args: EncodableT):
447
+ return await self._client.command_executor.execute_pubsub_method(
448
+ "execute_command", *args
449
+ )
450
+
451
+ async def psubscribe(self, *args: ChannelT, **kwargs: PubSubHandler):
452
+ """
453
+ Subscribe to channel patterns. Patterns supplied as keyword arguments
454
+ expect a pattern name as the key and a callable as the value. A
455
+ pattern's callable will be invoked automatically when a message is
456
+ received on that pattern rather than producing a message via
457
+ ``listen()``.
458
+ """
459
+ return await self._client.command_executor.execute_pubsub_method(
460
+ "psubscribe", *args, **kwargs
461
+ )
462
+
463
+ async def punsubscribe(self, *args: ChannelT):
464
+ """
465
+ Unsubscribe from the supplied patterns. If empty, unsubscribe from
466
+ all patterns.
467
+ """
468
+ return await self._client.command_executor.execute_pubsub_method(
469
+ "punsubscribe", *args
470
+ )
471
+
472
+ async def subscribe(self, *args: ChannelT, **kwargs: Callable):
473
+ """
474
+ Subscribe to channels. Channels supplied as keyword arguments expect
475
+ a channel name as the key and a callable as the value. A channel's
476
+ callable will be invoked automatically when a message is received on
477
+ that channel rather than producing a message via ``listen()`` or
478
+ ``get_message()``.
479
+ """
480
+ return await self._client.command_executor.execute_pubsub_method(
481
+ "subscribe", *args, **kwargs
482
+ )
483
+
484
+ async def unsubscribe(self, *args):
485
+ """
486
+ Unsubscribe from the supplied channels. If empty, unsubscribe from
487
+ all channels
488
+ """
489
+ return await self._client.command_executor.execute_pubsub_method(
490
+ "unsubscribe", *args
491
+ )
492
+
493
+ async def get_message(
494
+ self, ignore_subscribe_messages: bool = False, timeout: Optional[float] = 0.0
495
+ ):
496
+ """
497
+ Get the next message if one is available, otherwise None.
498
+
499
+ If timeout is specified, the system will wait for `timeout` seconds
500
+ before returning. Timeout should be specified as a floating point
501
+ number or None to wait indefinitely.
502
+ """
503
+ return await self._client.command_executor.execute_pubsub_method(
504
+ "get_message",
505
+ ignore_subscribe_messages=ignore_subscribe_messages,
506
+ timeout=timeout,
507
+ )
508
+
509
+ async def run(
510
+ self,
511
+ *,
512
+ exception_handler=None,
513
+ poll_timeout: float = 1.0,
514
+ ) -> None:
515
+ """Process pub/sub messages using registered callbacks.
516
+
517
+ This is the equivalent of :py:meth:`redis.PubSub.run_in_thread` in
518
+ redis-py, but it is a coroutine. To launch it as a separate task, use
519
+ ``asyncio.create_task``:
520
+
521
+ >>> task = asyncio.create_task(pubsub.run())
522
+
523
+ To shut it down, use asyncio cancellation:
524
+
525
+ >>> task.cancel()
526
+ >>> await task
527
+ """
528
+ return await self._client.command_executor.execute_pubsub_run(
529
+ sleep_time=poll_timeout, exception_handler=exception_handler, pubsub=self
530
+ )