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

774 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.maint_notifications import MaintNotificationsConfig 

85from redis.observability.attributes import PubSubDirection 

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

87from redis.utils import ( 

88 SENTINEL, 

89 SSL_AVAILABLE, 

90 _set_info_logger, 

91 check_protocol_version, 

92 deprecated_args, 

93 deprecated_function, 

94 safe_str, 

95 str_if_bytes, 

96 truncate_text, 

97) 

98 

99if TYPE_CHECKING and SSL_AVAILABLE: 

100 from ssl import TLSVersion, VerifyFlags, VerifyMode 

101else: 

102 TLSVersion = None 

103 VerifyMode = None 

104 VerifyFlags = None 

105 

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

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

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

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

110if TYPE_CHECKING: 

111 from redis.asyncio.keyspace_notifications import AsyncKeyspaceNotifications 

112 from redis.commands.core import Script 

113 

114 

115class ResponseCallbackProtocol(Protocol): 

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

117 

118 

119class AsyncResponseCallbackProtocol(Protocol): 

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

121 

122 

123ResponseCallbackT = Union[ResponseCallbackProtocol, AsyncResponseCallbackProtocol] 

124 

125 

126class Redis( 

127 AbstractRedis, AsyncRedisModuleCommands, AsyncCoreCommands, AsyncSentinelCommands 

128): 

129 """ 

130 Implementation of the Redis protocol. 

131 

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

133 and an implementation of the Redis protocol. 

134 

135 Pipelines derive from this, implementing how 

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

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

138 Connection object to talk to redis. 

139 """ 

140 

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

142 _is_async_client: Literal[True] = True 

143 

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

145 

146 @classmethod 

147 def from_url( 

148 cls: Type["Redis"], 

149 url: str, 

150 single_connection_client: bool = False, 

151 auto_close_connection_pool: Optional[bool] = None, 

152 **kwargs, 

153 ) -> "Redis": 

154 """ 

155 Return a Redis client object configured from the given URL 

156 

157 For example:: 

158 

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

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

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

162 

163 Three URL schemes are supported: 

164 

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

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

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

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

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

170 

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

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

173 percent-encoded values with their corresponding characters. 

174 

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

176 found will be used: 

177 

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

179 

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

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

182 

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

184 

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

186 

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

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

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

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

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

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

193 arguments always win. 

194 

195 """ 

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

197 client = cls( 

198 connection_pool=connection_pool, 

199 single_connection_client=single_connection_client, 

200 ) 

201 if auto_close_connection_pool is not None: 

202 warnings.warn( 

203 DeprecationWarning( 

204 '"auto_close_connection_pool" is deprecated ' 

205 "since version 5.0.1. " 

206 "Please create a ConnectionPool explicitly and " 

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

208 ) 

209 ) 

210 else: 

211 auto_close_connection_pool = True 

212 client.auto_close_connection_pool = auto_close_connection_pool 

213 return client 

214 

215 @classmethod 

216 def from_pool( 

217 cls: Type["Redis"], 

218 connection_pool: ConnectionPool, 

219 ) -> "Redis": 

220 """ 

221 Return a Redis client from the given connection pool. 

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

223 close it when the Redis client is closed. 

224 

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

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

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

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

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

230 still in use by the others. 

231 

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

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

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

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

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

237 context manager protocol for this:: 

238 

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

240 r = Redis(connection_pool=pool) 

241 """ 

242 client = cls( 

243 connection_pool=connection_pool, 

244 ) 

245 client.auto_close_connection_pool = True 

246 return client 

247 

248 @deprecated_args( 

249 args_to_warn=["retry_on_timeout"], 

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

251 version="6.0.0", 

252 ) 

253 @deprecated_args( 

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

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

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

257 ) 

258 def __init__( 

259 self, 

260 *, 

261 host: str = "localhost", 

262 port: int = 6379, 

263 db: str | int = 0, 

264 password: str | None = None, 

265 socket_timeout: float | None = DEFAULT_SOCKET_TIMEOUT, 

266 socket_connect_timeout: float | None = DEFAULT_SOCKET_CONNECT_TIMEOUT, 

267 socket_read_size: int = DEFAULT_SOCKET_READ_SIZE, 

268 socket_keepalive: bool | None = True, 

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

270 connection_pool: ConnectionPool | None = None, 

271 unix_socket_path: str | None = None, 

272 encoding: str = "utf-8", 

273 encoding_errors: str = "strict", 

274 decode_responses: bool = False, 

275 retry_on_timeout: bool = False, 

276 retry: Retry = Retry( 

277 backoff=ExponentialWithJitterBackoff( 

278 base=DEFAULT_RETRY_BASE, cap=DEFAULT_RETRY_CAP 

279 ), 

280 retries=DEFAULT_RETRY_COUNT, 

281 ), 

282 retry_on_error: list | None = None, 

283 ssl: bool = False, 

284 ssl_keyfile: str | None = None, 

285 ssl_certfile: str | None = None, 

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

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

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

289 ssl_ca_certs: str | None = None, 

290 ssl_ca_data: str | None = None, 

291 ssl_ca_path: str | None = None, 

292 ssl_check_hostname: bool = True, 

293 ssl_min_version: "TLSVersion | None" = None, 

294 ssl_ciphers: str | None = None, 

295 ssl_password: str | None = None, 

296 max_connections: int | None = None, 

297 single_connection_client: bool = False, 

298 health_check_interval: int = 0, 

299 client_name: str | None = None, 

300 lib_name: str | object | None = SENTINEL, 

301 lib_version: str | object | None = SENTINEL, 

302 driver_info: DriverInfo | object | None = SENTINEL, 

303 username: str | None = None, 

304 auto_close_connection_pool: bool | None = None, 

305 redis_connect_func=None, 

306 credential_provider: CredentialProvider | None = None, 

307 protocol: int | None = None, 

308 legacy_responses: bool = True, 

309 event_dispatcher: EventDispatcher | None = None, 

310 maint_notifications_config: MaintNotificationsConfig | None = None, 

311 ): 

