Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/redis/asyncio/client.py: 22%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

762 statements  

1import asyncio 

2import copy 

3import inspect 

4import math 

5import re 

6import time 

7import warnings 

8from typing import ( 

9 TYPE_CHECKING, 

10 Any, 

11 AsyncIterator, 

12 Awaitable, 

13 Callable, 

14 Dict, 

15 Iterable, 

16 List, 

17 Literal, 

18 Mapping, 

19 MutableMapping, 

20 Optional, 

21 Protocol, 

22 Set, 

23 Tuple, 

24 Type, 

25 TypedDict, 

26 TypeVar, 

27 Union, 

28 cast, 

29) 

30 

31from redis._defaults import ( 

32 DEFAULT_RETRY_BASE, 

33 DEFAULT_RETRY_CAP, 

34 DEFAULT_RETRY_COUNT, 

35 DEFAULT_SOCKET_CONNECT_TIMEOUT, 

36 DEFAULT_SOCKET_READ_SIZE, 

37 DEFAULT_SOCKET_TIMEOUT, 

38) 

39from redis._parsers.helpers import bool_ok, get_response_callbacks 

40from redis.asyncio.connection import ( 

41 Connection, 

42 ConnectionPool, 

43 SSLConnection, 

44 UnixDomainSocketConnection, 

45) 

46from redis.asyncio.lock import Lock 

47from redis.asyncio.observability.recorder import ( 

48 record_error_count, 

49 record_operation_duration, 

50 record_pubsub_message, 

51) 

52from redis.asyncio.retry import Retry 

53from redis.backoff import ExponentialWithJitterBackoff 

54from redis.client import ( 

55 EMPTY_RESPONSE, 

56 NEVER_DECODE, 

57 AbstractRedis, 

58 CaseInsensitiveDict, 

59) 

60from redis.commands import ( 

61 AsyncCoreCommands, 

62 AsyncRedisModuleCommands, 

63 AsyncSentinelCommands, 

64 list_or_args, 

65) 

66from redis.commands.helpers import parse_pubsub_subscriptions, pubsub_subscription_args 

67from redis.credentials import CredentialProvider 

68from redis.driver_info import DriverInfo, resolve_driver_info 

69from redis.event import ( 

70 AfterPooledConnectionsInstantiationEvent, 

71 AfterPubSubConnectionInstantiationEvent, 

72 AfterSingleConnectionInstantiationEvent, 

73 ClientType, 

74 EventDispatcher, 

75) 

76from redis.exceptions import ( 

77 ConnectionError, 

78 ExecAbortError, 

79 PubSubError, 

80 RedisError, 

81 ResponseError, 

82 WatchError, 

83) 

84from redis.observability.attributes import PubSubDirection 

85from redis.typing import ChannelT, EncodableT, KeyT, PubSubHandler, Subscription 

86from redis.utils import ( 

87 SENTINEL, 

88 SSL_AVAILABLE, 

89 _set_info_logger, 

90 deprecated_args, 

91 deprecated_function, 

92 safe_str, 

93 str_if_bytes, 

94 truncate_text, 

95) 

96 

97if TYPE_CHECKING and SSL_AVAILABLE: 

98 from ssl import TLSVersion, VerifyFlags, VerifyMode 

99else: 

100 TLSVersion = None 

101 VerifyMode = None 

102 VerifyFlags = None 

103 

104_KeyT = TypeVar("_KeyT", bound=KeyT) 

105_ArgT = TypeVar("_ArgT", KeyT, EncodableT) 

106_RedisT = TypeVar("_RedisT", bound="Redis") 

107_NormalizeKeysT = TypeVar("_NormalizeKeysT", bound=Mapping[ChannelT, object]) 

108if TYPE_CHECKING: 

109 from redis.asyncio.keyspace_notifications import AsyncKeyspaceNotifications 

110 from redis.commands.core import Script 

111 

112 

113class ResponseCallbackProtocol(Protocol): 

114 def __call__(self, response: Any, **kwargs): ... 

115 

116 

117class AsyncResponseCallbackProtocol(Protocol): 

118 async def __call__(self, response: Any, **kwargs): ... 

119 

120 

121ResponseCallbackT = Union[ResponseCallbackProtocol, AsyncResponseCallbackProtocol] 

122 

123 

124class Redis( 

125 AbstractRedis, AsyncRedisModuleCommands, AsyncCoreCommands, AsyncSentinelCommands 

126): 

127 """ 

128 Implementation of the Redis protocol. 

129 

130 This abstract class provides a Python interface to all Redis commands 

131 and an implementation of the Redis protocol. 

132 

133 Pipelines derive from this, implementing how 

134 the commands are sent and received to the Redis server. Based on 

135 configuration, an instance will either use a ConnectionPool, or 

136 Connection object to talk to redis. 

137 """ 

138 

139 # Type discrimination marker for @overload self-type pattern 

140 _is_async_client: Literal[True] = True 

141 

142 response_callbacks: MutableMapping[Union[str, bytes], ResponseCallbackT] 

143 

144 @classmethod 

145 def from_url( 

146 cls: Type["Redis"], 

147 url: str, 

148 single_connection_client: bool = False, 

149 auto_close_connection_pool: Optional[bool] = None, 

150 **kwargs, 

151 ) -> "Redis": 

152 """ 

153 Return a Redis client object configured from the given URL 

154 

155 For example:: 

156 

157 redis://[[username]:[password]]@localhost:6379/0 

158 rediss://[[username]:[password]]@localhost:6379/0 

159 unix://[username@]/path/to/socket.sock?db=0[&password=password] 

160 

161 Three URL schemes are supported: 

162 

163 - `redis://` creates a TCP socket connection. See more at: 

164 <https://www.iana.org/assignments/uri-schemes/prov/redis> 

165 - `rediss://` creates a SSL wrapped TCP socket connection. See more at: 

166 <https://www.iana.org/assignments/uri-schemes/prov/rediss> 

167 - ``unix://``: creates a Unix Domain Socket connection. 

168 

169 The username, password, hostname, path and all querystring values 

170 are passed through urllib.parse.unquote in order to replace any 

171 percent-encoded values with their corresponding characters. 

172 

173 There are several ways to specify a database number. The first value 

174 found will be used: 

175 

176 1. A ``db`` querystring option, e.g. redis://localhost?db=0 

177 

178 2. If using the redis:// or rediss:// schemes, the path argument 

179 of the url, e.g. redis://localhost/0 

180 

181 3. A ``db`` keyword argument to this function. 

182 

183 If none of these options are specified, the default db=0 is used. 

184 

185 All querystring options are cast to their appropriate Python types. 

186 Boolean arguments can be specified with string values "True"/"False" 

187 or "Yes"/"No". Values that cannot be properly cast cause a 

188 ``ValueError`` to be raised. Once parsed, the querystring arguments 

189 and keyword arguments are passed to the ``ConnectionPool``'s 

190 class initializer. In the case of conflicting arguments, querystring 

191 arguments always win. 

192 

193 """ 

194 connection_pool = ConnectionPool.from_url(url, **kwargs) 

195 client = cls( 

196 connection_pool=connection_pool, 

197 single_connection_client=single_connection_client, 

198 ) 

199 if auto_close_connection_pool is not None: 

200 warnings.warn( 

201 DeprecationWarning( 

202 '"auto_close_connection_pool" is deprecated ' 

203 "since version 5.0.1. " 

204 "Please create a ConnectionPool explicitly and " 

205 "provide to the Redis() constructor instead." 

206 ) 

207 ) 

208 else: 

209 auto_close_connection_pool = True 

210 client.auto_close_connection_pool = auto_close_connection_pool 

211 return client 

212 

213 @classmethod 

214 def from_pool( 

215 cls: Type["Redis"], 

216 connection_pool: ConnectionPool, 

217 ) -> "Redis": 

218 """ 

219 Return a Redis client from the given connection pool. 

220 The Redis client will take ownership of the connection pool and 

221 close it when the Redis client is closed. 

222 

223 Because the client closes (disconnects all connections in) the pool 

224 when it is closed or garbage-collected, the pool must not be shared 

225 with other clients. Constructing multiple clients from the same pool 

226 via ``from_pool`` -- for example one per request across tasks -- is 

227 not safe: when one client is closed it will disconnect connections 

228 still in use by the others. 

229 

230 To share a single pool across clients, construct the pool explicitly 

231 and manage its lifecycle instead. Unlike ``from_pool``, the plain 

232 ``Redis(connection_pool=pool)`` constructor does not take ownership of 

233 the pool and will not close it, so a pool created this way can be 

234 safely shared across clients. ``ConnectionPool`` supports the async 

235 context manager protocol for this:: 

236 

237 async with ConnectionPool.from_url(url) as pool: 

238 r = Redis(connection_pool=pool) 

239 """ 

240 client = cls( 

241 connection_pool=connection_pool, 

242 ) 

243 client.auto_close_connection_pool = True 

244 return client 

245 

246 @deprecated_args( 

247 args_to_warn=["retry_on_timeout"], 

248 reason="TimeoutError is included by default.", 

249 version="6.0.0", 

250 ) 

251 @deprecated_args( 

252 args_to_warn=["lib_name", "lib_version"], 

253 reason="Use 'driver_info' parameter instead. " 

254 "lib_name and lib_version will be removed in a future version.", 

255 ) 

256 def __init__( 

257 self, 

258 *, 

259 host: str = "localhost", 

260 port: int = 6379, 

261 db: str | int = 0, 

262 password: str | None = None, 

263 socket_timeout: float | None = DEFAULT_SOCKET_TIMEOUT, 

264 socket_connect_timeout: float | None = DEFAULT_SOCKET_CONNECT_TIMEOUT, 

265 socket_read_size: int = DEFAULT_SOCKET_READ_SIZE, 

266 socket_keepalive: bool | None = True, 

267 socket_keepalive_options: Mapping[int, int | bytes] | object | None = SENTINEL, 

268 connection_pool: ConnectionPool | None = None, 

269 unix_socket_path: str | None = None, 

270 encoding: str = "utf-8", 

271 encoding_errors: str = "strict", 

272 decode_responses: bool = False, 

273 retry_on_timeout: bool = False, 

274 retry: Retry = Retry( 

275 backoff=ExponentialWithJitterBackoff( 

276 base=DEFAULT_RETRY_BASE, cap=DEFAULT_RETRY_CAP 

277 ), 

278 retries=DEFAULT_RETRY_COUNT, 

279 ), 

280 retry_on_error: list | None = None, 

281 ssl: bool = False, 

282 ssl_keyfile: str | None = None, 

283 ssl_certfile: str | None = None, 

284 ssl_cert_reqs: "str | VerifyMode" = "required", 

285 ssl_include_verify_flags: List["VerifyFlags"] | None = None, 

286 ssl_exclude_verify_flags: List["VerifyFlags"] | None = None, 

287 ssl_ca_certs: str | None = None, 

288 ssl_ca_data: str | None = None, 

289 ssl_ca_path: str | None = None, 

290 ssl_check_hostname: bool = True, 

291 ssl_min_version: "TLSVersion | None" = None, 

292 ssl_ciphers: str | None = None, 

293 ssl_password: str | None = None, 

294 max_connections: int | None = None, 

295 single_connection_client: bool = False, 

296 health_check_interval: int = 0, 

297 client_name: str | None = None, 

298 lib_name: str | object | None = SENTINEL, 

299 lib_version: str | object | None = SENTINEL, 

300 driver_info: DriverInfo | object | None = SENTINEL, 

301 username: str | None = None, 

302 auto_close_connection_pool: bool | None = None, 

303 redis_connect_func=None, 

304 credential_provider: CredentialProvider | None = None, 

305 protocol: int | None = None, 

306 legacy_responses: bool = True, 

307 event_dispatcher: EventDispatcher | None = None, 

308 ): 

309 """ 

310 Initialize a new Redis client. 

311 

312 To specify a retry policy for specific errors, you have two options: 

313 

314 1. Set the `retry_on_error` to a list of the error/s to retry on, and 

315 you can also set `retry` to a valid `Retry` object(in case the default 

316 one is not appropriate) - with this approach the retries will be triggered 

317 on the default errors specified in the Retry object enriched with the 

318 errors specified in `retry_on_error`. 

319 

320 2. Define a `Retry` object with configured 'supported_errors' and set 

321 it to the `retry` parameter - with this approach you completely redefine 

322 the errors on which retries will happen. 

323 

324 `retry_on_timeout` is deprecated - please include the TimeoutError 

325 either in the Retry object or in the `retry_on_error` list. 

326 

327 When 'connection_pool' is provided - the retry configuration of the 

328 provided pool will be used. 

329 

330 Args: 

331 

332 socket_keepalive: 

333 if `True`, TCP keepalive is enabled for TCP socket connections. 

334 Argument is ignored when connection_pool is provided. 

335 socket_keepalive_options: 

336 mapping of TCP keepalive socket option constants to values, for 

337 example `{socket.TCP_KEEPIDLE: 30}`. If left unspecified, redis-py 

338 uses TCP keepalive defaults when `socket_keepalive` is enabled: 

339 idle 30 seconds, interval 5 seconds, and 3 probes. Platform-specific 

340 options that are not available are skipped. Pass `None` or `{}` to 

341 avoid setting additional TCP keepalive options. Argument is ignored 

342 when connection_pool is provided. 

343 """ 

344 kwargs: Dict[str, Any] 

345 if event_dispatcher is None: 

346 self._event_dispatcher = EventDispatcher() 

347 else: 

348 self._event_dispatcher = event_dispatcher 

349 # auto_close_connection_pool only has an effect if connection_pool is 

350 # None. It is assumed that if connection_pool is not None, the user 

351 # wants to manage the connection pool themselves. 

352 if auto_close_connection_pool is not None: 

353 warnings.warn( 

354 DeprecationWarning( 

355 '"auto_close_connection_pool" is deprecated ' 

356 "since version 5.0.1. " 

357 "Please create a ConnectionPool explicitly and " 

358 "provide to the Redis() constructor instead." 

359 ) 

360 ) 

361 else: 

362 auto_close_connection_pool = True 

363 

364 if not connection_pool: 

365 # Create internal connection pool, expected to be closed by Redis instance 

366 if not retry_on_error: 

367 retry_on_error = [] 

368 

369 # Handle driver_info: if provided, use it; otherwise create from lib_name/lib_version. 

370 computed_driver_info = resolve_driver_info( 

371 driver_info, lib_name, lib_version 

372 ) 

373 

374 kwargs = { 

375 "db": db, 

376 "username": username, 

377 "password": password, 

378 "credential_provider": credential_provider, 

379 "socket_timeout": socket_timeout, 

380 "socket_read_size": socket_read_size, 

381 "encoding": encoding, 

382 "encoding_errors": encoding_errors, 

383 "decode_responses": decode_responses, 

384 "retry_on_error": retry_on_error, 

385 "retry": copy.deepcopy(retry), 

386 "max_connections": max_connections, 

387 "health_check_interval": health_check_interval, 

388 "client_name": client_name, 

389 "driver_info": computed_driver_info, 

390 "redis_connect_func": redis_connect_func, 

391 "protocol": protocol, 

392 "legacy_responses": legacy_responses, 

393 } 

394 # based on input, setup appropriate connection args 

395 if unix_socket_path is not None: 

396 kwargs.update( 

397 { 

398 "path": unix_socket_path, 

399 "connection_class": UnixDomainSocketConnection, 

400 } 

401 ) 

402 else: 

403 # TCP specific options 

404 kwargs.update( 

405 { 

406 "host": host, 

407 "port": port, 

408 "socket_connect_timeout": socket_connect_timeout, 

409 "socket_keepalive": socket_keepalive, 

410 "socket_keepalive_options": socket_keepalive_options, 

411 } 

412 ) 

413 

414 if ssl: 

415 kwargs.update( 

416 { 

417 "connection_class": SSLConnection, 

418 "ssl_keyfile": ssl_keyfile, 

419 "ssl_certfile": ssl_certfile, 

420 "ssl_cert_reqs": ssl_cert_reqs, 

421 "ssl_include_verify_flags": ssl_include_verify_flags, 

422 "ssl_exclude_verify_flags": ssl_exclude_verify_flags, 

423 "ssl_ca_certs": ssl_ca_certs, 

424 "ssl_ca_data": ssl_ca_data, 

425 "ssl_ca_path": ssl_ca_path, 

426 "ssl_check_hostname": ssl_check_hostname, 

427 "ssl_min_version": ssl_min_version, 

428 "ssl_ciphers": ssl_ciphers, 

429 "ssl_password": ssl_password, 

430 } 

431 ) 

432 # This arg only used if no pool is passed in 

433 self.auto_close_connection_pool = auto_close_connection_pool 

434 connection_pool = ConnectionPool(**kwargs) 

435 self._event_dispatcher.dispatch( 

436 AfterPooledConnectionsInstantiationEvent( 

437 [connection_pool], ClientType.ASYNC, credential_provider 

438 ) 

439 ) 

440 else: 

441 # If a pool is passed in, do not close it 

442 self.auto_close_connection_pool = False 

443 self._event_dispatcher.dispatch( 

444 AfterPooledConnectionsInstantiationEvent( 

445 [connection_pool], ClientType.ASYNC, credential_provider 

446 ) 

447 ) 

448 

449 self.connection_pool = connection_pool 

450 self.single_connection_client = single_connection_client 

451 self.connection: Optional[Connection] = None 

452 

453 connection_kwargs = self.connection_pool.connection_kwargs 

454 self.response_callbacks = CaseInsensitiveDict( 

455 get_response_callbacks( 

456 user_protocol=connection_kwargs.get("protocol"), 

457 legacy_responses=connection_kwargs.get("legacy_responses", True), 

458 ) 

459 ) 

460 

461 # If using a single connection client, we need to lock creation-of and use-of 

462 # the client in order to avoid race conditions such as using asyncio.gather 

463 # on a set of redis commands 

464 self._single_conn_lock = asyncio.Lock() 

465 

466 # When used as an async context manager, we need to increment and decrement 

467 # a usage counter so that we can close the connection pool when no one is 

468 # using the client. 

469 self._usage_counter = 0 

470 self._usage_lock = asyncio.Lock() 

471 

472 def __repr__(self): 

473 return ( 

474 f"<{self.__class__.__module__}.{self.__class__.__name__}" 

475 f"({self.connection_pool!r})>" 

476 ) 

477 

478 def __await__(self): 

479 return self.initialize().__await__() 

480 

481 async def initialize(self: _RedisT) -> _RedisT: 

482 if self.single_connection_client: 

483 async with self._single_conn_lock: 

484 if self.connection is None: 

485 self.connection = await self.connection_pool.get_connection() 

486 

487 self._event_dispatcher.dispatch( 

488 AfterSingleConnectionInstantiationEvent( 

489 self.connection, ClientType.ASYNC, self._single_conn_lock 

490 ) 

491 ) 

492 return self 

493 

494 def set_response_callback(self, command: str, callback: ResponseCallbackT): 

495 """Set a custom Response Callback""" 

496 self.response_callbacks[command] = callback 

497 

498 def get_encoder(self): 

499 """Get the connection pool's encoder""" 

500 return self.connection_pool.get_encoder() 

501 

502 def get_connection_kwargs(self): 

503 """Get the connection's key-word arguments""" 

504 return self.connection_pool.connection_kwargs 

505 

506 def get_retry(self) -> Optional[Retry]: 

507 return self.get_connection_kwargs().get("retry") 

508 

509 def set_retry(self, retry: Retry) -> None: 

510 self.get_connection_kwargs().update({"retry": retry}) 

511 self.connection_pool.set_retry(retry) 

512 

513 def load_external_module(self, funcname, func): 

514 """ 

515 This function can be used to add externally defined redis modules, 

516 and their namespaces to the redis client. 

517 

518 funcname - A string containing the name of the function to create 

519 func - The function, being added to this class. 

520 

521 ex: Assume that one has a custom redis module named foomod that 

522 creates command named 'foo.dothing' and 'foo.anotherthing' in redis. 

523 To load function functions into this namespace: 

524 

525 from redis import Redis 

526 from foomodule import F 

527 r = Redis() 

528 r.load_external_module("foo", F) 

529 r.foo().dothing('your', 'arguments') 

530 

531 For a concrete example see the reimport of the redisjson module in 

532 tests/test_connection.py::test_loading_external_modules 

533 """ 

534 setattr(self, funcname, func) 

535 

536 def pipeline( 

537 self, transaction: bool = True, shard_hint: Optional[str] = None 

538 ) -> "Pipeline": 