312 """ 

313 Initialize a new Redis client. 

314 

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

316 

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

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

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

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

321 errors specified in `retry_on_error`. 

322 

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

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

325 the errors on which retries will happen. 

326 

327 `retry_on_timeout` is deprecated - please include the TimeoutError 

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

329 

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

331 provided pool will be used. 

332 

333 Args: 

334 

335 socket_keepalive: 

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

337 Argument is ignored when connection_pool is provided. 

338 socket_keepalive_options: 

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

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

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

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

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

344 avoid setting additional TCP keepalive options. Argument is ignored 

345 when connection_pool is provided. 

346 maint_notifications_config: 

347 configures the pool to support maintenance notifications - see 

348 `redis.maint_notifications.MaintNotificationsConfig` for details. 

349 Only supported with RESP3 

350 If not provided and protocol is RESP3, the maintenance notifications 

351 will be enabled by default (logic is included in the connection pool 

352 initialization). 

353 Argument is ignored when connection_pool is provided. 

354 """ 

355 kwargs: Dict[str, Any] 

356 if event_dispatcher is None: 

357 self._event_dispatcher = EventDispatcher() 

358 else: 

359 self._event_dispatcher = event_dispatcher 

360 # auto_close_connection_pool only has an effect if connection_pool is 

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

362 # wants to manage the connection pool themselves. 

363 if auto_close_connection_pool is not None: 

364 warnings.warn( 

365 DeprecationWarning( 

366 '"auto_close_connection_pool" is deprecated ' 

367 "since version 5.0.1. " 

368 "Please create a ConnectionPool explicitly and " 

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

370 ) 

371 ) 

372 else: 

373 auto_close_connection_pool = True 

374 

375 if not connection_pool: 

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

377 if not retry_on_error: 

378 retry_on_error = [] 

379 

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

381 computed_driver_info = resolve_driver_info( 

382 driver_info, lib_name, lib_version 

383 ) 

384 

385 kwargs = { 

386 "db": db, 

387 "username": username, 

388 "password": password, 

389 "credential_provider": credential_provider, 

390 "socket_timeout": socket_timeout, 

391 "socket_read_size": socket_read_size, 

392 "encoding": encoding, 

393 "encoding_errors": encoding_errors, 

394 "decode_responses": decode_responses, 

395 "retry_on_error": retry_on_error, 

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

397 "max_connections": max_connections, 

398 "health_check_interval": health_check_interval, 

399 "client_name": client_name, 

400 "driver_info": computed_driver_info, 

401 "redis_connect_func": redis_connect_func, 

402 "protocol": protocol, 

403 "legacy_responses": legacy_responses, 

404 } 

405 # based on input, setup appropriate connection args 

406 if unix_socket_path is not None: 

407 if ( 

408 maint_notifications_config 

409 and maint_notifications_config.enabled is True 

410 ): 

411 raise RedisError( 

412 "Maintenance notifications are not supported with Unix " 

413 "domain socket connections" 

414 ) 

415 kwargs.update( 

416 { 

417 "path": unix_socket_path, 

418 "connection_class": UnixDomainSocketConnection, 

419 "maint_notifications_config": MaintNotificationsConfig( 

420 enabled=False 

421 ), 

422 } 

423 ) 

424 else: 

425 # TCP specific options 

426 kwargs.update( 

427 { 

428 "host": host, 

429 "port": port, 

430 "socket_connect_timeout": socket_connect_timeout, 

431 "socket_keepalive": socket_keepalive, 

432 "socket_keepalive_options": socket_keepalive_options, 

433 } 

434 ) 

435 

436 if ssl: 

437 kwargs.update( 

438 { 

439 "connection_class": SSLConnection, 

440 "ssl_keyfile": ssl_keyfile, 

441 "ssl_certfile": ssl_certfile, 

442 "ssl_cert_reqs": ssl_cert_reqs, 

443 "ssl_include_verify_flags": ssl_include_verify_flags, 

444 "ssl_exclude_verify_flags": ssl_exclude_verify_flags, 

445 "ssl_ca_certs": ssl_ca_certs, 

446 "ssl_ca_data": ssl_ca_data, 

447 "ssl_ca_path": ssl_ca_path, 

448 "ssl_check_hostname": ssl_check_hostname, 

449 "ssl_min_version": ssl_min_version, 

450 "ssl_ciphers": ssl_ciphers, 

451 "ssl_password": ssl_password, 

452 } 

453 ) 

454 maint_notifications_enabled = ( 

455 maint_notifications_config and maint_notifications_config.enabled 

456 ) 

457 if maint_notifications_enabled and not check_protocol_version(protocol, 3): 

458 raise RedisError( 

459 "Maintenance notifications handlers on connection are only supported with RESP version 3" 

460 ) 

461 if maint_notifications_config: 

462 kwargs.update( 

463 { 

464 "maint_notifications_config": maint_notifications_config, 

465 } 

466 ) 

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

468 self.auto_close_connection_pool = auto_close_connection_pool 

469 connection_pool = ConnectionPool(**kwargs) 

470 self._event_dispatcher.dispatch( 

471 AfterPooledConnectionsInstantiationEvent( 

472 [connection_pool], ClientType.ASYNC, credential_provider 

473 ) 

474 ) 

475 else: 

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

477 self.auto_close_connection_pool = False 

478 self._event_dispatcher.dispatch( 

479 AfterPooledConnectionsInstantiationEvent( 

480 [connection_pool], ClientType.ASYNC, credential_provider 

481 ) 

482 ) 

483 

484 self.connection_pool = connection_pool 

485 self.single_connection_client = single_connection_client 

486 self.connection: Optional[Connection] = None 

487 

488 connection_kwargs = self.connection_pool.connection_kwargs 

489 self.response_callbacks = CaseInsensitiveDict( 

490 get_response_callbacks( 

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

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

493 ) 

494 ) 

495 

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

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

498 # on a set of redis commands 

499 self._single_conn_lock = asyncio.Lock() 

500 

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

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

503 # using the client. 

504 self._usage_counter = 0 

505 self._usage_lock = asyncio.Lock() 

506 

507 def __repr__(self): 

508 return ( 

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

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

511 ) 

512 

513 def __await__(self): 

514 return self.initialize().__await__() 

515 

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

517 if self.single_connection_client: 

518 async with self._single_conn_lock: 

519 if self.connection is None: 

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

521 

522 self._event_dispatcher.dispatch( 

523 AfterSingleConnectionInstantiationEvent( 

524 self.connection, ClientType.ASYNC, self._single_conn_lock 

525 ) 

526 ) 

527 return self 

528 

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

530 """Set a custom Response Callback""" 

531 self.response_callbacks[command] = callback 

532 

533 def get_encoder(self): 

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

535 return self.connection_pool.get_encoder() 

536 

537 def get_connection_kwargs(self): 

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

539 return self.connection_pool.connection_kwargs 

540 

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

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

543 

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

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

546 self.connection_pool.set_retry(retry) 

547 

548 def load_external_module(self, funcname, func): 

549 """ 

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

551 and their namespaces to the redis client. 

552 

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

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

555 

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

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

558 To load function functions into this namespace: 

559 

560 from redis import Redis 

561 from foomodule import F 

562 r = Redis() 

563 r.load_external_module("foo", F) 

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

565 

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

567 tests/test_connection.py::test_loading_external_modules 

568 """ 

569 setattr(self, funcname, func) 

570 

571 def pipeline( 

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

573 ) -> "Pipeline": 

574 """ 

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

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

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

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

579 between the client and server. 

580 """ 

581 return Pipeline( 

582 self.connection_pool, self.response_callbacks, transaction, shard_hint 

583 ) 

584 

585 async def transaction( 

586 self, 

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

588 *watches: KeyT, 

589 shard_hint: Optional[str] = None, 

590 value_from_callable: bool = False, 

591 watch_delay: Optional[float] = None, 

592 ): 

593 """ 

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

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

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

597 """ 

598 pipe: Pipeline 

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

600 while True: 

601 try: 

602 if watches: 

603 await pipe.watch(*watches) 

604 func_value = func(pipe) 

605 if inspect.isawaitable(func_value): 

606 func_value = await func_value 

607 exec_value = await pipe.execute() 

608 return func_value if value_from_callable else exec_value 

609 except WatchError: 

610 if watch_delay is not None and watch_delay > 0: 

611 await asyncio.sleep(watch_delay) 

612 continue 

613 

614 def lock( 

615 self, 

616 name: KeyT, 

617 timeout: Optional[float] = None, 

618 sleep: float = 0.1, 

619 blocking: bool = True, 

620 blocking_timeout: Optional[float] = None, 

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

622 thread_local: bool = True, 

623 raise_on_release_error: bool = True, 

624 ) -> Lock: 

625 """ 

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

627 the behavior of threading.Lock. 

628 

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

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

631 

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

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

634 holding the lock. 

635 

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

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

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

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

640 argument to ``acquire``. 

641 

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

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

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

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

646 

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

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

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

650 you have created your own custom lock class. 

651 

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

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

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

655 another thread. Consider the following timeline: 

656 

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

658 thread-1 sets the token to "abc" 

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

660 Lock instance. 

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

662 key. 

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

664 thread-2 sets the token to "xyz" 

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

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

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

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

669 

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

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

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

673 will be logged and the exception will be suppressed. 

674 

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

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

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

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

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

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

681 thread local storage.""" 

682 if lock_class is None: 

683 lock_class = Lock 

684 return lock_class( 

685 self, 

686 name, 

687 timeout=timeout, 

688 sleep=sleep, 

689 blocking=blocking, 

690 blocking_timeout=blocking_timeout, 

691 thread_local=thread_local, 

692 raise_on_release_error=raise_on_release_error, 

693 ) 

694 

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

696 """ 

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

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

699 them. 

700 """ 

701 return PubSub( 

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

703 ) 

704 

705 def keyspace_notifications( 

706 self, 

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

708 ignore_subscribe_messages: bool = True, 

709 ) -> "AsyncKeyspaceNotifications": 

710 """ 

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

712 object for subscribing to keyspace and keyevent notifications. 

713 

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

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

716 

717 Args: 

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

719 notifications. 

720 ignore_subscribe_messages: If True, subscribe/unsubscribe 

721 confirmations are not returned by 

722 get_message/listen. 

723 """ 

724 from redis.asyncio.keyspace_notifications import AsyncKeyspaceNotifications 

725 

726 return AsyncKeyspaceNotifications( 

727 self, 

728 key_prefix=key_prefix, 

729 ignore_subscribe_messages=ignore_subscribe_messages, 

730 ) 

731 

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

733 return Monitor(self.connection_pool) 

734 

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

736 return self.__class__( 

737 connection_pool=self.connection_pool, single_connection_client=True 

738 ) 

739 

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

741 """ 

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

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

744 the client. 

745 """ 

746 await self._increment_usage() 

747 try: 

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

749 return await self.initialize() 

750 except Exception: 

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

752 await self._decrement_usage() 

753 raise 

754 

755 async def _increment_usage(self) -> int: 

756 """ 

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

758 Returns the new value of the usage counter. 

759 """ 

760 async with self._usage_lock: 

761 self._usage_counter += 1 

762 return self._usage_counter 

763 

764 async def _decrement_usage(self) -> int: 

765 """ 

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

767 Returns the new value of the usage counter. 

768 """ 

769 async with self._usage_lock: 

770 self._usage_counter -= 1 

771 return self._usage_counter 

772 

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

774 """ 

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

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

777 """ 

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

779 if current_usage == 0: 

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

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

782 

783 _DEL_MESSAGE = "Unclosed Redis client" 

784 

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

786 # by the time __del__ is called at shutdown 

787 def __del__( 

788 self, 

789 _warn: Any = warnings.warn, 

790 _grl: Any = asyncio.get_running_loop, 

791 ) -> None: 

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

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

794 try: 

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

796 _grl().call_exception_handler(context) 

797 except RuntimeError: 

798 pass 

799 self.connection._close() 

800 

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

802 """ 

803 Closes Redis client connection 

804 

805 Args: 

806 close_connection_pool: 

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

808 overriding Redis.auto_close_connection_pool. 

809 By default, let Redis.auto_close_connection_pool decide 

810 whether to close the connection pool. 

811 """ 

812 conn = self.connection 

813 if conn: 

814 self.connection = None 

815 await self.connection_pool.release(conn) 

816 if close_connection_pool or ( 

817 close_connection_pool is None and self.auto_close_connection_pool 

818 ): 

819 await self.connection_pool.aclose() 

820 

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

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

823 """ 

824 Alias for aclose(), for backwards compatibility 

825 """ 

826 await self.aclose(close_connection_pool) 

827 

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

829 """ 

830 Send a command and parse the response 

831 """ 

832 await conn.send_command(*args) 

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

834 

835 async def _close_connection( 

836 self, 

837 conn: Connection, 

838 error: Optional[BaseException] = None, 

839 failure_count: Optional[int] = None, 

840 start_time: Optional[float] = None, 

841 command_name: Optional[str] = None, 

842 ): 