539 """ 

540 Return a new pipeline object that can queue multiple commands for 

541 later execution. ``transaction`` indicates whether all commands 

542 should be executed atomically. Apart from making a group of operations 

543 atomic, pipelines are useful for reducing the back-and-forth overhead 

544 between the client and server. 

545 """ 

546 return Pipeline( 

547 self.connection_pool, self.response_callbacks, transaction, shard_hint 

548 ) 

549 

550 async def transaction( 

551 self, 

552 func: Callable[["Pipeline"], Union[Any, Awaitable[Any]]], 

553 *watches: KeyT, 

554 shard_hint: Optional[str] = None, 

555 value_from_callable: bool = False, 

556 watch_delay: Optional[float] = None, 

557 ): 

558 """ 

559 Convenience method for executing the callable `func` as a transaction 

560 while watching all keys specified in `watches`. The 'func' callable 

561 should expect a single argument which is a Pipeline object. 

562 """ 

563 pipe: Pipeline 

564 async with self.pipeline(True, shard_hint) as pipe: 

565 while True: 

566 try: 

567 if watches: 

568 await pipe.watch(*watches) 

569 func_value = func(pipe) 

570 if inspect.isawaitable(func_value): 

571 func_value = await func_value 

572 exec_value = await pipe.execute() 

573 return func_value if value_from_callable else exec_value 

574 except WatchError: 

575 if watch_delay is not None and watch_delay > 0: 

576 await asyncio.sleep(watch_delay) 

577 continue 

578 

579 def lock( 

580 self, 

581 name: KeyT, 

582 timeout: Optional[float] = None, 

583 sleep: float = 0.1, 

584 blocking: bool = True, 

585 blocking_timeout: Optional[float] = None, 

586 lock_class: Optional[Type[Lock]] = None, 

587 thread_local: bool = True, 

588 raise_on_release_error: bool = True, 

589 ) -> Lock: 

590 """ 

591 Return a new Lock object using key ``name`` that mimics 

592 the behavior of threading.Lock. 

593 

594 If specified, ``timeout`` indicates a maximum life for the lock. 

595 By default, it will remain locked until release() is called. 

596 

597 ``sleep`` indicates the amount of time to sleep per loop iteration 

598 when the lock is in blocking mode and another client is currently 

599 holding the lock. 

600 

601 ``blocking`` indicates whether calling ``acquire`` should block until 

602 the lock has been acquired or to fail immediately, causing ``acquire`` 

603 to return False and the lock not being acquired. Defaults to True. 

604 Note this value can be overridden by passing a ``blocking`` 

605 argument to ``acquire``. 

606 

607 ``blocking_timeout`` indicates the maximum amount of time in seconds to 

608 spend trying to acquire the lock. A value of ``None`` indicates 

609 continue trying forever. ``blocking_timeout`` can be specified as a 

610 float or integer, both representing the number of seconds to wait. 

611 

612 ``lock_class`` forces the specified lock implementation. Note that as 

613 of redis-py 3.0, the only lock class we implement is ``Lock`` (which is 

614 a Lua-based lock). So, it's unlikely you'll need this parameter, unless 

615 you have created your own custom lock class. 

616 

617 ``thread_local`` indicates whether the lock token is placed in 

618 thread-local storage. By default, the token is placed in thread local 

619 storage so that a thread only sees its token, not a token set by 

620 another thread. Consider the following timeline: 

621 

622 time: 0, thread-1 acquires `my-lock`, with a timeout of 5 seconds. 

623 thread-1 sets the token to "abc" 

624 time: 1, thread-2 blocks trying to acquire `my-lock` using the 

625 Lock instance. 

626 time: 5, thread-1 has not yet completed. redis expires the lock 

627 key. 

628 time: 5, thread-2 acquired `my-lock` now that it's available. 

629 thread-2 sets the token to "xyz" 

630 time: 6, thread-1 finishes its work and calls release(). if the 

631 token is *not* stored in thread local storage, then 

632 thread-1 would see the token value as "xyz" and would be 

633 able to successfully release the thread-2's lock. 

634 

635 ``raise_on_release_error`` indicates whether to raise an exception when 

636 the lock is no longer owned when exiting the context manager. By default, 

637 this is True, meaning an exception will be raised. If False, the warning 

638 will be logged and the exception will be suppressed. 

639 

640 In some use cases it's necessary to disable thread local storage. For 

641 example, if you have code where one thread acquires a lock and passes 

642 that lock instance to a worker thread to release later. If thread 

643 local storage isn't disabled in this case, the worker thread won't see 

644 the token set by the thread that acquired the lock. Our assumption 

645 is that these cases aren't common and as such default to using 

646 thread local storage.""" 

647 if lock_class is None: 

648 lock_class = Lock 

649 return lock_class( 

650 self, 

651 name, 

652 timeout=timeout, 

653 sleep=sleep, 

654 blocking=blocking, 

655 blocking_timeout=blocking_timeout, 

656 thread_local=thread_local, 

657 raise_on_release_error=raise_on_release_error, 

658 ) 

659 

660 def pubsub(self, **kwargs) -> "PubSub": 

661 """ 

662 Return a Publish/Subscribe object. With this object, you can 

663 subscribe to channels and listen for messages that get published to 

664 them. 

665 """ 

666 return PubSub( 

667 self.connection_pool, event_dispatcher=self._event_dispatcher, **kwargs 

668 ) 

669 

670 def keyspace_notifications( 

671 self, 

672 key_prefix: Union[str, bytes, None] = None, 

673 ignore_subscribe_messages: bool = True, 

674 ) -> "AsyncKeyspaceNotifications": 

675 """ 

676 Return an :class:`~redis.asyncio.keyspace_notifications.AsyncKeyspaceNotifications` 

677 object for subscribing to keyspace and keyevent notifications. 

678 

679 Note: Keyspace notifications must be enabled on the Redis server via 

680 the ``notify-keyspace-events`` configuration option. 

681 

682 Args: 

683 key_prefix: Optional prefix to filter and strip from keys in 

684 notifications. 

685 ignore_subscribe_messages: If True, subscribe/unsubscribe 

686 confirmations are not returned by 

687 get_message/listen. 

688 """ 

689 from redis.asyncio.keyspace_notifications import AsyncKeyspaceNotifications 

690 

691 return AsyncKeyspaceNotifications( 

692 self, 

693 key_prefix=key_prefix, 

694 ignore_subscribe_messages=ignore_subscribe_messages, 

695 ) 

696 

697 def monitor(self) -> "Monitor": 

698 return Monitor(self.connection_pool) 

699 

700 def client(self) -> "Redis": 

701 return self.__class__( 

702 connection_pool=self.connection_pool, single_connection_client=True 

703 ) 

704 

705 async def __aenter__(self: _RedisT) -> _RedisT: 

706 """ 

707 Async context manager entry. Increments a usage counter so that the 

708 connection pool is only closed (via aclose()) when no context is using 

709 the client. 

710 """ 

711 await self._increment_usage() 

712 try: 

713 # Initialize the client (i.e. establish connection, etc.) 

714 return await self.initialize() 

715 except Exception: 

716 # If initialization fails, decrement the counter to keep it in sync 

717 await self._decrement_usage() 

718 raise 

719 

720 async def _increment_usage(self) -> int: 

721 """ 

722 Helper coroutine to increment the usage counter while holding the lock. 

723 Returns the new value of the usage counter. 

724 """ 

725 async with self._usage_lock: 

726 self._usage_counter += 1 

727 return self._usage_counter 

728 

729 async def _decrement_usage(self) -> int: 

730 """ 

731 Helper coroutine to decrement the usage counter while holding the lock. 

732 Returns the new value of the usage counter. 

733 """ 

734 async with self._usage_lock: 

735 self._usage_counter -= 1 

736 return self._usage_counter 

737 

738 async def __aexit__(self, exc_type, exc_value, traceback): 

739 """ 

740 Async context manager exit. Decrements a usage counter. If this is the 

741 last exit (counter becomes zero), the client closes its connection pool. 

742 """ 

743 current_usage = await asyncio.shield(self._decrement_usage()) 

744 if current_usage == 0: 

745 # This was the last active context, so disconnect the pool. 

746 await asyncio.shield(self.aclose()) 

747 

748 _DEL_MESSAGE = "Unclosed Redis client" 

749 

750 # passing _warnings and _grl as argument default since they may be gone 

751 # by the time __del__ is called at shutdown 

752 def __del__( 

753 self, 

754 _warn: Any = warnings.warn, 

755 _grl: Any = asyncio.get_running_loop, 

756 ) -> None: 

757 if hasattr(self, "connection") and (self.connection is not None): 

758 _warn(f"Unclosed client session {self!r}", ResourceWarning, source=self) 

759 try: 

760 context = {"client": self, "message": self._DEL_MESSAGE} 

761 _grl().call_exception_handler(context) 

762 except RuntimeError: 

763 pass 

764 self.connection._close() 

765 

766 async def aclose(self, close_connection_pool: Optional[bool] = None) -> None: 

767 """ 

768 Closes Redis client connection 

769 

770 Args: 

771 close_connection_pool: 

772 decides whether to close the connection pool used by this Redis client, 

773 overriding Redis.auto_close_connection_pool. 

774 By default, let Redis.auto_close_connection_pool decide 

775 whether to close the connection pool. 

776 """ 

777 conn = self.connection 

778 if conn: 

779 self.connection = None 

780 await self.connection_pool.release(conn) 

781 if close_connection_pool or ( 

782 close_connection_pool is None and self.auto_close_connection_pool 

783 ): 

784 await self.connection_pool.disconnect() 

785 

786 @deprecated_function(version="5.0.1", reason="Use aclose() instead", name="close") 

787 async def close(self, close_connection_pool: Optional[bool] = None) -> None: 

788 """ 

789 Alias for aclose(), for backwards compatibility 

790 """ 

791 await self.aclose(close_connection_pool) 

792 

793 async def _send_command_parse_response(self, conn, command_name, *args, **options): 

794 """ 

795 Send a command and parse the response 

796 """ 

797 await conn.send_command(*args) 

798 return await self.parse_response(conn, command_name, **options) 

799 

800 async def _close_connection( 

801 self, 

802 conn: Connection, 

803 error: Optional[BaseException] = None, 

804 failure_count: Optional[int] = None, 

805 start_time: Optional[float] = None, 

806 command_name: Optional[str] = None, 

807 ): 

808 """ 

809 Close the connection before retrying. 

810 

811 The supported exceptions are already checked in the 

812 retry object so we don't need to do it here. 

813 

814 After we disconnect the connection, it will try to reconnect and 

815 do a health check as part of the send_command logic(on connection level). 

816 """ 

817 if ( 

818 error 

819 and failure_count is not None 

820 and failure_count <= conn.retry.get_retries() 

821 ): 

822 await record_operation_duration( 

823 command_name=command_name, 

824 duration_seconds=time.monotonic() - start_time, 

825 server_address=getattr(conn, "host", None), 

826 server_port=getattr(conn, "port", None), 

827 db_namespace=str(conn.db), 

828 error=error, 

829 retry_attempts=failure_count, 

830 ) 

831 

832 await conn.disconnect(error=error, failure_count=failure_count) 

833 

834 # COMMAND EXECUTION AND PROTOCOL PARSING 

835 async def execute_command(self, *args, **options): 

836 """Execute a command and return a parsed response""" 

837 await self.initialize() 

838 pool = self.connection_pool 

839 command_name = args[0] 

840 conn = self.connection or await pool.get_connection() 

841 

842 # Start timing for observability 

843 start_time = time.monotonic() 

844 # Track actual retry attempts for error reporting 

845 actual_retry_attempts = 0 

846 

847 def failure_callback(error, failure_count): 

848 nonlocal actual_retry_attempts 

849 actual_retry_attempts = failure_count 

850 return self._close_connection( 

851 conn, error, failure_count, start_time, command_name 

852 ) 

853 

854 if self.single_connection_client: 

855 await self._single_conn_lock.acquire() 

856 try: 

857 result = await conn.retry.call_with_retry( 

858 lambda: self._send_command_parse_response( 

859 conn, command_name, *args, **options 

860 ), 

861 failure_callback, 

862 with_failure_count=True, 

863 ) 

864 

865 await record_operation_duration( 

866 command_name=command_name, 

867 duration_seconds=time.monotonic() - start_time, 

868 server_address=getattr(conn, "host", None), 

869 server_port=getattr(conn, "port", None), 

870 db_namespace=str(conn.db), 

871 ) 

872 return result 

873 except Exception as e: 

874 await record_error_count( 

875 server_address=getattr(conn, "host", None), 

876 server_port=getattr(conn, "port", None), 

877 network_peer_address=getattr(conn, "host", None), 

878 network_peer_port=getattr(conn, "port", None), 

879 error_type=e, 

880 retry_attempts=actual_retry_attempts, 

881 is_internal=False, 

882 ) 

883 raise 

884 finally: 

885 if self.single_connection_client: 

886 self._single_conn_lock.release() 

887 if not self.connection: 

888 await pool.release(conn) 

889 

890 async def parse_response( 

891 self, connection: Connection, command_name: Union[str, bytes], **options 

892 ): 

893 """Parses a response from the Redis server""" 

894 try: 

895 if NEVER_DECODE in options: 

896 response = await connection.read_response(disable_decoding=True) 

897 options.pop(NEVER_DECODE) 

898 else: 

899 response = await connection.read_response() 

900 except ResponseError: 

901 if EMPTY_RESPONSE in options: 

902 return options[EMPTY_RESPONSE] 

903 raise 

904 

905 if EMPTY_RESPONSE in options: 

906 options.pop(EMPTY_RESPONSE) 

907 

908 # Remove keys entry, it needs only for cache. 

909 options.pop("keys", None) 

910 

911 if command_name in self.response_callbacks: 

912 # Mypy bug: https://github.com/python/mypy/issues/10977 

913 command_name = cast(str, command_name) 

914 retval = self.response_callbacks[command_name](response, **options) 

915 return await retval if inspect.isawaitable(retval) else retval 

916 return response 

917 

918 

919StrictRedis = Redis 

920 

921 

922class MonitorCommandInfo(TypedDict): 

923 time: float 

924 db: int 

925 client_address: str 

926 client_port: str 

927 client_type: str 

928 command: str 

929 

930 

931class Monitor: 

932 """ 

933 Monitor is useful for handling the MONITOR command to the redis server. 

934 next_command() method returns one command from monitor 

935 listen() method yields commands from monitor. 

936 """ 

937 

938 monitor_re = re.compile(r"\[(\d+) (.*?)\] (.*)") 

939 command_re = re.compile(r'"(.*?)(?<!\\)"') 

940 

941 def __init__(self, connection_pool: ConnectionPool): 

942 self.connection_pool = connection_pool 

943 self.connection: Optional[Connection] = None 

944 

945 async def connect(self): 

946 if self.connection is None: 

947 self.connection = await self.connection_pool.get_connection() 

948 

949 async def __aenter__(self): 

950 await self.connect() 

951 await self.connection.send_command("MONITOR") 

952 # check that monitor returns 'OK', but don't return it to user 

953 response = await self.connection.read_response() 

954 if not bool_ok(response): 

955 raise RedisError(f"MONITOR failed: {response}") 

956 return self 

957 

958 async def __aexit__(self, *args): 

959 await self.connection.disconnect() 

960 await self.connection_pool.release(self.connection) 

961 

962 async def next_command(self) -> MonitorCommandInfo: 

963 """Parse the response from a monitor command""" 

964 await self.connect() 

965 response = await self.connection.read_response() 

966 if isinstance(response, bytes): 

967 response = self.connection.encoder.decode(response, force=True) 

968 command_time, command_data = response.split(" ", 1) 

969 m = self.monitor_re.match(command_data) 

970 db_id, client_info, command = m.groups() 

971 command = " ".join(self.command_re.findall(command)) 

972 # Redis escapes double quotes because each piece of the command 

973 # string is surrounded by double quotes. We don't have that 

974 # requirement so remove the escaping and leave the quote. 

975 command = command.replace('\\"', '"') 

976 

977 if client_info == "lua": 

978 client_address = "lua" 

979 client_port = "" 

980 client_type = "lua" 

981 elif client_info.startswith("unix"): 

982 client_address = "unix" 

983 client_port = client_info[5:] 

984 client_type = "unix" 

985 else: 

986 # use rsplit as ipv6 addresses contain colons 

987 client_address, client_port = client_info.rsplit(":", 1) 

988 client_type = "tcp" 

989 return { 

990 "time": float(command_time), 

991 "db": int(db_id), 

992 "client_address": client_address, 

993 "client_port": client_port, 

994 "client_type": client_type, 

995 "command": command, 

996 } 

997 

998 async def listen(self) -> AsyncIterator[MonitorCommandInfo]: 

999 """Listen for commands coming to the server.""" 

1000 while True: 

1001 yield await self.next_command() 

1002 

1003 

1004class PubSub: 

1005 """ 

1006 PubSub provides publish, subscribe and listen support to Redis channels. 

1007 

1008 After subscribing to one or more channels, the listen() method will block 

1009 until a message arrives on one of the subscribed channels. That message 

1010 will be returned and it's safe to start listening again. 

1011 """ 

1012 

1013 PUBLISH_MESSAGE_TYPES = ("message", "pmessage", "smessage") 

1014 UNSUBSCRIBE_MESSAGE_TYPES = ("unsubscribe", "punsubscribe", "sunsubscribe") 

1015 HEALTH_CHECK_MESSAGE = "redis-py-health-check" 

1016 

1017 def __init__( 

1018 self, 

1019 connection_pool: ConnectionPool, 

1020 shard_hint: Optional[str] = None, 

1021 ignore_subscribe_messages: bool = False, 

1022 encoder=None, 

1023 push_handler_func: Optional[Callable] = None, 

1024 event_dispatcher: Optional["EventDispatcher"] = None, 

1025 ): 

1026 if event_dispatcher is None: 

1027 self._event_dispatcher = EventDispatcher() 

1028 else: 

1029 self._event_dispatcher = event_dispatcher 

1030 self.connection_pool = connection_pool 

1031 self.shard_hint = shard_hint 

1032 self.ignore_subscribe_messages = ignore_subscribe_messages 

1033 self.connection = None 

1034 # we need to know the encoding options for this connection in order 

1035 # to lookup channel and pattern names for callback handlers. 

1036 self.encoder = encoder 

1037 self.push_handler_func = push_handler_func 

1038 if self.encoder is None: 

1039 self.encoder = self.connection_pool.get_encoder() 

1040 if self.encoder.decode_responses: 

1041 self.health_check_response = [ 

1042 ["pong", self.HEALTH_CHECK_MESSAGE], 

1043 self.HEALTH_CHECK_MESSAGE, 

1044 ] 

1045 else: 

1046 self.health_check_response = [ 

1047 [b"pong", self.encoder.encode(self.HEALTH_CHECK_MESSAGE)], 

1048 self.encoder.encode(self.HEALTH_CHECK_MESSAGE), 

1049 ] 

1050 if self.push_handler_func is None: 

1051 _set_info_logger() 

1052 self.channels = {} 

1053 self.pending_unsubscribe_channels = set() 

1054 self.patterns = {} 

1055 self.pending_unsubscribe_patterns = set() 

1056 self.shard_channels = {} 

1057 self.pending_unsubscribe_shard_channels = set() 

1058 self._lock = asyncio.Lock() 

1059 

1060 async def __aenter__(self): 

1061 return self 

1062 

1063 async def __aexit__(self, exc_type, exc_value, traceback): 

1064 await self.aclose() 

1065 

1066 def __del__(self): 

1067 if self.connection: 

1068 self.connection.deregister_connect_callback(self.on_connect) 

1069 

1070 async def aclose(self): 

1071 # In case a connection property does not yet exist 

1072 # (due to a crash earlier in the Redis() constructor), return 

1073 # immediately as there is nothing to clean-up. 

1074 if not hasattr(self, "connection"): 

1075 return 

1076 async with self._lock: 

1077 if self.connection: 

1078 # Use nowait=True to avoid awaiting StreamWriter.wait_closed(), 

1079 # which can deadlock when a concurrent reader task (e.g. one 

1080 # running pubsub.run() or get_message(block=True)) still holds 

1081 # the transport. See https://github.com/redis/redis-py/issues/3941 

1082 await self.connection.disconnect(nowait=True) 

1083 self.connection.deregister_connect_callback(self.on_connect) 

1084 await self.connection_pool.release(self.connection) 