843 """ 

844 Close the connection before retrying. 

845 

846 The supported exceptions are already checked in the 

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

848 

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

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

851 """ 

852 if ( 

853 error 

854 and failure_count is not None 

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

856 ): 

857 await record_operation_duration( 

858 command_name=command_name, 

859 duration_seconds=time.monotonic() - start_time, 

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

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

862 db_namespace=str(conn.db), 

863 error=error, 

864 retry_attempts=failure_count, 

865 ) 

866 

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

868 

869 # COMMAND EXECUTION AND PROTOCOL PARSING 

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

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

872 await self.initialize() 

873 pool = self.connection_pool 

874 command_name = args[0] 

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

876 

877 # Start timing for observability 

878 start_time = time.monotonic() 

879 # Track actual retry attempts for error reporting 

880 actual_retry_attempts = 0 

881 

882 def failure_callback(error, failure_count): 

883 nonlocal actual_retry_attempts 

884 actual_retry_attempts = failure_count 

885 return self._close_connection( 

886 conn, error, failure_count, start_time, command_name 

887 ) 

888 

889 if self.single_connection_client: 

890 await self._single_conn_lock.acquire() 

891 try: 

892 result = await conn.retry.call_with_retry( 

893 lambda: self._send_command_parse_response( 

894 conn, command_name, *args, **options 

895 ), 

896 failure_callback, 

897 with_failure_count=True, 

898 ) 

899 

900 await record_operation_duration( 

901 command_name=command_name, 

902 duration_seconds=time.monotonic() - start_time, 

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

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

905 db_namespace=str(conn.db), 

906 ) 

907 return result 

908 except Exception as e: 

909 await record_error_count( 

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

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

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

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

914 error_type=e, 

915 retry_attempts=actual_retry_attempts, 

916 is_internal=False, 

917 ) 

918 raise 

919 finally: 

920 try: 

921 if self.single_connection_client and conn and conn.should_reconnect(): 

922 await self._close_connection(conn) 

923 await conn.connect() 

924 finally: 

925 if self.single_connection_client: 

926 self._single_conn_lock.release() 

927 if not self.connection: 

928 await pool.release(conn) 

929 

930 async def parse_response( 

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

932 ): 

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

934 try: 

935 if NEVER_DECODE in options: 

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

937 options.pop(NEVER_DECODE) 

938 else: 

939 response = await connection.read_response() 

940 except ResponseError: 

941 if EMPTY_RESPONSE in options: 

942 return options[EMPTY_RESPONSE] 

943 raise 

944 

945 if EMPTY_RESPONSE in options: 

946 options.pop(EMPTY_RESPONSE) 

947 

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

949 options.pop("keys", None) 

950 

951 if command_name in self.response_callbacks: 

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

953 command_name = cast(str, command_name) 

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

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

956 return response 

957 

958 

959StrictRedis = Redis 

960 

961 

962class MonitorCommandInfo(TypedDict): 

963 time: float 

964 db: int 

965 client_address: str 

966 client_port: str 

967 client_type: str 

968 command: str 

969 

970 

971class Monitor: 

972 """ 

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

974 next_command() method returns one command from monitor 

975 listen() method yields commands from monitor. 

976 """ 

977 

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

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

980 

981 def __init__(self, connection_pool: ConnectionPool): 

982 self.connection_pool = connection_pool 

983 self.connection: Optional[Connection] = None 

984 

985 async def connect(self): 

986 if self.connection is None: 

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

988 

989 async def __aenter__(self): 

990 await self.connect() 

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

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

993 response = await self.connection.read_response() 

994 if not bool_ok(response): 

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

996 return self 

997 

998 async def __aexit__(self, *args): 

999 await self.connection.disconnect() 

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

1001 

1002 async def next_command(self) -> MonitorCommandInfo: 

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

1004 await self.connect() 

1005 response = await self.connection.read_response() 

1006 if isinstance(response, bytes): 

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

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

1009 m = self.monitor_re.match(command_data) 

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

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

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

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

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

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

1016 

1017 if client_info == "lua": 

1018 client_address = "lua" 

1019 client_port = "" 

1020 client_type = "lua" 

1021 elif client_info.startswith("unix"): 

1022 client_address = "unix" 

1023 client_port = client_info[5:] 

1024 client_type = "unix" 

1025 else: 

1026 # use rsplit as ipv6 addresses contain colons 

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

1028 client_type = "tcp" 

1029 return { 

1030 "time": float(command_time), 

1031 "db": int(db_id), 

1032 "client_address": client_address, 

1033 "client_port": client_port, 

1034 "client_type": client_type, 

1035 "command": command, 

1036 } 

1037 

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

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

1040 while True: 

1041 yield await self.next_command() 

1042 

1043 

1044class PubSub: 

1045 """ 

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

1047 

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

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

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

1051 """ 

1052 

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

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

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

1056 

1057 def __init__( 

1058 self, 

1059 connection_pool: ConnectionPool, 

1060 shard_hint: Optional[str] = None, 

1061 ignore_subscribe_messages: bool = False, 

1062 encoder=None, 

1063 push_handler_func: Optional[Callable] = None, 

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

1065 ): 

1066 if event_dispatcher is None: 

1067 self._event_dispatcher = EventDispatcher() 

1068 else: 

1069 self._event_dispatcher = event_dispatcher 

1070 self.connection_pool = connection_pool 

1071 self.shard_hint = shard_hint 

1072 self.ignore_subscribe_messages = ignore_subscribe_messages 

1073 self.connection = None 

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

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

1076 self.encoder = encoder 

1077 self.push_handler_func = push_handler_func 

1078 if self.encoder is None: 

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

1080 if self.encoder.decode_responses: 

1081 self.health_check_response = [ 

1082 ["pong", self.HEALTH_CHECK_MESSAGE], 

1083 self.HEALTH_CHECK_MESSAGE, 

1084 ] 

1085 else: 

1086 self.health_check_response = [ 

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

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

1089 ] 

1090 if self.push_handler_func is None: 

1091 _set_info_logger() 

1092 self.channels = {} 

1093 self.pending_unsubscribe_channels = set() 

1094 self.patterns = {} 

1095 self.pending_unsubscribe_patterns = set() 

1096 self.shard_channels = {} 

1097 self.pending_unsubscribe_shard_channels = set() 

1098 self._lock = asyncio.Lock() 

1099 

1100 async def __aenter__(self): 

1101 return self 

1102 

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

1104 await self.aclose() 

1105 

1106 def __del__(self): 

1107 if self.connection: 

1108 self.connection.deregister_connect_callback(self.on_connect) 

1109 

1110 async def aclose(self): 

1111 # In case a connection property does not yet exist 

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

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

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

1115 return 

1116 async with self._lock: 

1117 if self.connection: 

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

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

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

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

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

1123 self.connection.deregister_connect_callback(self.on_connect) 

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

1125 self.connection = None 

1126 self.channels = {} 

1127 self.pending_unsubscribe_channels = set() 

1128 self.patterns = {} 

1129 self.pending_unsubscribe_patterns = set() 

1130 self.shard_channels = {} 

1131 self.pending_unsubscribe_shard_channels = set() 

1132 

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

1134 async def close(self) -> None: 

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

1136 await self.aclose() 

1137 

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

1139 async def reset(self) -> None: 

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

1141 await self.aclose() 

1142 

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

1144 # Replay handler-backed subscriptions as positional Subscription objects 

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

1146 subscriptions = pubsub_subscription_args(subscribed) 

1147 if subscriptions: 

1148 await subscribe_fn(*subscriptions) 

1149 

1150 async def _resubscribe_shard_channels(self) -> None: 

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

1152 

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

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

1155 self.pending_unsubscribe_channels.clear() 

1156 self.pending_unsubscribe_patterns.clear() 

1157 self.pending_unsubscribe_shard_channels.clear() 

1158 if self.channels: 

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

1160 if self.patterns: 

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

1162 if self.shard_channels: 

1163 await self._resubscribe_shard_channels() 

1164 

1165 @property 

1166 def subscribed(self): 

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

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

1169 

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

1171 """Execute a publish/subscribe command""" 

1172 

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

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

1175 # subscribed to one or more channels 

1176 

1177 await self.connect() 

1178 connection = self.connection 

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

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

1181 

1182 async def connect(self): 

1183 """ 

1184 Ensure that the PubSub is connected 

1185 """ 

1186 if self.connection is None: 

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

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

1189 # were listening to when we were disconnected 

1190 self.connection.register_connect_callback(self.on_connect) 

1191 else: 

1192 await self.connection.connect() 

1193 if self.push_handler_func is not None: 

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

1195 

1196 self._event_dispatcher.dispatch( 

1197 AfterPubSubConnectionInstantiationEvent( 

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

1199 ) 

1200 ) 

1201 

1202 async def _reconnect( 

1203 self, 

1204 conn, 

1205 error: Optional[BaseException] = None, 

1206 failure_count: Optional[int] = None, 

1207 start_time: Optional[float] = None, 

1208 command_name: Optional[str] = None, 

1209 ): 

1210 """ 

1211 The supported exceptions are already checked in the 

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

1213 

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

1215 """ 

1216 if ( 

1217 error 

1218 and failure_count is not None 

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

1220 ): 

1221 if command_name: 

1222 await record_operation_duration( 

1223 command_name=command_name, 

1224 duration_seconds=time.monotonic() - start_time, 

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

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

1227 db_namespace=str(conn.db), 

1228 error=error, 

1229 retry_attempts=failure_count, 

1230 ) 

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

1232 await conn.connect() 

1233 

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

1235 """ 

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

1237 this will fail and raise a ConnectionError as desired. 

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

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

1240 patterns we were previously listening to 

1241 """ 

1242 if not len(args) == 0: 

1243 command_name = args[0] 

1244 else: 

1245 command_name = None 

1246 

1247 # Start timing for observability 

1248 start_time = time.monotonic() 

1249 # Track actual retry attempts for error reporting 

1250 actual_retry_attempts = 0 

1251 

1252 def failure_callback(error, failure_count): 

1253 nonlocal actual_retry_attempts 

1254 actual_retry_attempts = failure_count 

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

1256 

1257 try: 

1258 response = await conn.retry.call_with_retry( 

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

1260 failure_callback, 

1261 with_failure_count=True, 

1262 ) 

1263 

1264 if command_name: 

1265 await record_operation_duration( 

1266 command_name=command_name, 

1267 duration_seconds=time.monotonic() - start_time, 

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

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

1270 db_namespace=str(conn.db), 

1271 ) 

1272 

1273 return response 

1274 except Exception as e: 

1275 await record_error_count( 

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

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

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

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

1280 error_type=e, 

1281 retry_attempts=actual_retry_attempts, 

1282 is_internal=False, 

1283 ) 

1284 raise 

1285 

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

1287 """ 

1288 Parse the response from a publish/subscribe command. 

1289 

1290 Args: 

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

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

1293 Default: True 

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

1295 This parameter is ignored when block=True. 

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

1297 

1298 Returns: 

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

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

1301 

1302 Important: 

1303 The block and timeout parameters work together: 

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

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

1306 

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

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

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

1310 

1311 Example: 

1312 # Block indefinitely (timeout is ignored) 

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

1314 

1315 # Non-blocking with 0.1 second timeout 

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

1317 

1318 # Non-blocking, return immediately 

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

1320 

1321 # Recommended: use get_message() instead 

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

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

1324 """ 

1325 conn = self.connection 

1326 if conn is None: 

1327 raise RuntimeError( 

1328 "pubsub connection not set: " 

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

1330 ) 

1331 

1332 await self.check_health() 

1333 

1334 if not conn.is_connected: 

1335 await conn.connect() 

1336 

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

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

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

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

1341 # operations performed by the retry layer continue to honor 

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

1343 # 

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

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

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

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

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

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

1350 # major release. 

1351 read_timeout = math.inf if block else timeout 

1352 response = await self._execute( 

1353 conn, 

1354 conn.read_response, 

1355 timeout=read_timeout, 

1356 disconnect_on_error=False, 

1357 push_request=True, 

1358 ) 

1359 

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

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

1362 return None 

1363 return response 

1364 

1365 async def check_health(self): 

1366 conn = self.connection 

1367 if conn is None: 

1368 raise RuntimeError( 

1369 "pubsub connection not set: " 

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

1371 ) 

1372 

1373 if ( 

1374 conn.health_check_interval 

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

1376 ): 

1377 await conn.send_command( 

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

1379 ) 

1380 

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

1382 """ 

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

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

1385 from coercing the value for each message coming in. 

1386 """ 

1387 encode = self.encoder.encode 