1085 self.connection = None 

1086 self.channels = {} 

1087 self.pending_unsubscribe_channels = set() 

1088 self.patterns = {} 

1089 self.pending_unsubscribe_patterns = set() 

1090 self.shard_channels = {} 

1091 self.pending_unsubscribe_shard_channels = set() 

1092 

1093 @deprecated_function(version="5.0.1", reason="Use aclose() instead", name="close") 

1094 async def close(self) -> None: 

1095 """Alias for aclose(), for backwards compatibility""" 

1096 await self.aclose() 

1097 

1098 @deprecated_function(version="5.0.1", reason="Use aclose() instead", name="reset") 

1099 async def reset(self) -> None: 

1100 """Alias for aclose(), for backwards compatibility""" 

1101 await self.aclose() 

1102 

1103 async def _resubscribe(self, subscribed, subscribe_fn) -> None: 

1104 # Replay handler-backed subscriptions as positional Subscription objects 

1105 # so binary names never need to be decoded into keyword argument keys. 

1106 subscriptions = pubsub_subscription_args(subscribed) 

1107 if subscriptions: 

1108 await subscribe_fn(*subscriptions) 

1109 

1110 async def _resubscribe_shard_channels(self) -> None: 

1111 await self._resubscribe(self.shard_channels, self.ssubscribe) 

1112 

1113 async def on_connect(self, connection: Connection): 

1114 """Re-subscribe to any channels and patterns previously subscribed to""" 

1115 self.pending_unsubscribe_channels.clear() 

1116 self.pending_unsubscribe_patterns.clear() 

1117 self.pending_unsubscribe_shard_channels.clear() 

1118 if self.channels: 

1119 await self._resubscribe(self.channels, self.subscribe) 

1120 if self.patterns: 

1121 await self._resubscribe(self.patterns, self.psubscribe) 

1122 if self.shard_channels: 

1123 await self._resubscribe_shard_channels() 

1124 

1125 @property 

1126 def subscribed(self): 

1127 """Indicates if there are subscriptions to any channels or patterns""" 

1128 return bool(self.channels or self.patterns or self.shard_channels) 

1129 

1130 async def execute_command(self, *args: EncodableT): 

1131 """Execute a publish/subscribe command""" 

1132 

1133 # NOTE: don't parse the response in this function -- it could pull a 

1134 # legitimate message off the stack if the connection is already 

1135 # subscribed to one or more channels 

1136 

1137 await self.connect() 

1138 connection = self.connection 

1139 kwargs = {"check_health": not self.subscribed} 

1140 await self._execute(connection, connection.send_command, *args, **kwargs) 

1141 

1142 async def connect(self): 

1143 """ 

1144 Ensure that the PubSub is connected 

1145 """ 

1146 if self.connection is None: 

1147 self.connection = await self.connection_pool.get_connection() 

1148 # register a callback that re-subscribes to any channels we 

1149 # were listening to when we were disconnected 

1150 self.connection.register_connect_callback(self.on_connect) 

1151 else: 

1152 await self.connection.connect() 

1153 if self.push_handler_func is not None: 

1154 self.connection._parser.set_pubsub_push_handler(self.push_handler_func) 

1155 

1156 self._event_dispatcher.dispatch( 

1157 AfterPubSubConnectionInstantiationEvent( 

1158 self.connection, self.connection_pool, ClientType.ASYNC, self._lock 

1159 ) 

1160 ) 

1161 

1162 async def _reconnect( 

1163 self, 

1164 conn, 

1165 error: Optional[BaseException] = None, 

1166 failure_count: Optional[int] = None, 

1167 start_time: Optional[float] = None, 

1168 command_name: Optional[str] = None, 

1169 ): 

1170 """ 

1171 The supported exceptions are already checked in the 

1172 retry object so we don't need to do it here. 

1173 

1174 In this error handler we are trying to reconnect to the server. 

1175 """ 

1176 if ( 

1177 error 

1178 and failure_count is not None 

1179 and failure_count <= conn.retry.get_retries() 

1180 ): 

1181 if command_name: 

1182 await record_operation_duration( 

1183 command_name=command_name, 

1184 duration_seconds=time.monotonic() - start_time, 

1185 server_address=getattr(conn, "host", None), 

1186 server_port=getattr(conn, "port", None), 

1187 db_namespace=str(conn.db), 

1188 error=error, 

1189 retry_attempts=failure_count, 

1190 ) 

1191 await conn.disconnect(error=error, failure_count=failure_count) 

1192 await conn.connect() 

1193 

1194 async def _execute(self, conn, command, *args, **kwargs): 

1195 """ 

1196 Connect manually upon disconnection. If the Redis server is down, 

1197 this will fail and raise a ConnectionError as desired. 

1198 After reconnection, the ``on_connect`` callback should have been 

1199 called by the # connection to resubscribe us to any channels and 

1200 patterns we were previously listening to 

1201 """ 

1202 if not len(args) == 0: 

1203 command_name = args[0] 

1204 else: 

1205 command_name = None 

1206 

1207 # Start timing for observability 

1208 start_time = time.monotonic() 

1209 # Track actual retry attempts for error reporting 

1210 actual_retry_attempts = 0 

1211 

1212 def failure_callback(error, failure_count): 

1213 nonlocal actual_retry_attempts 

1214 actual_retry_attempts = failure_count 

1215 return self._reconnect(conn, error, failure_count, start_time, command_name) 

1216 

1217 try: 

1218 response = await conn.retry.call_with_retry( 

1219 lambda: command(*args, **kwargs), 

1220 failure_callback, 

1221 with_failure_count=True, 

1222 ) 

1223 

1224 if command_name: 

1225 await record_operation_duration( 

1226 command_name=command_name, 

1227 duration_seconds=time.monotonic() - start_time, 

1228 server_address=getattr(conn, "host", None), 

1229 server_port=getattr(conn, "port", None), 

1230 db_namespace=str(conn.db), 

1231 ) 

1232 

1233 return response 

1234 except Exception as e: 

1235 await record_error_count( 

1236 server_address=getattr(conn, "host", None), 

1237 server_port=getattr(conn, "port", None), 

1238 network_peer_address=getattr(conn, "host", None), 

1239 network_peer_port=getattr(conn, "port", None), 

1240 error_type=e, 

1241 retry_attempts=actual_retry_attempts, 

1242 is_internal=False, 

1243 ) 

1244 raise 

1245 

1246 async def parse_response(self, block: bool = True, timeout: float = 0): 

1247 """ 

1248 Parse the response from a publish/subscribe command. 

1249 

1250 Args: 

1251 block: If True, block indefinitely until a message is available. 

1252 If False, return immediately if no message is available. 

1253 Default: True 

1254 timeout: The timeout in seconds for reading a response when block=False. 

1255 This parameter is ignored when block=True. 

1256 Default: 0 (return immediately if no data available) 

1257 

1258 Returns: 

1259 The parsed response from the server, or None if no message is available 

1260 within the timeout period (when block=False). 

1261 

1262 Important: 

1263 The block and timeout parameters work together: 

1264 - When block=True: timeout is IGNORED, method blocks indefinitely 

1265 - When block=False: timeout is USED, method returns after timeout expires 

1266 

1267 Typically, you should use get_message(timeout=X) instead of calling 

1268 parse_response() directly. The get_message() method automatically sets 

1269 block=False when a timeout is provided, and block=True when timeout=None. 

1270 

1271 Example: 

1272 # Block indefinitely (timeout is ignored) 

1273 response = await pubsub.parse_response(block=True, timeout=0.1) 

1274 

1275 # Non-blocking with 0.1 second timeout 

1276 response = await pubsub.parse_response(block=False, timeout=0.1) 

1277 

1278 # Non-blocking, return immediately 

1279 response = await pubsub.parse_response(block=False, timeout=0) 

1280 

1281 # Recommended: use get_message() instead 

1282 msg = await pubsub.get_message(timeout=0.1) # automatically sets block=False 

1283 msg = await pubsub.get_message(timeout=None) # automatically sets block=True 

1284 """ 

1285 conn = self.connection 

1286 if conn is None: 

1287 raise RuntimeError( 

1288 "pubsub connection not set: " 

1289 "did you forget to call subscribe() or psubscribe()?" 

1290 ) 

1291 

1292 await self.check_health() 

1293 

1294 if not conn.is_connected: 

1295 await conn.connect() 

1296 

1297 # Block=True: signal "no timeout" to conn.read_response via 

1298 # math.inf. The connection treats math.inf as the per-read 

1299 # opt-in for blocking indefinitely without falling back to 

1300 # self.socket_timeout. Reconnect/AUTH/HELLO/resubscribe 

1301 # operations performed by the retry layer continue to honor 

1302 # self.socket_timeout because they do not pass math.inf. 

1303 # 

1304 # TODO(next-major): when the async Connection.read_response 

1305 # default for ``timeout`` is changed to SENTINEL, passing 

1306 # ``timeout=None`` from this method will become the natural 

1307 # "no timeout" signal and the math.inf hand-off can be 

1308 # removed. That swap is a breaking change to the 

1309 # Connection.read_response signature so it must wait for a 

1310 # major release. 

1311 read_timeout = math.inf if block else timeout 

1312 response = await self._execute( 

1313 conn, 

1314 conn.read_response, 

1315 timeout=read_timeout, 

1316 disconnect_on_error=False, 

1317 push_request=True, 

1318 ) 

1319 

1320 if conn.health_check_interval and response in self.health_check_response: 

1321 # ignore the health check message as user might not expect it 

1322 return None 

1323 return response 

1324 

1325 async def check_health(self): 

1326 conn = self.connection 

1327 if conn is None: 

1328 raise RuntimeError( 

1329 "pubsub connection not set: " 

1330 "did you forget to call subscribe() or psubscribe()?" 

1331 ) 

1332 

1333 if ( 

1334 conn.health_check_interval 

1335 and asyncio.get_running_loop().time() > conn.next_health_check 

1336 ): 

1337 await conn.send_command( 

1338 "PING", self.HEALTH_CHECK_MESSAGE, check_health=False 

1339 ) 

1340 

1341 def _normalize_keys(self, data: _NormalizeKeysT) -> _NormalizeKeysT: 

1342 """ 

1343 normalize channel/pattern names to be either bytes or strings 

1344 based on whether responses are automatically decoded. this saves us 

1345 from coercing the value for each message coming in. 

1346 """ 

1347 encode = self.encoder.encode 

1348 decode = self.encoder.decode 

1349 return {decode(encode(k)): v for k, v in data.items()} # type: ignore[return-value] # noqa: E501 

1350 

1351 async def psubscribe( 

1352 self, *args: ChannelT | Subscription, **kwargs: PubSubHandler 

1353 ) -> None: 

1354 """ 

1355 Subscribe to channel patterns. 

1356 Patterns supplied as keyword arguments expect a pattern name as the 

1357 key and a callable as the value. 

1358 ``Subscription`` objects can also be supplied positionally with an 

1359 optional handler. 

1360 A pattern's callable will be invoked automatically 

1361 when a message is received on that pattern rather than producing a 

1362 message via ``listen()``. 

1363 """ 

1364 new_patterns = parse_pubsub_subscriptions(args, kwargs) 

1365 ret_val = await self.execute_command("PSUBSCRIBE", *new_patterns.keys()) 

1366 # update the patterns dict AFTER we send the command. we don't want to 

1367 # subscribe twice to these patterns, once for the command and again 

1368 # for the reconnection. 

1369 new_patterns = self._normalize_keys(new_patterns) 

1370 self.patterns.update(new_patterns) 

1371 self.pending_unsubscribe_patterns.difference_update(new_patterns) 

1372 return ret_val 

1373 

1374 def punsubscribe(self, *args: ChannelT) -> Awaitable: 

1375 """ 

1376 Unsubscribe from the supplied patterns. If empty, unsubscribe from 

1377 all patterns. 

1378 """ 

1379 patterns: Iterable[ChannelT] 

1380 if args: 

1381 parsed_args = list_or_args((args[0],), args[1:]) 

1382 patterns = self._normalize_keys(dict.fromkeys(parsed_args)).keys() 

1383 else: 

1384 parsed_args = [] 

1385 patterns = self.patterns 

1386 self.pending_unsubscribe_patterns.update(patterns) 

1387 return self.execute_command("PUNSUBSCRIBE", *parsed_args) 

1388 

1389 async def subscribe( 

1390 self, *args: ChannelT | Subscription, **kwargs: PubSubHandler 

1391 ) -> None: 

1392 """ 

1393 Subscribe to channels. 

1394 Channels supplied as keyword arguments expect 

1395 a channel name as the key and a callable as the value. 

1396 ``Subscription`` objects can also be supplied positionally with an 

1397 optional handler. 

1398 A channel's callable will be invoked automatically 

1399 when a message is received on that channel rather than producing a 

1400 message via ``listen()`` or ``get_message()``. 

1401 """ 

1402 new_channels = parse_pubsub_subscriptions(args, kwargs) 

1403 ret_val = await self.execute_command("SUBSCRIBE", *new_channels.keys()) 

1404 # update the channels dict AFTER we send the command. we don't want to 

1405 # subscribe twice to these channels, once for the command and again 

1406 # for the reconnection. 

1407 new_channels = self._normalize_keys(new_channels) 

1408 self.channels.update(new_channels) 

1409 self.pending_unsubscribe_channels.difference_update(new_channels) 

1410 return ret_val 

1411 

1412 def unsubscribe(self, *args) -> Awaitable: 

1413 """ 

1414 Unsubscribe from the supplied channels. If empty, unsubscribe from 

1415 all channels 

1416 """ 

1417 if args: 

1418 parsed_args = list_or_args(args[0], args[1:]) 

1419 channels = self._normalize_keys(dict.fromkeys(parsed_args)) 

1420 else: 

1421 parsed_args = [] 

1422 channels = self.channels 

1423 self.pending_unsubscribe_channels.update(channels) 

1424 return self.execute_command("UNSUBSCRIBE", *parsed_args) 

1425 

1426 async def ssubscribe( 

1427 self, 

1428 *args: ChannelT | Subscription, 

1429 target_node: Any = None, 

1430 **kwargs: PubSubHandler, 

1431 ) -> None: 

1432 """ 

1433 Subscribes the client to the specified shard channels. 

1434 Channels supplied as keyword arguments expect a channel name as the key 

1435 and a callable as the value. 

1436 ``Subscription`` objects can also be supplied positionally 

1437 with an optional handler. 

1438 A channel's callable will be invoked automatically when a message 

1439 is received on that channel rather than producing a message 

1440 via ``listen()`` or ``get_sharded_message()``. 

1441 """ 

1442 new_s_channels = parse_pubsub_subscriptions(args, kwargs) 

1443 ret_val = await self.execute_command("SSUBSCRIBE", *new_s_channels.keys()) 

1444 # update the s_channels dict AFTER we send the command. we don't want to 

1445 # subscribe twice to these channels, once for the command and again 

1446 # for the reconnection. 

1447 new_s_channels = self._normalize_keys(new_s_channels) 

1448 self.shard_channels.update(new_s_channels) 

1449 self.pending_unsubscribe_shard_channels.difference_update(new_s_channels) 

1450 return ret_val 

1451 

1452 def sunsubscribe(self, *args, target_node=None) -> Awaitable: 

1453 """ 

1454 Unsubscribe from the supplied shard_channels. If empty, unsubscribe from 

1455 all shard_channels 

1456 """ 

1457 if args: 

1458 args = list_or_args(args[0], args[1:]) 

1459 s_channels = self._normalize_keys(dict.fromkeys(args)) 

1460 else: 

1461 s_channels = self.shard_channels 

1462 self.pending_unsubscribe_shard_channels.update(s_channels) 

1463 return self.execute_command("SUNSUBSCRIBE", *args) 

1464 

1465 async def listen(self) -> AsyncIterator: 

1466 """Listen for messages on channels this client has been subscribed to""" 

1467 while self.subscribed: 

1468 response = await self.handle_message(await self.parse_response(block=True)) 

1469 if response is not None: 

1470 yield response 

1471 

1472 async def get_message( 

1473 self, ignore_subscribe_messages: bool = False, timeout: Optional[float] = 0.0 

1474 ): 

1475 """ 

1476 Get the next message if one is available, otherwise None. 

1477 

1478 If timeout is specified, the system will wait for `timeout` seconds 

1479 before returning. Timeout should be specified as a floating point 

1480 number or None to wait indefinitely. 

1481 """ 

1482 response = await self.parse_response(block=(timeout is None), timeout=timeout) 

1483 if response: 

1484 return await self.handle_message(response, ignore_subscribe_messages) 

1485 return None 

1486 

1487 def ping(self, message=None) -> Awaitable[bool]: 

1488 """ 

1489 Ping the Redis server to test connectivity. 

1490 

1491 Sends a PING command to the Redis server and returns True if the server 

1492 responds with "PONG". 

1493 """ 

1494 args = ["PING", message] if message is not None else ["PING"] 

1495 return self.execute_command(*args) 

1496 

1497 async def handle_message(self, response, ignore_subscribe_messages=False): 

1498 """ 

1499 Parses a pub/sub message. If the channel or pattern was subscribed to 

1500 with a message handler, the handler is invoked instead of a parsed 

1501 message being returned. 

1502 """ 

1503 if response is None: 

1504 return None 

1505 if isinstance(response, bytes): 

1506 response = [b"pong", response] if response != b"PONG" else [b"pong", b""] 

1507 message_type = str_if_bytes(response[0]) 

1508 if message_type == "pmessage": 

1509 message = { 

1510 "type": message_type, 

1511 "pattern": response[1], 

1512 "channel": response[2], 

1513 "data": response[3], 

1514 } 

1515 elif message_type == "pong": 

1516 message = { 

1517 "type": message_type, 

1518 "pattern": None, 

1519 "channel": None, 

1520 "data": response[1], 

1521 } 

1522 else: 

1523 message = { 

1524 "type": message_type, 

1525 "pattern": None, 

1526 "channel": response[1], 

1527 "data": response[2], 

1528 } 

1529 

1530 if message_type in ["message", "pmessage"]: 

1531 channel = str_if_bytes(message["channel"]) 

1532 await record_pubsub_message( 

1533 direction=PubSubDirection.RECEIVE, 

1534 channel=channel, 

1535 ) 

1536 elif message_type == "smessage": 

1537 channel = str_if_bytes(message["channel"]) 

1538 await record_pubsub_message( 

1539 direction=PubSubDirection.RECEIVE, 

1540 channel=channel, 

1541 sharded=True, 

1542 ) 

1543 

1544 # if this is an unsubscribe message, remove it from memory 

1545 if message_type in self.UNSUBSCRIBE_MESSAGE_TYPES: 

1546 if message_type == "punsubscribe": 

1547 pattern = response[1] 

1548 if pattern in self.pending_unsubscribe_patterns: 

1549 self.pending_unsubscribe_patterns.remove(pattern) 

1550 self.patterns.pop(pattern, None) 

1551 elif message_type == "sunsubscribe": 

1552 s_channel = response[1] 

1553 if s_channel in self.pending_unsubscribe_shard_channels: 

1554 self.pending_unsubscribe_shard_channels.remove(s_channel) 

1555 self.shard_channels.pop(s_channel, None) 

1556 else: 

1557 channel = response[1] 

1558 if channel in self.pending_unsubscribe_channels: 

1559 self.pending_unsubscribe_channels.remove(channel) 

1560 self.channels.pop(channel, None) 

1561 

1562 if message_type in self.PUBLISH_MESSAGE_TYPES: 

1563 # if there's a message handler, invoke it 

1564 if message_type == "pmessage": 

1565 handler = self.patterns.get(message["pattern"], None) 

1566 elif message_type == "smessage": 

1567 handler = self.shard_channels.get(message["channel"], None) 

1568 else: 

1569 handler = self.channels.get(message["channel"], None) 

1570 if handler: 

1571 if inspect.iscoroutinefunction(handler): 

1572 await handler(message) 

1573 else: 

1574 handler(message) 

1575 return None 

1576 elif message_type != "pong": 

1577 # this is a subscribe/unsubscribe message. ignore if we don't 

1578 # want them 

1579 if ignore_subscribe_messages or self.ignore_subscribe_messages: 

1580 return None 

1581 

1582 return message 

1583 

1584 async def run( 

1585 self, 

1586 *, 

1587 exception_handler: Optional["PSWorkerThreadExcHandlerT"] = None, 

1588 poll_timeout: float = 1.0, 

1589 pubsub=None, 

1590 ) -> None: 