1388 decode = self.encoder.decode 

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

1390 

1391 async def psubscribe( 

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

1393 ) -> None: 

1394 """ 

1395 Subscribe to channel patterns. 

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

1397 key and a callable as the value. 

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

1399 optional handler. 

1400 A pattern's callable will be invoked automatically 

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

1402 message via ``listen()``. 

1403 """ 

1404 new_patterns = parse_pubsub_subscriptions(args, kwargs) 

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

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

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

1408 # for the reconnection. 

1409 new_patterns = self._normalize_keys(new_patterns) 

1410 self.patterns.update(new_patterns) 

1411 self.pending_unsubscribe_patterns.difference_update(new_patterns) 

1412 return ret_val 

1413 

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

1415 """ 

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

1417 all patterns. 

1418 """ 

1419 patterns: Iterable[ChannelT] 

1420 if args: 

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

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

1423 else: 

1424 parsed_args = [] 

1425 patterns = self.patterns 

1426 self.pending_unsubscribe_patterns.update(patterns) 

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

1428 

1429 async def subscribe( 

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

1431 ) -> None: 

1432 """ 

1433 Subscribe to channels. 

1434 Channels supplied as keyword arguments expect 

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

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

1437 optional handler. 

1438 A channel's callable will be invoked automatically 

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

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

1441 """ 

1442 new_channels = parse_pubsub_subscriptions(args, kwargs) 

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

1444 # update the 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_channels = self._normalize_keys(new_channels) 

1448 self.channels.update(new_channels) 

1449 self.pending_unsubscribe_channels.difference_update(new_channels) 

1450 return ret_val 

1451 

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

1453 """ 

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

1455 all channels 

1456 """ 

1457 if args: 

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

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

1460 else: 

1461 parsed_args = [] 

1462 channels = self.channels 

1463 self.pending_unsubscribe_channels.update(channels) 

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

1465 

1466 async def ssubscribe( 

1467 self, 

1468 *args: ChannelT | Subscription, 

1469 target_node: Any = None, 

1470 **kwargs: PubSubHandler, 

1471 ) -> None: 

1472 """ 

1473 Subscribes the client to the specified shard channels. 

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

1475 and a callable as the value. 

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

1477 with an optional handler. 

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

1479 is received on that channel rather than producing a message 

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

1481 """ 

1482 new_s_channels = parse_pubsub_subscriptions(args, kwargs) 

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

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

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

1486 # for the reconnection. 

1487 new_s_channels = self._normalize_keys(new_s_channels) 

1488 self.shard_channels.update(new_s_channels) 

1489 self.pending_unsubscribe_shard_channels.difference_update(new_s_channels) 

1490 return ret_val 

1491 

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

1493 """ 

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

1495 all shard_channels 

1496 """ 

1497 if args: 

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

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

1500 else: 

1501 s_channels = self.shard_channels 

1502 self.pending_unsubscribe_shard_channels.update(s_channels) 

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

1504 

1505 async def listen(self) -> AsyncIterator: 

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

1507 while self.subscribed: 

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

1509 if response is not None: 

1510 yield response 

1511 

1512 async def get_message( 

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

1514 ): 

1515 """ 

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

1517 

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

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

1520 number or None to wait indefinitely. 

1521 """ 

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

1523 if response: 

1524 return await self.handle_message(response, ignore_subscribe_messages) 

1525 return None 

1526 

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

1528 """ 

1529 Ping the Redis server to test connectivity. 

1530 

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

1532 responds with "PONG". 

1533 """ 

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

1535 return self.execute_command(*args) 

1536 

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

1538 """ 

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

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

1541 message being returned. 

1542 """ 

1543 if response is None: 

1544 return None 

1545 if isinstance(response, bytes): 

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

1547 message_type = str_if_bytes(response[0]) 

1548 if message_type == "pmessage": 

1549 message = { 

1550 "type": message_type, 

1551 "pattern": response[1], 

1552 "channel": response[2], 

1553 "data": response[3], 

1554 } 

1555 elif message_type == "pong": 

1556 message = { 

1557 "type": message_type, 

1558 "pattern": None, 

1559 "channel": None, 

1560 "data": response[1], 

1561 } 

1562 else: 

1563 message = { 

1564 "type": message_type, 

1565 "pattern": None, 

1566 "channel": response[1], 

1567 "data": response[2], 

1568 } 

1569 

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

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

1572 await record_pubsub_message( 

1573 direction=PubSubDirection.RECEIVE, 

1574 channel=channel, 

1575 ) 

1576 elif message_type == "smessage": 

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

1578 await record_pubsub_message( 

1579 direction=PubSubDirection.RECEIVE, 

1580 channel=channel, 

1581 sharded=True, 

1582 ) 

1583 

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

1585 if message_type in self.UNSUBSCRIBE_MESSAGE_TYPES: 

1586 if message_type == "punsubscribe": 

1587 pattern = response[1] 

1588 if pattern in self.pending_unsubscribe_patterns: 

1589 self.pending_unsubscribe_patterns.remove(pattern) 

1590 self.patterns.pop(pattern, None) 

1591 elif message_type == "sunsubscribe": 

1592 s_channel = response[1] 

1593 if s_channel in self.pending_unsubscribe_shard_channels: 

1594 self.pending_unsubscribe_shard_channels.remove(s_channel) 

1595 self.shard_channels.pop(s_channel, None) 

1596 else: 

1597 channel = response[1] 

1598 if channel in self.pending_unsubscribe_channels: 

1599 self.pending_unsubscribe_channels.remove(channel) 

1600 self.channels.pop(channel, None) 

1601 

1602 if message_type in self.PUBLISH_MESSAGE_TYPES: 

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

1604 if message_type == "pmessage": 

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

1606 elif message_type == "smessage": 

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

1608 else: 

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

1610 if handler: 

1611 if inspect.iscoroutinefunction(handler): 

1612 await handler(message) 

1613 else: 

1614 handler(message) 

1615 return None 

1616 elif message_type != "pong": 

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

1618 # want them 

1619 if ignore_subscribe_messages or self.ignore_subscribe_messages: 

1620 return None 

1621 

1622 return message 

1623 

1624 async def run( 

1625 self, 

1626 *, 

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

1628 poll_timeout: float = 1.0, 

1629 pubsub=None, 

1630 ) -> None: 

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

1632 

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

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

1635 ``asyncio.create_task``: 

1636 

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

1638 