1591 """Process pub/sub messages using registered callbacks. 

1592 

1593 This is the equivalent of :py:meth:`redis.PubSub.run_in_thread` in 

1594 redis-py, but it is a coroutine. To launch it as a separate task, use 

1595 ``asyncio.create_task``: 

1596 

1597 >>> task = asyncio.create_task(pubsub.run()) 

1598 

1599 To shut it down, use asyncio cancellation: 

1600 

1601 >>> task.cancel() 

1602 >>> await task 

1603 """ 

1604 for channel, handler in self.channels.items(): 

1605 if handler is None: 

1606 raise PubSubError(f"Channel: '{channel}' has no handler registered") 

1607 for pattern, handler in self.patterns.items(): 

1608 if handler is None: 

1609 raise PubSubError(f"Pattern: '{pattern}' has no handler registered") 

1610 

1611 await self.connect() 

1612 while True: 

1613 try: 

1614 if pubsub is None: 

1615 await self.get_message( 

1616 ignore_subscribe_messages=True, timeout=poll_timeout 

1617 ) 

1618 else: 

1619 await pubsub.get_message( 

1620 ignore_subscribe_messages=True, timeout=poll_timeout 

1621 ) 

1622 except asyncio.CancelledError: 

1623 raise 

1624 except BaseException as e: 

1625 if exception_handler is None: 

1626 raise 

1627 res = exception_handler(e, self) 

1628 if inspect.isawaitable(res): 

1629 await res 

1630 # Ensure that other tasks on the event loop get a chance to run 

1631 # if we didn't have to block for I/O anywhere. 

1632 await asyncio.sleep(0) 

1633 

1634 

1635class PubsubWorkerExceptionHandler(Protocol): 

1636 def __call__(self, e: BaseException, pubsub: PubSub): ... 

1637 

1638 

1639class AsyncPubsubWorkerExceptionHandler(Protocol): 

1640 async def __call__(self, e: BaseException, pubsub: PubSub): ... 

1641 

1642 

1643PSWorkerThreadExcHandlerT = Union[ 

1644 PubsubWorkerExceptionHandler, AsyncPubsubWorkerExceptionHandler 

1645] 

1646 

1647 

1648CommandT = Tuple[Tuple[Union[str, bytes], ...], Mapping[str, Any]] 

1649CommandStackT = List[CommandT] 

1650 

1651 

1652class Pipeline(Redis): # lgtm [py/init-calls-subclass] 

1653 """ 

1654 Pipelines provide a way to transmit multiple commands to the Redis server 

1655 in one transmission. This is convenient for batch processing, such as 

1656 saving all the values in a list to Redis. 

1657 

1658 All commands executed within a pipeline(when running in transactional mode, 

1659 which is the default behavior) are wrapped with MULTI and EXEC 

1660 calls. This guarantees all commands executed in the pipeline will be 

1661 executed atomically. 

1662 

1663 Any command raising an exception does *not* halt the execution of 

1664 subsequent commands in the pipeline. Instead, the exception is caught 

1665 and its instance is placed into the response list returned by execute(). 

1666 Code iterating over the response list should be able to deal with an 

1667 instance of an exception as a potential value. In general, these will be 

1668 ResponseError exceptions, such as those raised when issuing a command 

1669 on a key of a different datatype. 

1670 """ 

1671 

1672 UNWATCH_COMMANDS = {"DISCARD", "EXEC", "UNWATCH"} 

1673 

1674 def __init__( 

1675 self, 

1676 connection_pool: ConnectionPool, 

1677 response_callbacks: MutableMapping[Union[str, bytes], ResponseCallbackT], 

1678 transaction: bool, 

1679 shard_hint: Optional[str], 

1680 ): 

1681 self.connection_pool = connection_pool 

1682 self.connection = None 

1683 self.response_callbacks = response_callbacks 

1684 self.is_transaction = transaction 

1685 self.shard_hint = shard_hint 

1686 self.watching = False 

1687 self.command_stack: CommandStackT = [] 

1688 self.scripts: Set[Script] = set() 

1689 self.explicit_transaction = False 

1690 

1691 async def __aenter__(self: _RedisT) -> _RedisT: 

1692 return self 

1693 

1694 async def __aexit__(self, exc_type, exc_value, traceback): 

1695 await self.reset() 

1696 

1697 def __await__(self): 

1698 return self._async_self().__await__() 

1699 

1700 _DEL_MESSAGE = "Unclosed Pipeline client" 

1701 

1702 def __len__(self): 

1703 return len(self.command_stack) 

1704 

1705 def __bool__(self): 

1706 """Pipeline instances should always evaluate to True""" 

1707 return True 

1708 

1709 async def _async_self(self): 

1710 return self 

1711 

1712 async def reset(self): 

1713 self.command_stack = [] 

1714 self.scripts = set() 

1715 try: 

1716 # make sure to reset the connection state in the event that we were 

1717 # watching something 

1718 if self.watching and self.connection: 

1719 try: 

1720 # call this manually since our unwatch or 

1721 # immediate_execute_command methods can call reset() 

1722 await self.connection.send_command("UNWATCH") 

1723 await self.connection.read_response() 

1724 except ConnectionError: 

1725 # disconnect will also remove any previous WATCHes 

1726 if self.connection: 

1727 await self.connection.disconnect() 

1728 except asyncio.CancelledError: 

1729 # Disconnect so any unread UNWATCH reply does not get 

1730 # served to the next caller that takes the connection. 

1731 if self.connection: 

1732 await self.connection.disconnect() 

1733 raise 

1734 finally: 

1735 self.watching = False 

1736 self.explicit_transaction = False 

1737 # We can safely return the connection to the pool here since we're 

1738 # sure we're no longer WATCHing anything. Detach self.connection 

1739 # before awaiting release: if a second cancel aborts the await, 

1740 # the pipeline must not be left holding a reference to a 

1741 # connection that is being returned to the pool. Shield the 

1742 # release itself so a second cancel cannot split the pool's 

1743 # internal in-use/available bookkeeping mid-update. 

1744 if self.connection: 

1745 connection, self.connection = self.connection, None 

1746 await asyncio.shield(self.connection_pool.release(connection)) 

1747 

1748 async def aclose(self) -> None: 

1749 """Alias for reset(), a standard method name for cleanup""" 

1750 await self.reset() 

1751 

1752 def multi(self): 

1753 """ 

1754 Start a transactional block of the pipeline after WATCH commands 

1755 are issued. End the transactional block with `execute`. 

1756 """ 

1757 if self.explicit_transaction: 

1758 raise RedisError("Cannot issue nested calls to MULTI") 

1759 if self.command_stack: 

1760 raise RedisError( 

1761 "Commands without an initial WATCH have already been issued" 

1762 ) 

1763 self.explicit_transaction = True 

1764 

1765 def execute_command( 

1766 self, *args, **kwargs 

1767 ) -> Union["Pipeline", Awaitable["Pipeline"]]: 

1768 if (self.watching or args[0] == "WATCH") and not self.explicit_transaction: 

1769 return self.immediate_execute_command(*args, **kwargs) 

1770 return self.pipeline_execute_command(*args, **kwargs) 

1771 

1772 async def _disconnect_reset_raise_on_watching( 

1773 self, 

1774 conn: Connection, 

1775 error: Exception, 

1776 failure_count: Optional[int] = None, 

1777 start_time: Optional[float] = None, 

1778 command_name: Optional[str] = None, 

1779 ) -> None: 

1780 """ 

1781 Close the connection reset watching state and 

1782 raise an exception if we were watching. 

1783 

1784 The supported exceptions are already checked in the 

1785 retry object so we don't need to do it here. 

1786 

1787 After we disconnect the connection, it will try to reconnect and 

1788 do a health check as part of the send_command logic(on connection level). 

1789 """ 

1790 if ( 

1791 error 

1792 and failure_count is not None 

1793 and failure_count <= conn.retry.get_retries() 

1794 ): 

1795 await record_operation_duration( 

1796 command_name=command_name, 

1797 duration_seconds=time.monotonic() - start_time, 

1798 server_address=getattr(conn, "host", None), 

1799 server_port=getattr(conn, "port", None), 

1800 db_namespace=str(conn.db), 

1801 error=error, 

1802 retry_attempts=failure_count, 

1803 ) 

1804 await conn.disconnect(error=error, failure_count=failure_count) 

1805 # if we were already watching a variable, the watch is no longer 

1806 # valid since this connection has died. raise a WatchError, which 

1807 # indicates the user should retry this transaction. 

1808 if self.watching: 

1809 await self.reset() 

1810 raise WatchError( 

1811 f"A {type(error).__name__} occurred while watching one or more keys" 

1812 ) 

1813 

1814 async def immediate_execute_command(self, *args, **options): 

1815 """ 

1816 Execute a command immediately, but don't auto-retry on the supported 

1817 errors for retry if we're already WATCHing a variable. 

1818 Used when issuing WATCH or subsequent commands retrieving their values but before 

1819 MULTI is called. 

1820 """ 

1821 command_name = args[0] 

1822 conn = self.connection 

1823 # if this is the first call, we need a connection 

1824 if not conn: 

1825 conn = await self.connection_pool.get_connection() 

1826 self.connection = conn 

1827 

1828 # Start timing for observability 

1829 start_time = time.monotonic() 

1830 # Track actual retry attempts for error reporting 

1831 actual_retry_attempts = 0 

1832 

1833 def failure_callback(error, failure_count): 

1834 nonlocal actual_retry_attempts 

1835 actual_retry_attempts = failure_count 

1836 return self._disconnect_reset_raise_on_watching( 

1837 conn, error, failure_count, start_time, command_name 

1838 ) 

1839 

1840 try: 

1841 response = await conn.retry.call_with_retry( 

1842 lambda: self._send_command_parse_response( 

1843 conn, command_name, *args, **options 

1844 ), 

1845 failure_callback, 

1846 with_failure_count=True, 

1847 ) 

1848 

1849 await record_operation_duration( 

1850 command_name=command_name, 

1851 duration_seconds=time.monotonic() - start_time, 

1852 server_address=getattr(conn, "host", None), 

1853 server_port=getattr(conn, "port", None), 

1854 db_namespace=str(conn.db), 

1855 ) 

1856 

1857 return response 

1858 except Exception as e: 

1859 await record_error_count( 

1860 server_address=getattr(conn, "host", None), 

1861 server_port=getattr(conn, "port", None), 

1862 network_peer_address=getattr(conn, "host", None), 

1863 network_peer_port=getattr(conn, "port", None), 

1864 error_type=e, 

1865 retry_attempts=actual_retry_attempts, 

1866 is_internal=False, 

1867 ) 

1868 raise 

1869 

1870 def pipeline_execute_command(self, *args, **options): 

1871 """ 

1872 Stage a command to be executed when execute() is next called 

1873 

1874 Returns the current Pipeline object back so commands can be 

1875 chained together, such as: 

1876 

1877 pipe = pipe.set('foo', 'bar').incr('baz').decr('bang') 

1878 

1879 At some other point, you can then run: pipe.execute(), 

1880 which will execute all commands queued in the pipe. 

1881 """ 

1882 self.command_stack.append((args, options)) 

1883 return self 

1884 

1885 async def _execute_transaction( # noqa: C901 

1886 self, connection: Connection, commands: CommandStackT, raise_on_error 

1887 ): 

1888 pre: CommandT = (("MULTI",), {}) 

1889 post: CommandT = (("EXEC",), {}) 

1890 cmds = (pre, *commands, post) 

1891 all_cmds = connection.pack_commands( 

1892 args for args, options in cmds if EMPTY_RESPONSE not in options 

1893 ) 

1894 await connection.send_packed_command(all_cmds) 

1895 errors = [] 

1896 

1897 # parse off the response for MULTI 

1898 # NOTE: we need to handle ResponseErrors here and continue 

1899 # so that we read all the additional command messages from 

1900 # the socket 

1901 try: 

1902 await self.parse_response(connection, "_") 

1903 except ResponseError as err: 

1904 errors.append((0, err)) 

1905 

1906 # and all the other commands 

1907 for i, command in enumerate(commands): 

1908 if EMPTY_RESPONSE in command[1]: 

1909 errors.append((i, command[1][EMPTY_RESPONSE])) 

1910 else: 

1911 try: 

1912 await self.parse_response(connection, "_") 

1913 except ResponseError as err: 

1914 self.annotate_exception(err, i + 1, command[0]) 

1915 errors.append((i, err)) 

1916 

1917 # parse the EXEC. 

1918 try: 

1919 response = await self.parse_response(connection, "_") 

1920 except ExecAbortError as err: 

1921 if errors: 

1922 raise errors[0][1] from err 

1923 raise 

1924 

1925 # EXEC clears any watched keys 

1926 self.watching = False 

1927 

1928 if response is None: 

1929 raise WatchError("Watched variable changed.") from None 

1930 

1931 # put any parse errors into the response 

1932 for i, e in errors: 

1933 response.insert(i, e) 

1934 

1935 if len(response) != len(commands): 

1936 if self.connection: 

1937 await self.connection.disconnect() 

1938 raise ResponseError( 

1939 "Wrong number of response items from pipeline execution" 

1940 ) from None 

1941 

1942 # find any errors in the response and raise if necessary 

1943 if raise_on_error: 

1944 self.raise_first_error(commands, response) 

1945 

1946 # We have to run response callbacks manually 

1947 data = [] 

1948 for r, cmd in zip(response, commands): 

1949 if not isinstance(r, Exception): 

1950 args, options = cmd 

1951 command_name = args[0] 

1952 

1953 # Remove keys entry, it needs only for cache. 

1954 options.pop("keys", None) 

1955 

1956 if command_name in self.response_callbacks: 

1957 r = self.response_callbacks[command_name](r, **options) 

1958 if inspect.isawaitable(r): 

1959 r = await r 

1960 data.append(r) 

1961 return data 

1962 

1963 async def _execute_pipeline( 

1964 self, connection: Connection, commands: CommandStackT, raise_on_error: bool 

1965 ): 

1966 # build up all commands into a single request to increase network perf 

1967 all_cmds = connection.pack_commands([args for args, _ in commands]) 

1968 await connection.send_packed_command(all_cmds) 

1969 

1970 response = [] 

1971 for args, options in commands: 

1972 try: 

1973 response.append( 

1974 await self.parse_response(connection, args[0], **options) 

1975 ) 

1976 except ResponseError as e: 

1977 response.append(e) 

1978 

1979 if raise_on_error: 

1980 self.raise_first_error(commands, response) 

1981 return response 

1982 

1983 def raise_first_error(self, commands: CommandStackT, response: Iterable[Any]): 

1984 for i, r in enumerate(response): 

1985 if isinstance(r, ResponseError): 

1986 self.annotate_exception(r, i + 1, commands[i][0]) 

1987 raise r 

1988 

1989 def annotate_exception( 

1990 self, exception: Exception, number: int, command: Iterable[object] 

1991 ) -> None: 

1992 cmd = " ".join(map(safe_str, command)) 

1993 msg = ( 

1994 f"Command # {number} ({truncate_text(cmd)}) " 

1995 f"of pipeline caused error: {exception.args}" 

1996 ) 

1997 exception.args = (msg,) + exception.args[1:] 

1998 

1999 async def parse_response( 

2000 self, connection: Connection, command_name: Union[str, bytes], **options 

2001 ): 

2002 result = await super().parse_response(connection, command_name, **options) 

2003 if command_name in self.UNWATCH_COMMANDS: 

2004 self.watching = False 

2005 elif command_name == "WATCH": 

2006 self.watching = True 

2007 return result 

2008 

2009 async def load_scripts(self): 

2010 # make sure all scripts that are about to be run on this pipeline exist 

2011 scripts = list(self.scripts) 

2012 immediate = self.immediate_execute_command 

2013 shas = [s.sha for s in scripts] 

2014 # we can't use the normal script_* methods because they would just 

2015 # get buffered in the pipeline. 

2016 exists = await immediate("SCRIPT EXISTS", *shas) 

2017 if not all(exists): 

2018 for s, exist in zip(scripts, exists): 

2019 if not exist: 

2020 s.sha = await immediate("SCRIPT LOAD", s.script) 

2021 

2022 async def _disconnect_raise_on_watching( 

2023 self, 

2024 conn: Connection, 

2025 error: Exception, 

2026 failure_count: Optional[int] = None, 

2027 start_time: Optional[float] = None, 

2028 command_name: Optional[str] = None, 

2029 ): 

2030 """ 

2031 Close the connection, raise an exception if we were watching. 

2032 

2033 The supported exceptions are already checked in the 

2034 retry object so we don't need to do it here. 

2035 

2036 After we disconnect the connection, it will try to reconnect and 

2037 do a health check as part of the send_command logic(on connection level). 

2038 """ 

2039 if ( 

2040 error 

2041 and failure_count is not None 

2042 and failure_count <= conn.retry.get_retries() 

2043 ): 

2044 await record_operation_duration( 

2045 command_name=command_name, 

2046 duration_seconds=time.monotonic() - start_time, 

2047 server_address=getattr(conn, "host", None), 

2048 server_port=getattr(conn, "port", None), 

2049 db_namespace=str(conn.db), 

2050 error=error, 

2051 retry_attempts=failure_count, 

2052 ) 

2053 await conn.disconnect(error=error, failure_count=failure_count) 

2054 # if we were watching a variable, the watch is no longer valid 

2055 # since this connection has died. raise a WatchError, which 

2056 # indicates the user should retry this transaction. 

2057 if self.watching: 

2058 raise WatchError( 

2059 f"A {type(error).__name__} occurred while watching one or more keys" 

2060 ) 

2061 

2062 async def execute(self, raise_on_error: bool = True) -> List[Any]: 

2063 """Execute all the commands in the current pipeline""" 

2064 stack = self.command_stack 

2065 if not stack and not self.watching: 

2066 return [] 

2067 if self.scripts: 

2068 await self.load_scripts() 

2069 if self.is_transaction or self.explicit_transaction: 

2070 execute = self._execute_transaction 

2071 operation_name = "MULTI" 

2072 else: 

2073 execute = self._execute_pipeline 

2074 operation_name = "PIPELINE" 

2075 

2076 conn = self.connection 

2077 if not conn: 

2078 conn = await self.connection_pool.get_connection() 

2079 # assign to self.connection so reset() releases the connection 

2080 # back to the pool after we're done 

2081 self.connection = conn 

2082 conn = cast(Connection, conn) 

2083 

2084 # Start timing for observability 

2085 start_time = time.monotonic() 

2086 # Track actual retry attempts for error reporting 

2087 actual_retry_attempts = 0 

2088 

2089 def failure_callback(error, failure_count): 

2090 nonlocal actual_retry_attempts 

2091 actual_retry_attempts = failure_count 

2092 return self._disconnect_raise_on_watching( 

2093 conn, error, failure_count, start_time, operation_name 

2094 ) 

2095 

2096 try: 

2097 response = await conn.retry.call_with_retry( 

2098 lambda: execute(conn, stack, raise_on_error), 

2099 failure_callback, 

2100 with_failure_count=True, 

2101 ) 

2102 

2103 await record_operation_duration( 

2104 command_name=operation_name, 

2105 duration_seconds=time.monotonic() - start_time, 

2106 server_address=getattr(conn, "host", None), 

2107 server_port=getattr(conn, "port", None), 

2108 db_namespace=str(conn.db), 

2109 ) 

2110 return response 

2111 except Exception as e: 

2112 await record_error_count( 

2113 server_address=getattr(conn, "host", None), 

2114 server_port=getattr(conn, "port", None), 

2115 network_peer_address=getattr(conn, "host", None), 

2116 network_peer_port=getattr(conn, "port", None), 

2117 error_type=e, 

2118 retry_attempts=actual_retry_attempts, 

2119 is_internal=False, 

2120 ) 

2121 raise 

2122 finally: 

2123 await self.reset() 

2124 

2125 async def discard(self): 

2126 """Flushes all previously queued commands 

2127 See: https://redis.io/commands/DISCARD 

2128 """ 

2129 await self.execute_command("DISCARD") 

2130 

2131 async def watch(self, *names: KeyT): 

2132 """Watches the values at keys ``names``""" 

2133 if self.explicit_transaction: 

2134 raise RedisError("Cannot issue a WATCH after a MULTI") 

2135 return await self.execute_command("WATCH", *names) 

2136 

2137 async def unwatch(self): 

2138 """Unwatches all previously specified keys""" 

2139 return self.watching and await self.execute_command("UNWATCH") or True