1639 To shut it down, use asyncio cancellation: 

1640 

1641 >>> task.cancel() 

1642 >>> await task 

1643 """ 

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

1645 if handler is None: 

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

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

1648 if handler is None: 

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

1650 

1651 await self.connect() 

1652 while True: 

1653 try: 

1654 if pubsub is None: 

1655 await self.get_message( 

1656 ignore_subscribe_messages=True, timeout=poll_timeout 

1657 ) 

1658 else: 

1659 await pubsub.get_message( 

1660 ignore_subscribe_messages=True, timeout=poll_timeout 

1661 ) 

1662 except asyncio.CancelledError: 

1663 raise 

1664 except BaseException as e: 

1665 if exception_handler is None: 

1666 raise 

1667 res = exception_handler(e, self) 

1668 if inspect.isawaitable(res): 

1669 await res 

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

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

1672 await asyncio.sleep(0) 

1673 

1674 

1675class PubsubWorkerExceptionHandler(Protocol): 

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

1677 

1678 

1679class AsyncPubsubWorkerExceptionHandler(Protocol): 

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

1681 

1682 

1683PSWorkerThreadExcHandlerT = Union[ 

1684 PubsubWorkerExceptionHandler, AsyncPubsubWorkerExceptionHandler 

1685] 

1686 

1687 

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

1689CommandStackT = List[CommandT] 

1690 

1691 

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

1693 """ 

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

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

1696 saving all the values in a list to Redis. 

1697 

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

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

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

1701 executed atomically. 

1702 

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

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

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

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

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

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

1709 on a key of a different datatype. 

1710 """ 

1711 

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

1713 

1714 def __init__( 

1715 self, 

1716 connection_pool: ConnectionPool, 

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

1718 transaction: bool, 

1719 shard_hint: Optional[str], 

1720 ): 

1721 self.connection_pool = connection_pool 

1722 self.connection = None 

1723 self.response_callbacks = response_callbacks 

1724 self.is_transaction = transaction 

1725 self.shard_hint = shard_hint 

1726 self.watching = False 

1727 self.command_stack: CommandStackT = [] 

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

1729 self.explicit_transaction = False 

1730 

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

1732 return self 

1733 

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

1735 await self.reset() 

1736 

1737 def __await__(self): 

1738 return self._async_self().__await__() 

1739 

1740 _DEL_MESSAGE = "Unclosed Pipeline client" 

1741 

1742 def __len__(self): 

1743 return len(self.command_stack) 

1744 

1745 def __bool__(self): 

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

1747 return True 

1748 

1749 async def _async_self(self): 

1750 return self 

1751 

1752 async def reset(self): 

1753 self.command_stack = [] 

1754 self.scripts = set() 

1755 try: 

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

1757 # watching something 

1758 if self.watching and self.connection: 

1759 try: 

1760 # call this manually since our unwatch or 

1761 # immediate_execute_command methods can call reset() 

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

1763 await self.connection.read_response() 

1764 except ConnectionError: 

1765 # disconnect will also remove any previous WATCHes 

1766 if self.connection: 

1767 await self.connection.disconnect() 

1768 except asyncio.CancelledError: 

1769 # Disconnect so any unread UNWATCH reply does not get 

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

1771 if self.connection: 

1772 await self.connection.disconnect() 

1773 raise 

1774 finally: 

1775 self.watching = False 

1776 self.explicit_transaction = False 

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

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

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

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

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

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

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

1784 if self.connection: 

1785 connection, self.connection = self.connection, None 

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

1787 

1788 async def aclose(self) -> None: 

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

1790 await self.reset() 

1791 

1792 def multi(self): 

1793 """ 

1794 Start a transactional block of the pipeline after WATCH commands 

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

1796 """ 

1797 if self.explicit_transaction: 

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

1799 if self.command_stack: 

1800 raise RedisError( 

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

1802 ) 

1803 self.explicit_transaction = True 

1804 

1805 def execute_command( 

1806 self, *args, **kwargs 

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

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

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

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

1811 

1812 async def _disconnect_reset_raise_on_watching( 

1813 self, 

1814 conn: Connection, 

1815 error: Exception, 

1816 failure_count: Optional[int] = None, 

1817 start_time: Optional[float] = None, 

1818 command_name: Optional[str] = None, 

1819 ) -> None: 

1820 """ 

1821 Close the connection reset watching state and 

1822 raise an exception if we were watching. 

1823 

1824 The supported exceptions are already checked in the 

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

1826 

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

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

1829 """ 

1830 if ( 

1831 error 

1832 and failure_count is not None 

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

1834 ): 

1835 await record_operation_duration( 

1836 command_name=command_name, 

1837 duration_seconds=time.monotonic() - start_time, 

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

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

1840 db_namespace=str(conn.db), 

1841 error=error, 

1842 retry_attempts=failure_count, 

1843 ) 

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

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

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

1847 # indicates the user should retry this transaction. 

1848 if self.watching: 

1849 await self.reset() 

1850 raise WatchError( 

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

1852 ) 

1853 

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

1855 """ 

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

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

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

1859 MULTI is called. 

1860 """ 

1861 command_name = args[0] 

1862 conn = self.connection 

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

1864 if not conn: 

1865 conn = await self.connection_pool.get_connection() 

1866 self.connection = conn 

1867 

1868 # Start timing for observability 

1869 start_time = time.monotonic() 

1870 # Track actual retry attempts for error reporting 

1871 actual_retry_attempts = 0 

1872 

1873 def failure_callback(error, failure_count): 

1874 nonlocal actual_retry_attempts 

1875 actual_retry_attempts = failure_count 

1876 return self._disconnect_reset_raise_on_watching( 

1877 conn, error, failure_count, start_time, command_name 

1878 ) 

1879 

1880 try: 

1881 response = await conn.retry.call_with_retry( 

1882 lambda: self._send_command_parse_response( 

1883 conn, command_name, *args, **options 

1884 ), 

1885 failure_callback, 

1886 with_failure_count=True, 

1887 ) 

1888 

1889 await record_operation_duration( 

1890 command_name=command_name, 

1891 duration_seconds=time.monotonic() - start_time, 

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

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

1894 db_namespace=str(conn.db), 

1895 ) 

1896 

1897 return response 

1898 except Exception as e: 

1899 await record_error_count( 

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

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

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

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

1904 error_type=e, 

1905 retry_attempts=actual_retry_attempts, 

1906 is_internal=False, 

1907 ) 

1908 raise 

1909 

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

1911 """ 

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

1913 

1914 Returns the current Pipeline object back so commands can be 

1915 chained together, such as: 

1916 

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

1918 

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

1920 which will execute all commands queued in the pipe. 

1921 """ 

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

1923 return self 

1924 

1925 async def _execute_transaction( # noqa: C901 

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

1927 ): 

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

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

1930 cmds = (pre, *commands, post) 

1931 all_cmds = connection.pack_commands( 

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

1933 ) 

1934 await connection.send_packed_command(all_cmds) 

1935 errors = [] 

1936 

1937 # parse off the response for MULTI 

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

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

1940 # the socket 

1941 try: 

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

1943 except ResponseError as err: 

1944 errors.append((0, err)) 

1945 

1946 # and all the other commands 

1947 for i, command in enumerate(commands): 

1948 if EMPTY_RESPONSE in command[1]: 

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

1950 else: 

1951 try: 

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

1953 except ResponseError as err: 

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

1955 errors.append((i, err)) 

1956 

1957 # parse the EXEC. 

1958 try: 

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

1960 except ExecAbortError as err: 

1961 if errors: 

1962 raise errors[0][1] from err 

1963 raise 

1964 

1965 # EXEC clears any watched keys 

1966 self.watching = False 

1967 

1968 if response is None: 

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

1970 

1971 # put any parse errors into the response 

1972 for i, e in errors: 

1973 response.insert(i, e) 

1974 

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

1976 if self.connection: 

1977 await self.connection.disconnect() 

1978 raise ResponseError( 

1979 "Wrong number of response items from pipeline execution" 

1980 ) from None 

1981 

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

1983 if raise_on_error: 

1984 self.raise_first_error(commands, response) 

1985 

1986 # We have to run response callbacks manually 

1987 data = [] 

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

1989 if not isinstance(r, Exception): 

1990 args, options = cmd 

1991 command_name = args[0] 

1992 

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

1994 options.pop("keys", None) 

1995 

1996 if command_name in self.response_callbacks: 

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

1998 if inspect.isawaitable(r): 

1999 r = await r 

2000 data.append(r) 

2001 return data 

2002 

2003 async def _execute_pipeline( 

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

2005 ): 

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

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

2008 await connection.send_packed_command(all_cmds) 

2009 

2010 response = [] 

2011 for args, options in commands: 

2012 try: 

2013 response.append( 

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

2015 ) 

2016 except ResponseError as e: 

2017 response.append(e) 

2018 

2019 if raise_on_error: 

2020 self.raise_first_error(commands, response) 

2021 return response 

2022 

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

2024 for i, r in enumerate(response): 

2025 if isinstance(r, ResponseError): 

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

2027 raise r 

2028 

2029 def annotate_exception( 

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

2031 ) -> None: 

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

2033 msg = ( 

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

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

2036 ) 

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

2038 

2039 async def parse_response( 

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

2041 ): 

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

2043 if command_name in self.UNWATCH_COMMANDS: 

2044 self.watching = False 

2045 elif command_name == "WATCH": 

2046 self.watching = True 

2047 return result 

2048 

2049 async def load_scripts(self): 

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

2051 scripts = list(self.scripts) 

2052 immediate = self.immediate_execute_command 

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

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

2055 # get buffered in the pipeline. 

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

2057 if not all(exists): 

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

2059 if not exist: 

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

2061 

2062 async def _disconnect_raise_on_watching( 

2063 self, 

2064 conn: Connection, 

2065 error: Exception, 

2066 failure_count: Optional[int] = None, 

2067 start_time: Optional[float] = None, 

2068 command_name: Optional[str] = None, 

2069 ): 

2070 """ 

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

2072 

2073 The supported exceptions are already checked in the 

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

2075 

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

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

2078 """ 

2079 if ( 

2080 error 

2081 and failure_count is not None 

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

2083 ): 

2084 await record_operation_duration( 

2085 command_name=command_name, 

2086 duration_seconds=time.monotonic() - start_time, 

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

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

2089 db_namespace=str(conn.db), 

2090 error=error, 

2091 retry_attempts=failure_count, 

2092 ) 

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

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

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

2096 # indicates the user should retry this transaction. 

2097 if self.watching: 

2098 raise WatchError( 

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

2100 ) 

2101 

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

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

2104 stack = self.command_stack 

2105 if not stack and not self.watching: 

2106 return [] 

2107 if self.scripts: 

2108 await self.load_scripts() 

2109 if self.is_transaction or self.explicit_transaction: 

2110 execute = self._execute_transaction 

2111 operation_name = "MULTI" 

2112 else: 

2113 execute = self._execute_pipeline 

2114 operation_name = "PIPELINE" 

2115 

2116 conn = self.connection 

2117 if not conn: 

2118 conn = await self.connection_pool.get_connection() 

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

2120 # back to the pool after we're done 

2121 self.connection = conn 

2122 conn = cast(Connection, conn) 

2123 

2124 # Start timing for observability 

2125 start_time = time.monotonic() 

2126 # Track actual retry attempts for error reporting 

2127 actual_retry_attempts = 0 

2128 

2129 def failure_callback(error, failure_count): 

2130 nonlocal actual_retry_attempts 

2131 actual_retry_attempts = failure_count 

2132 return self._disconnect_raise_on_watching( 

2133 conn, error, failure_count, start_time, operation_name 

2134 ) 

2135 

2136 try: 

2137 response = await conn.retry.call_with_retry( 

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

2139 failure_callback, 

2140 with_failure_count=True, 

2141 ) 

2142 

2143 await record_operation_duration( 

2144 command_name=operation_name, 

2145 duration_seconds=time.monotonic() - start_time, 

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

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

2148 db_namespace=str(conn.db), 

2149 ) 

2150 return response 

2151 except Exception as e: 

2152 await record_error_count( 

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

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

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

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

2157 error_type=e, 

2158 retry_attempts=actual_retry_attempts, 

2159 is_internal=False, 

2160 ) 

2161 raise 

2162 finally: 

2163 await self.reset() 

2164 

2165 async def discard(self): 

2166 """Flushes all previously queued commands 

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

2168 """ 

2169 await self.execute_command("DISCARD") 

2170 

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

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

2173 if self.explicit_transaction: 

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

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

2176 

2177 async def unwatch(self): 

2178 """Unwatches all previously specified keys""" 

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