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

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

856 statements  

1import copy 

2import logging 

3import re 

4import threading 

5import time 

6from itertools import chain 

7from typing import ( 

8 TYPE_CHECKING, 

9 Any, 

10 Callable, 

11 Dict, 

12 Iterable, 

13 List, 

14 Literal, 

15 Mapping, 

16 Optional, 

17 Sequence, 

18 Set, 

19 Type, 

20 Union, 

21) 

22 

23from redis import _himport_exec 

24from redis._defaults import ( 

25 DEFAULT_RETRY_BASE, 

26 DEFAULT_RETRY_CAP, 

27 DEFAULT_RETRY_COUNT, 

28 DEFAULT_SOCKET_CONNECT_TIMEOUT, 

29 DEFAULT_SOCKET_READ_SIZE, 

30 DEFAULT_SOCKET_TIMEOUT, 

31) 

32from redis._parsers.encoders import Encoder 

33from redis._parsers.helpers import bool_ok, get_response_callbacks 

34from redis.backoff import ExponentialWithJitterBackoff 

35from redis.cache import CacheConfig, CacheInterface 

36from redis.commands import ( 

37 CoreCommands, 

38 RedisModuleCommands, 

39 SentinelCommands, 

40 list_or_args, 

41) 

42from redis.commands.core import Script 

43from redis.commands.helpers import parse_pubsub_subscriptions, pubsub_subscription_args 

44from redis.commands.metadata import MetadataResolver 

45from redis.connection import ( 

46 AbstractConnection, 

47 Connection, 

48 ConnectionPool, 

49 SSLConnection, 

50 UnixDomainSocketConnection, 

51) 

52from redis.credentials import CredentialProvider 

53from redis.driver_info import DriverInfo, resolve_driver_info 

54from redis.event import ( 

55 AfterPooledConnectionsInstantiationEvent, 

56 AfterPubSubConnectionInstantiationEvent, 

57 AfterSingleConnectionInstantiationEvent, 

58 ClientType, 

59 EventDispatcher, 

60) 

61from redis.exceptions import ( 

62 ConnectionError, 

63 ExecAbortError, 

64 PubSubError, 

65 RedisError, 

66 ResponseError, 

67 WatchError, 

68) 

69from redis.himport import HImportRegistry, parse_himport_set_args 

70from redis.lock import Lock 

71from redis.maint_notifications import ( 

72 MaintNotificationsConfig, 

73 OSSMaintNotificationsHandler, 

74) 

75from redis.observability.attributes import PubSubDirection 

76from redis.observability.recorder import ( 

77 record_error_count, 

78 record_operation_duration, 

79 record_pubsub_message, 

80) 

81from redis.retry import Retry 

82from redis.typing import ( 

83 ChannelT, 

84 FieldT, 

85 PubSubHandler, 

86 Subscription, 

87) 

88from redis.utils import ( 

89 SENTINEL, 

90 _set_info_logger, 

91 check_protocol_version, 

92 deprecated_args, 

93 experimental_method, 

94 safe_str, 

95 str_if_bytes, 

96 truncate_text, 

97) 

98 

99if TYPE_CHECKING: 

100 import ssl 

101 

102 import OpenSSL 

103 

104 from redis.keyspace_notifications import KeyspaceNotifications 

105 

106SYM_EMPTY = b"" 

107EMPTY_RESPONSE = "EMPTY_RESPONSE" 

108 

109# some responses (ie. dump) are binary, and just meant to never be decoded 

110NEVER_DECODE = "NEVER_DECODE" 

111 

112 

113logger = logging.getLogger(__name__) 

114 

115 

116def is_debug_log_enabled(): 

117 return logger.isEnabledFor(logging.DEBUG) 

118 

119 

120def add_debug_log_for_operation_failure( 

121 connection: AbstractConnection, 

122 error: BaseException | None = None, 

123 args: Sequence[Any] | None = None, 

124): 

125 details = connection.extract_connection_details() if connection else "no connection" 

126 prefix = ( 

127 f"{type(error).__name__} received" if error is not None else "Operation failed" 

128 ) 

129 # Log only the command name - argument values can carry secrets 

130 # (AUTH, CONFIG SET requirepass, ACL SETUSER) or user data. 

131 command = f" for command {safe_str(args[0])}" if args else "" 

132 suffix = f", error: {error}" if error is not None else "" 

133 logger.debug( 

134 f"{prefix}{command}, with connection: {connection}, details: {details}{suffix}", 

135 ) 

136 

137 

138class CaseInsensitiveDict(dict): 

139 "Case insensitive dict implementation. Assumes string keys only." 

140 

141 def __init__(self, data: Dict[str, str]) -> None: 

142 for k, v in data.items(): 

143 self[k.upper()] = v 

144 

145 def __contains__(self, k): 

146 return super().__contains__(k.upper()) 

147 

148 def __delitem__(self, k): 

149 super().__delitem__(k.upper()) 

150 

151 def __getitem__(self, k): 

152 return super().__getitem__(k.upper()) 

153 

154 def get(self, k, default=None): 

155 return super().get(k.upper(), default) 

156 

157 def __setitem__(self, k, v): 

158 super().__setitem__(k.upper(), v) 

159 

160 def update(self, data): 

161 data = CaseInsensitiveDict(data) 

162 super().update(data) 

163 

164 

165class AbstractRedis: 

166 pass 

167 

168 

169class Redis(RedisModuleCommands, CoreCommands, SentinelCommands): 

170 """ 

171 Implementation of the Redis protocol. 

172 

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

174 and an implementation of the Redis protocol. 

175 

176 Pipelines derive from this, implementing how 

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

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

179 Connection object to talk to redis. 

180 

181 It is not safe to pass PubSub or Pipeline objects between threads. 

182 """ 

183 

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

185 _is_async_client: Literal[False] = False 

186 

187 @classmethod 

188 def from_url(cls, url: str, **kwargs) -> "Redis": 

189 """ 

190 Return a Redis client object configured from the given URL 

191 

192 For example:: 

193 

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

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

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

197 

198 Three URL schemes are supported: 

199 

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

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

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

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

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

205 

206 The username, password, hostname and path are passed through 

207 urllib.parse.unquote in order to replace any percent-encoded values 

208 with their corresponding characters. Querystring values are decoded 

209 by urllib.parse.parse_qs and are not unquoted again. 

210 

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

212 found will be used: 

213 

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

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

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

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

218 

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

220 

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

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

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

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

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

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

227 arguments always win. 

228 

229 """ 

230 single_connection_client = kwargs.pop("single_connection_client", False) 

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

232 client = cls( 

233 connection_pool=connection_pool, 

234 single_connection_client=single_connection_client, 

235 ) 

236 client.auto_close_connection_pool = True 

237 return client 

238 

239 @classmethod 

240 def from_pool( 

241 cls: Type["Redis"], 

242 connection_pool: ConnectionPool, 

243 ) -> "Redis": 

244 """ 

245 Return a Redis client from the given connection pool. 

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

247 close it when the Redis client is closed. 

248 

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

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

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

252 via ``from_pool`` -- for example one per request across threads -- is 

253 not thread safe: when one client is closed it will disconnect 

254 connections still in use by the others. 

255 

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

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

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

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

260 safely shared across clients. ``ConnectionPool`` supports the context 

261 manager protocol for this:: 

262 

263 with ConnectionPool.from_url(url) as pool: 

264 r = Redis(connection_pool=pool) 

265 """ 

266 client = cls( 

267 connection_pool=connection_pool, 

268 ) 

269 client.auto_close_connection_pool = True 

270 return client 

271 

272 @deprecated_args( 

273 args_to_warn=["retry_on_timeout"], 

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

275 version="6.0.0", 

276 ) 

277 @deprecated_args( 

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

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

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

281 ) 

282 def __init__( 

283 self, 

284 host: str = "localhost", 

285 port: int = 6379, 

286 db: int = 0, 

287 password: str | None = None, 

288 socket_timeout: float | None = DEFAULT_SOCKET_TIMEOUT, 

289 socket_connect_timeout: float | None = DEFAULT_SOCKET_CONNECT_TIMEOUT, 

290 socket_read_size: int = DEFAULT_SOCKET_READ_SIZE, 

291 socket_keepalive: bool | None = True, 

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

293 connection_pool: ConnectionPool | None = None, 

294 unix_socket_path: str | None = None, 

295 encoding: str = "utf-8", 

296 encoding_errors: str = "strict", 

297 decode_responses: bool = False, 

298 retry_on_timeout: bool = False, 

299 retry: Retry = Retry( 

300 backoff=ExponentialWithJitterBackoff( 

301 base=DEFAULT_RETRY_BASE, cap=DEFAULT_RETRY_CAP 

302 ), 

303 retries=DEFAULT_RETRY_COUNT, 

304 ), 

305 retry_on_error: List[Type[Exception]] | None = None, 

306 ssl: bool = False, 

307 ssl_keyfile: str | None = None, 

308 ssl_certfile: str | None = None, 

309 ssl_cert_reqs: "str | ssl.VerifyMode" = "required", 

310 ssl_include_verify_flags: List["ssl.VerifyFlags"] | None = None, 

311 ssl_exclude_verify_flags: List["ssl.VerifyFlags"] | None = None, 

312 ssl_ca_certs: str | None = None, 

313 ssl_ca_path: str | None = None, 

314 ssl_ca_data: str | None = None, 

315 ssl_check_hostname: bool = True, 

316 ssl_password: str | None = None, 

317 ssl_validate_ocsp: bool = False, 

318 ssl_validate_ocsp_stapled: bool = False, 

319 ssl_ocsp_context: "OpenSSL.SSL.Context | None" = None, 

320 ssl_ocsp_expected_cert: str | None = None, 

321 ssl_min_version: "ssl.TLSVersion | None" = None, 

322 ssl_ciphers: str | None = None, 

323 max_connections: int | None = None, 

324 single_connection_client: bool = False, 

325 health_check_interval: int = 0, 

326 client_name: str | None = None, 

327 lib_name: str | object | None = SENTINEL, 

328 lib_version: str | object | None = SENTINEL, 

329 driver_info: DriverInfo | object | None = SENTINEL, 

330 username: str | None = None, 

331 redis_connect_func: Callable[[], None] | None = None, 

332 credential_provider: CredentialProvider | None = None, 

333 protocol: int | None = None, 

334 legacy_responses: bool = True, 

335 cache: CacheInterface | None = None, 

336 cache_config: CacheConfig | None = None, 

337 event_dispatcher: EventDispatcher | None = None, 

338 maint_notifications_config: MaintNotificationsConfig | None = None, 

339 oss_cluster_maint_notifications_handler: OSSMaintNotificationsHandler 

340 | None = None, 

341 metadata_resolver: MetadataResolver | None = None, 

342 ) -> None: 

343 """ 

344 Initialize a new Redis client. 

345 

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

347 

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

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

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

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

352 errors specified in `retry_on_error`. 

353 

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

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

356 the errors on which retries will happen. 

357 

358 `retry_on_timeout` is deprecated - please include the TimeoutError 

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

360 

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

362 provided pool will be used. 

363 

364 Args: 

365 

366 socket_keepalive: 

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

368 Argument is ignored when connection_pool is provided. 

369 socket_keepalive_options: 

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

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

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

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

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

375 avoid setting additional TCP keepalive options. Argument is ignored 

376 when connection_pool is provided. 

377 single_connection_client: 

378 if `True`, connection pool is not used. In that case `Redis` 

379 instance use is not thread safe. 

380 decode_responses: 

381 if `True`, the response will be decoded to utf-8. 

382 Argument is ignored when connection_pool is provided. 

383 driver_info: 

384 Optional DriverInfo object to identify upstream libraries. 

385 If provided, lib_name and lib_version are ignored. 

386 If not provided, a DriverInfo will be created from lib_name and lib_version. 

387 Explicit None disables CLIENT SETINFO. 

388 Argument is ignored when connection_pool is provided. 

389 lib_name: 

390 **Deprecated.** Use driver_info instead. Library name for CLIENT SETINFO. 

391 lib_version: 

392 **Deprecated.** Use driver_info instead. Library version for CLIENT SETINFO. 

393 maint_notifications_config: 

394 configures the pool to support maintenance notifications - see 

395 `redis.maint_notifications.MaintNotificationsConfig` for details. 

396 Only supported with RESP3 

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

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

399 initialization). 

400 Argument is ignored when connection_pool is provided. 

401 oss_cluster_maint_notifications_handler: 

402 handler for OSS cluster notifications - see 

403 `redis.maint_notifications.OSSMaintNotificationsHandler` for details. 

404 Only supported with RESP3 

405 Argument is ignored when connection_pool is provided. 

406 metadata_resolver: 

407 decides which commands are eligible for client-side caching - see 

408 `redis.commands.metadata.MetadataResolver`. Defaults to the static command 

409 metadata this library ships. Resolvers chain through `with_fallback`, first match 

410 wins, so one placed in front of `StaticMetadataResolver` overrides the commands it 

411 carries while the static records answer for everything else. 

412 The library never reads `COMMAND` on its own behalf for this, so the default adds 

413 no round trips. To decide eligibility from the connected server, pass a 

414 `DynamicMetadataResolver` built from a live `COMMAND` reply - use it with care, 

415 because reading that reply relies on a class in the private `redis._parsers` 

416 package. 

417 Argument is ignored when connection_pool is provided - configure it on the pool 

418 instead. 

419 """ 

420 if event_dispatcher is None: 

421 self._event_dispatcher = EventDispatcher() 

422 else: 

423 self._event_dispatcher = event_dispatcher 

424 if not connection_pool: 

425 if not retry_on_error: 

426 retry_on_error = [] 

427 

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

429 computed_driver_info = resolve_driver_info( 

430 driver_info, lib_name, lib_version 

431 ) 

432 

433 kwargs = { 

434 "db": db, 

435 "username": username, 

436 "password": password, 

437 "socket_timeout": socket_timeout, 

438 "socket_read_size": socket_read_size, 

439 "encoding": encoding, 

440 "encoding_errors": encoding_errors, 

441 "decode_responses": decode_responses, 

442 "retry_on_error": retry_on_error, 

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

444 "max_connections": max_connections, 

445 "health_check_interval": health_check_interval, 

446 "client_name": client_name, 

447 "driver_info": computed_driver_info, 

448 "redis_connect_func": redis_connect_func, 

449 "credential_provider": credential_provider, 

450 "protocol": protocol, 

451 "legacy_responses": legacy_responses, 

452 } 

453 # based on input, setup appropriate connection args 

454 if unix_socket_path is not None: 

455 if ( 

456 maint_notifications_config 

457 and maint_notifications_config.enabled is True 

458 ): 

459 raise RedisError( 

460 "Maintenance notifications are not supported with Unix " 

461 "domain socket connections" 

462 ) 

463 kwargs.update( 

464 { 

465 "path": unix_socket_path, 

466 "connection_class": UnixDomainSocketConnection, 

467 "maint_notifications_config": MaintNotificationsConfig( 

468 enabled=False 

469 ), 

470 } 

471 ) 

472 else: 

473 # TCP specific options 

474 kwargs.update( 

475 { 

476 "host": host, 

477 "port": port, 

478 "socket_connect_timeout": socket_connect_timeout, 

479 "socket_keepalive": socket_keepalive, 

480 "socket_keepalive_options": socket_keepalive_options, 

481 } 

482 ) 

483 

484 if ssl: 

485 kwargs.update( 

486 { 

487 "connection_class": SSLConnection, 

488 "ssl_keyfile": ssl_keyfile, 

489 "ssl_certfile": ssl_certfile, 

490 "ssl_cert_reqs": ssl_cert_reqs, 

491 "ssl_include_verify_flags": ssl_include_verify_flags, 

492 "ssl_exclude_verify_flags": ssl_exclude_verify_flags, 

493 "ssl_ca_certs": ssl_ca_certs, 

494 "ssl_ca_data": ssl_ca_data, 

495 "ssl_check_hostname": ssl_check_hostname, 

496 "ssl_password": ssl_password, 

497 "ssl_ca_path": ssl_ca_path, 

498 "ssl_validate_ocsp_stapled": ssl_validate_ocsp_stapled, 

499 "ssl_validate_ocsp": ssl_validate_ocsp, 

500 "ssl_ocsp_context": ssl_ocsp_context, 

501 "ssl_ocsp_expected_cert": ssl_ocsp_expected_cert, 

502 "ssl_min_version": ssl_min_version, 

503 "ssl_ciphers": ssl_ciphers, 

504 } 

505 ) 

506 if (cache_config or cache) and check_protocol_version(protocol, 3): 

507 kwargs.update( 

508 { 

509 "cache": cache, 

510 "cache_config": cache_config, 

511 } 

512 ) 

513 if metadata_resolver is not None: 

514 # A named ``ConnectionPool`` parameter, not a connection kwarg, so it 

515 # reaches the pool without also being forwarded to every connection. 

516 kwargs.update( 

517 { 

518 "metadata_resolver": metadata_resolver, 

519 } 

520 ) 

521 maint_notifications_enabled = ( 

522 maint_notifications_config and maint_notifications_config.enabled 

523 ) 

524 if maint_notifications_enabled and not check_protocol_version( 

525 protocol, 3 

526 ): 

527 raise RedisError( 

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

529 ) 

530 if maint_notifications_config: 

531 kwargs.update( 

532 { 

533 "maint_notifications_config": maint_notifications_config, 

534 } 

535 ) 

536 if oss_cluster_maint_notifications_handler: 

537 kwargs.update( 

538 { 

539 "oss_cluster_maint_notifications_handler": oss_cluster_maint_notifications_handler, 

540 } 

541 ) 

542 connection_pool = ConnectionPool(**kwargs) 

543 self._event_dispatcher.dispatch( 

544 AfterPooledConnectionsInstantiationEvent( 

545 [connection_pool], ClientType.SYNC, credential_provider 

546 ) 

547 ) 

548 self.auto_close_connection_pool = True 

549 else: 

550 self.auto_close_connection_pool = False 

551 self._event_dispatcher.dispatch( 

552 AfterPooledConnectionsInstantiationEvent( 

553 [connection_pool], ClientType.SYNC, credential_provider 

554 ) 

555 ) 

556 

557 self.connection_pool = connection_pool 

558 

559 if (cache_config or cache) and not check_protocol_version( 

560 self.connection_pool.get_protocol(), 3 

561 ): 

562 raise RedisError("Client caching is only supported with RESP version 3") 

563 

564 self.single_connection_lock = threading.RLock() 

565 self.connection = None 

566 self._single_connection_client = single_connection_client 

567 if self._single_connection_client: 

568 self.connection = self.connection_pool.get_connection() 

569 self._event_dispatcher.dispatch( 

570 AfterSingleConnectionInstantiationEvent( 

571 self.connection, ClientType.SYNC, self.single_connection_lock 

572 ) 

573 ) 

574 

575 connection_kwargs = self.connection_pool.connection_kwargs 

576 self.response_callbacks = CaseInsensitiveDict( 

577 get_response_callbacks( 

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

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

580 ) 

581 ) 

582 

583 def __repr__(self) -> str: 

584 return ( 

585 f"<{type(self).__module__}.{type(self).__name__}" 

586 f"({repr(self.connection_pool)})>" 

587 ) 

588 

589 def get_encoder(self) -> "Encoder": 

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

591 return self.connection_pool.get_encoder() 

592 

593 def get_connection_kwargs(self) -> Dict: 

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

595 return self.connection_pool.connection_kwargs 

596 

597 @property 

598 def himport_registry(self) -> HImportRegistry: 

599 """The client's HIMPORT fieldset registry (contains empty 

600 schema registry if none was declared). 

601 

602 Read-only: the registry is mutated only through the HIMPORT command methods. 

603 """ 

604 return self.connection_pool.himport_registry 

605 

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

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

608 

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

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

611 self.connection_pool.set_retry(retry) 

612 

613 def set_response_callback(self, command: str, callback: Callable) -> None: 

614 """Set a custom Response Callback""" 

615 self.response_callbacks[command] = callback 

616 

617 def load_external_module(self, funcname, func) -> None: 

618 """ 

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

620 and their namespaces to the redis client. 

621 

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

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

624 

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

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

627 To load function functions into this namespace: 

628 

629 from redis import Redis 

630 from foomodule import F 

631 r = Redis() 

632 r.load_external_module("foo", F) 

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

634 

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

636 tests/test_connection.py::test_loading_external_modules 

637 """ 

638 setattr(self, funcname, func) 

639 

640 def pipeline(self, transaction=True, shard_hint=None) -> "Pipeline": 

641 """ 

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

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

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

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

646 between the client and server. 

647 """ 

648 return Pipeline( 

649 self.connection_pool, self.response_callbacks, transaction, shard_hint 

650 ) 

651 

652 def transaction( 

653 self, func: Callable[["Pipeline"], None], *watches, **kwargs 

654 ) -> Union[List[Any], Any, None]: 

655 """ 

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

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

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

659 """ 

660 shard_hint = kwargs.pop("shard_hint", None) 

661 value_from_callable = kwargs.pop("value_from_callable", False) 

662 watch_delay = kwargs.pop("watch_delay", None) 

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

664 while True: 

665 try: 

666 if watches: 

667 pipe.watch(*watches) 

668 func_value = func(pipe) 

669 exec_value = pipe.execute() 

670 return func_value if value_from_callable else exec_value 

671 except WatchError: 

672 if watch_delay is not None and watch_delay > 0: 

673 time.sleep(watch_delay) 

674 continue 

675 

676 def lock( 

677 self, 

678 name: str, 

679 timeout: Optional[float] = None, 

680 sleep: float = 0.1, 

681 blocking: bool = True, 

682 blocking_timeout: Optional[float] = None, 

683 lock_class: Union[None, Any] = None, 

684 thread_local: bool = True, 

685 raise_on_release_error: bool = True, 

686 ): 

687 """ 

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

689 the behavior of threading.Lock. 

690 

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

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

693 

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

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

696 holding the lock. 

697 

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

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

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

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

702 argument to ``acquire``. 

703 

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

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

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

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

708 

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

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

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

712 you have created your own custom lock class. 

713 

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

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

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

717 another thread. Consider the following timeline: 

718 

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

720 thread-1 sets the token to "abc" 

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

722 Lock instance. 

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

724 key. 

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

726 thread-2 sets the token to "xyz" 

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

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

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

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

731 

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

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

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

735 will be logged and the exception will be suppressed. 

736 

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

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

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

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

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

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

743 thread local storage.""" 

744 if lock_class is None: 

745 lock_class = Lock 

746 return lock_class( 

747 self, 

748 name, 

749 timeout=timeout, 

750 sleep=sleep, 

751 blocking=blocking, 

752 blocking_timeout=blocking_timeout, 

753 thread_local=thread_local, 

754 raise_on_release_error=raise_on_release_error, 

755 ) 

756 

757 def pubsub(self, **kwargs): 

758 """ 

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

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

761 them. 

762 """ 

763 return PubSub( 

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

765 ) 

766 

767 def keyspace_notifications( 

768 self, 

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

770 ignore_subscribe_messages: bool = True, 

771 ) -> "KeyspaceNotifications": 

772 """ 

773 Return a :class:`~redis.keyspace_notifications.KeyspaceNotifications` 

774 object for subscribing to keyspace and keyevent notifications. 

775 

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

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

778 

779 Args: 

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

781 notifications. 

782 ignore_subscribe_messages: If True, subscribe/unsubscribe 

783 confirmations are not returned by 

784 get_message/listen. 

785 """ 

786 from redis.keyspace_notifications import KeyspaceNotifications 

787 

788 return KeyspaceNotifications( 

789 self, 

790 key_prefix=key_prefix, 

791 ignore_subscribe_messages=ignore_subscribe_messages, 

792 ) 

793 

794 def monitor(self): 

795 return Monitor(self.connection_pool) 

796 

797 def client(self): 

798 return self.__class__( 

799 connection_pool=self.connection_pool, 

800 single_connection_client=True, 

801 ) 

802 

803 def __enter__(self): 

804 return self 

805 

806 def __exit__(self, exc_type, exc_value, traceback): 

807 self.close() 

808 

809 def __del__(self): 

810 try: 

811 self.close() 

812 except Exception: 

813 pass 

814 

815 def close(self) -> None: 

816 # In case a connection property does not yet exist 

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

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

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

820 return 

821 

822 conn = self.connection 

823 if conn: 

824 self.connection = None 

825 self.connection_pool.release(conn) 

826 

827 if self.auto_close_connection_pool: 

828 self.connection_pool.close() 

829 

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

831 """ 

832 Send a command and parse the response 

833 """ 

834 # HIMPORT SET is the one command whose wire form depends on per-connection 

835 # state: the fieldset must be PREPAREd on this connection first, and any 

836 # fieldset discarded since this connection last reconciled must be dropped. 

837 # Handling it here (rather than in himport_set) lets himport_set reuse the 

838 # full execute_command machinery — retry, disconnect-on-error, pooling — so 

839 # a failed HIMPORT SET disconnects the connection like any other command. 

840 # This per-command branch in the hot dispatch path is deliberate and has no 

841 # cleaner alternative: this is the only seam where the concrete borrowed 

842 # connection is known, and connection-scoped session setup can only happen 

843 # once that connection is chosen. The overhead is one string compare per 

844 # command. 

845 himport_set = parse_himport_set_args(args) 

846 if himport_set is not None: 

847 # ``args`` is an HIMPORT SET in either the joined ("HIMPORT SET", key, 

848 # ...) or split ("HIMPORT", "SET", key, ...) raw form; the operands come 

849 # back at the right offsets for the form. A command with too few operands 

850 # returns None and falls through to the normal send path so the server 

851 # returns its arity error instead of a client-side IndexError here. 

852 key, fieldset_name, values = himport_set 

853 return self._himport_execute_set(conn, key, fieldset_name, values) 

854 conn.send_command(*args, **options) 

855 return self.parse_response(conn, command_name, **options) 

856 

857 def _himport_reconcile_discards(self, conn): 

858 """Delegate to the shared sync HIMPORT executor.""" 

859 return _himport_exec.reconcile_discards(self, conn) 

860 

861 def _himport_prepare_and_set(self, conn, key, fieldset_name, values, fieldset): 

862 """Delegate to the shared sync HIMPORT executor.""" 

863 return _himport_exec.prepare_and_set( 

864 self, conn, key, fieldset_name, values, fieldset 

865 ) 

866 

867 def _himport_execute_set(self, conn, key, fieldset_name, values): 

868 """Delegate to the shared sync HIMPORT executor.""" 

869 return _himport_exec.execute_set(self, conn, key, fieldset_name, values) 

870 

871 def _close_connection( 

872 self, 

873 conn, 

874 error: Optional[Exception] = None, 

875 failure_count: Optional[int] = None, 

876 start_time: Optional[float] = None, 

877 command_name: Optional[str] = None, 

878 ) -> None: 

879 """ 

880 Close the connection before retrying. 

881 

882 The supported exceptions are already checked in the 

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

884 

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

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

887 """ 

888 if error and failure_count <= conn.retry.get_retries(): 

889 record_operation_duration( 

890 command_name=command_name, 

891 duration_seconds=time.monotonic() - start_time, 

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

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

894 db_namespace=str(conn.db), 

895 error=error, 

896 retry_attempts=failure_count, 

897 ) 

898 

899 conn.disconnect() 

900 

901 # COMMAND EXECUTION AND PROTOCOL PARSING 

902 def execute_command(self, *args, **options): 

903 return self._execute_command(*args, **options) 

904 

905 def _execute_command(self, *args, **options): 

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

907 pool = self.connection_pool 

908 command_name = args[0] 

909 conn = self.connection or pool.get_connection() 

910 

911 # Start timing for observability 

912 start_time = time.monotonic() 

913 # Track actual retry attempts for error reporting 

914 actual_retry_attempts = [0] 

915 

916 def failure_callback(error, failure_count): 

917 if is_debug_log_enabled(): 

918 add_debug_log_for_operation_failure(conn, error, args) 

919 actual_retry_attempts[0] = failure_count 

920 self._close_connection(conn, error, failure_count, start_time, command_name) 

921 

922 if self._single_connection_client: 

923 self.single_connection_lock.acquire() 

924 try: 

925 result = conn.retry.call_with_retry( 

926 lambda: self._send_command_parse_response( 

927 conn, command_name, *args, **options 

928 ), 

929 failure_callback, 

930 with_failure_count=True, 

931 ) 

932 

933 record_operation_duration( 

934 command_name=command_name, 

935 duration_seconds=time.monotonic() - start_time, 

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

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

938 db_namespace=str(conn.db), 

939 ) 

940 return result 

941 except Exception as e: 

942 record_error_count( 

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

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

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

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

947 error_type=e, 

948 retry_attempts=actual_retry_attempts[0], 

949 is_internal=False, 

950 ) 

951 raise 

952 

953 finally: 

954 try: 

955 if conn and conn.should_reconnect(): 

956 self._close_connection(conn) 

957 conn.connect() 

958 finally: 

959 if self._single_connection_client: 

960 self.single_connection_lock.release() 

961 if not self.connection: 

962 pool.release(conn) 

963 

964 def parse_response(self, connection, command_name, **options): 

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

966 try: 

967 if NEVER_DECODE in options: 

968 response = connection.read_response(disable_decoding=True) 

969 options.pop(NEVER_DECODE) 

970 else: 

971 response = connection.read_response() 

972 except ResponseError: 

973 if EMPTY_RESPONSE in options: 

974 return options[EMPTY_RESPONSE] 

975 raise 

976 

977 if EMPTY_RESPONSE in options: 

978 options.pop(EMPTY_RESPONSE) 

979 

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

981 options.pop("keys", None) 

982 

983 if command_name in self.response_callbacks: 

984 return self.response_callbacks[command_name](response, **options) 

985 return response 

986 

987 def get_cache(self) -> Optional[CacheInterface]: 

988 return self.connection_pool.cache 

989 

990 # HIMPORT orchestration. The registry lives on the shared HImportRegistry; the 

991 # server-side effect is applied lazily per connection (PREPARE bundled into the 

992 # first himport_set; DISCARD reconciled when a connection is next borrowed for a 

993 # himport_set). The connection carries the per-connection HIMPORT state; a 

994 # CacheProxyConnection transparently delegates it to the wrapped connection, so 

995 # this code never needs to know which connection type it holds. 

996 

997 @experimental_method() 

998 def himport_prepare(self, fieldset_name: str, fields: Iterable[FieldT]) -> bool: 

999 """Declare an HIMPORT fieldset for use by :meth:`himport_set`. 

1000 

1001 Registers ``fieldset_name`` (ordered ``fields``, verbatim) in the client's 

1002 shared registry. On a pooled client the server-side ``PREPARE`` is deferred 

1003 and bundled into the next ``himport_set`` per connection. On a single 

1004 connection client it is run immediately when the pinned connection is live; 

1005 while that connection is not connected there is no session state to prepare, 

1006 so the next ``himport_set`` prepares it lazily instead. 

1007 """ 

1008 fieldset = self.himport_registry.prepare(fieldset_name, fields) 

1009 conn = self.connection 

1010 if self._single_connection_client and conn is not None and conn.is_connected: 

1011 self.himport_prepare_internal(fieldset_name, fieldset.fields) 

1012 conn._himport_prepared[fieldset_name] = fieldset.version 

1013 return True 

1014 

1015 @experimental_method() 

1016 def himport_discard(self, fieldset_name: str) -> int: 

1017 """Remove a fieldset from the registry. 

1018 

1019 Returns ``1`` if it was registered, ``0`` otherwise. On a pooled client the 

1020 server-side ``DISCARD`` is reconciled lazily when each connection is next 

1021 used for ``himport_set``. On a single connection client it runs immediately 

1022 on the pinned connection when it is live; while that connection is not 

1023 connected there is nothing prepared on the server to discard (its tracking is 

1024 reset on connect), so no server call is made. 

1025 """ 

1026 removed = self.himport_registry.discard(fieldset_name) 

1027 conn = self.connection 

1028 if self._single_connection_client and conn is not None and conn.is_connected: 

1029 if removed: 

1030 self.himport_discard_internal(fieldset_name) 

1031 conn._himport_prepared.pop(fieldset_name, None) 

1032 conn._himport_reconciled_revision = self.himport_registry.revision 

1033 return 1 if removed else 0 

1034 

1035 @experimental_method() 

1036 def himport_discard_all(self) -> int: 

1037 """Remove all fieldsets from the registry. 

1038 

1039 Returns the number removed from the registry. Server-side removal follows the 

1040 same live/lazy rule as :meth:`himport_discard`. 

1041 """ 

1042 count = self.himport_registry.discard_all() 

1043 conn = self.connection 

1044 if self._single_connection_client and conn is not None and conn.is_connected: 

1045 if count: 

1046 self.himport_discard_all_internal() 

1047 conn._himport_prepared.clear() 

1048 conn._himport_reconciled_revision = self.himport_registry.revision 

1049 return count 

1050 

1051 

1052StrictRedis = Redis 

1053 

1054 

1055class Monitor: 

1056 """ 

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

1058 next_command() method returns one command from monitor 

1059 listen() method yields commands from monitor. 

1060 """ 

1061 

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

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

1064 

1065 def __init__(self, connection_pool): 

1066 self.connection_pool = connection_pool 

1067 self.connection = self.connection_pool.get_connection() 

1068 

1069 def __enter__(self): 

1070 self._start_monitor() 

1071 return self 

1072 

1073 def __exit__(self, *args): 

1074 self.connection.disconnect() 

1075 self.connection_pool.release(self.connection) 

1076 

1077 def next_command(self): 

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

1079 response = self.connection.read_response() 

1080 

1081 if response is None: 

1082 return None 

1083 

1084 if isinstance(response, bytes): 

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

1086 

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

1088 m = self.monitor_re.match(command_data) 

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

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

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

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

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

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

1095 

1096 if client_info == "lua": 

1097 client_address = "lua" 

1098 client_port = "" 

1099 client_type = "lua" 

1100 elif client_info.startswith("unix"): 

1101 client_address = "unix" 

1102 client_port = client_info[5:] 

1103 client_type = "unix" 

1104 else: 

1105 if client_info == "": 

1106 client_address = "" 

1107 client_port = "" 

1108 client_type = "unknown" 

1109 else: 

1110 # use rsplit as ipv6 addresses contain colons 

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

1112 client_type = "tcp" 

1113 return { 

1114 "time": float(command_time), 

1115 "db": int(db_id), 

1116 "client_address": client_address, 

1117 "client_port": client_port, 

1118 "client_type": client_type, 

1119 "command": command, 

1120 } 

1121 

1122 def listen(self): 

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

1124 while True: 

1125 yield self.next_command() 

1126 

1127 def _start_monitor(self): 

1128 self.connection.send_command("MONITOR") 

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

1130 response = self.connection.read_response() 

1131 

1132 if not bool_ok(response): 

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

1134 

1135 

1136class PubSub: 

1137 """ 

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

1139 

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

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

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

1143 """ 

1144 

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

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

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

1148 

1149 def __init__( 

1150 self, 

1151 connection_pool, 

1152 shard_hint=None, 

1153 ignore_subscribe_messages: bool = False, 

1154 encoder: Optional["Encoder"] = None, 

1155 push_handler_func: Union[None, Callable[[str], None]] = None, 

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

1157 ): 

1158 self.connection_pool = connection_pool 

1159 self.shard_hint = shard_hint 

1160 self.ignore_subscribe_messages = ignore_subscribe_messages 

1161 self.connection = None 

1162 self.subscribed_event = threading.Event() 

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

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

1165 self.encoder = encoder 

1166 self.push_handler_func = push_handler_func 

1167 if event_dispatcher is None: 

1168 self._event_dispatcher = EventDispatcher() 

1169 else: 

1170 self._event_dispatcher = event_dispatcher 

1171 

1172 self._lock = threading.RLock() 

1173 if self.encoder is None: 

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

1175 self.health_check_response_b = self.encoder.encode(self.HEALTH_CHECK_MESSAGE) 

1176 if self.encoder.decode_responses: 

1177 self.health_check_response = ["pong", self.HEALTH_CHECK_MESSAGE] 

1178 else: 

1179 self.health_check_response = [b"pong", self.health_check_response_b] 

1180 if self.push_handler_func is None: 

1181 _set_info_logger() 

1182 self.reset() 

1183 

1184 def __enter__(self) -> "PubSub": 

1185 return self 

1186 

1187 def __exit__(self, exc_type, exc_value, traceback) -> None: 

1188 self.reset() 

1189 

1190 def __del__(self) -> None: 

1191 try: 

1192 # if this object went out of scope prior to shutting down 

1193 # subscriptions, close the connection manually before 

1194 # returning it to the connection pool 

1195 self.reset() 

1196 except Exception: 

1197 pass 

1198 

1199 def reset(self) -> None: 

1200 if self.connection: 

1201 self.connection.disconnect() 

1202 self.connection.deregister_connect_callback(self.on_connect) 

1203 self.connection_pool.release(self.connection) 

1204 self.connection = None 

1205 self.health_check_response_counter = 0 

1206 self.channels = {} 

1207 self.pending_unsubscribe_channels = set() 

1208 self.shard_channels = {} 

1209 self.pending_unsubscribe_shard_channels = set() 

1210 self.patterns = {} 

1211 self.pending_unsubscribe_patterns = set() 

1212 self.subscribed_event.clear() 

1213 

1214 def close(self) -> None: 

1215 self.reset() 

1216 

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

1218 # Replay handler-backed subscriptions as positional Subscription objects 

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

1220 subscriptions = pubsub_subscription_args(subscribed) 

1221 if subscriptions: 

1222 subscribe_fn(*subscriptions) 

1223 

1224 def _resubscribe_shard_channels(self) -> None: 

1225 self._resubscribe(self.shard_channels, self.ssubscribe) 

1226 

1227 def on_connect(self, connection) -> None: 

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

1229 self.pending_unsubscribe_channels.clear() 

1230 self.pending_unsubscribe_patterns.clear() 

1231 self.pending_unsubscribe_shard_channels.clear() 

1232 if self.channels: 

1233 self._resubscribe(self.channels, self.subscribe) 

1234 if self.patterns: 

1235 self._resubscribe(self.patterns, self.psubscribe) 

1236 if self.shard_channels: 

1237 self._resubscribe_shard_channels() 

1238 

1239 @property 

1240 def subscribed(self) -> bool: 

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

1242 return self.subscribed_event.is_set() 

1243 

1244 def execute_command(self, *args): 

1245 """Execute a publish/subscribe command""" 

1246 

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

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

1249 # subscribed to one or more channels 

1250 

1251 if self.connection is None: 

1252 self.connection = self.connection_pool.get_connection() 

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

1254 # were listening to when we were disconnected 

1255 self.connection.register_connect_callback(self.on_connect) 

1256 if self.push_handler_func is not None: 

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

1258 self._event_dispatcher.dispatch( 

1259 AfterPubSubConnectionInstantiationEvent( 

1260 self.connection, self.connection_pool, ClientType.SYNC, self._lock 

1261 ) 

1262 ) 

1263 connection = self.connection 

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

1265 if not self.subscribed: 

1266 self.clean_health_check_responses() 

1267 with self._lock: 

1268 self._execute(connection, connection.send_command, *args, **kwargs) 

1269 

1270 def clean_health_check_responses(self) -> None: 

1271 """ 

1272 If any health check responses are present, clean them 

1273 """ 

1274 ttl = 10 

1275 conn = self.connection 

1276 while conn and self.health_check_response_counter > 0 and ttl > 0: 

1277 if self._execute(conn, conn.can_read, timeout=conn.socket_timeout): 

1278 response = self._execute(conn, conn.read_response) 

1279 if self.is_health_check_response(response): 

1280 self.health_check_response_counter -= 1 

1281 else: 

1282 raise PubSubError( 

1283 "A non health check response was cleaned by " 

1284 "execute_command: {}".format(response) 

1285 ) 

1286 ttl -= 1 

1287 

1288 def _reconnect( 

1289 self, 

1290 conn, 

1291 error: Optional[Exception] = None, 

1292 failure_count: Optional[int] = None, 

1293 start_time: Optional[float] = None, 

1294 command_name: Optional[str] = None, 

1295 ) -> None: 

1296 """ 

1297 The supported exceptions are already checked in the 

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

1299 

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

1301 """ 

1302 if error and failure_count <= conn.retry.get_retries(): 

1303 if command_name: 

1304 record_operation_duration( 

1305 command_name=command_name, 

1306 duration_seconds=time.monotonic() - start_time, 

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

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

1309 db_namespace=str(conn.db), 

1310 error=error, 

1311 retry_attempts=failure_count, 

1312 ) 

1313 conn.disconnect() 

1314 conn.connect() 

1315 

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

1317 """ 

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

1319 this will fail and raise a ConnectionError as desired. 

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

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

1322 patterns we were previously listening to 

1323 """ 

1324 

1325 if conn.should_reconnect(): 

1326 self._reconnect(conn) 

1327 

1328 if not len(args) == 0: 

1329 command_name = args[0] 

1330 else: 

1331 command_name = None 

1332 

1333 # Start timing for observability 

1334 start_time = time.monotonic() 

1335 # Track actual retry attempts for error reporting 

1336 actual_retry_attempts = [0] 

1337 

1338 def failure_callback(error, failure_count): 

1339 if is_debug_log_enabled(): 

1340 add_debug_log_for_operation_failure(conn, error, args) 

1341 actual_retry_attempts[0] = failure_count 

1342 self._reconnect(conn, error, failure_count, start_time, command_name) 

1343 

1344 try: 

1345 response = conn.retry.call_with_retry( 

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

1347 failure_callback, 

1348 with_failure_count=True, 

1349 ) 

1350 

1351 if command_name: 

1352 record_operation_duration( 

1353 command_name=command_name, 

1354 duration_seconds=time.monotonic() - start_time, 

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

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

1357 db_namespace=str(conn.db), 

1358 ) 

1359 

1360 return response 

1361 except Exception as e: 

1362 record_error_count( 

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

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

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

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

1367 error_type=e, 

1368 retry_attempts=actual_retry_attempts[0], 

1369 is_internal=False, 

1370 ) 

1371 raise 

1372 

1373 def parse_response(self, block=True, timeout=0): 

1374 """ 

1375 Parse the response from a publish/subscribe command. 

1376 

1377 Args: 

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

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

1380 Default: True 

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

1382 This parameter is ignored when block=True. 

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

1384 

1385 Returns: 

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

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

1388 

1389 Important: 

1390 The block and timeout parameters work together: 

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

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

1393 

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

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

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

1397 

1398 Example: 

1399 # Block indefinitely (timeout is ignored) 

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

1401 

1402 # Non-blocking with 0.1 second timeout 

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

1404 

1405 # Non-blocking, return immediately 

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

1407 

1408 # Recommended: use get_message() instead 

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

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

1411 """ 

1412 conn = self.connection 

1413 if conn is None: 

1414 raise RuntimeError( 

1415 "pubsub connection not set: " 

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

1417 ) 

1418 

1419 self.check_health() 

1420 

1421 def try_read(): 

1422 if not block: 

1423 if not conn.can_read(timeout=timeout): 

1424 return None 

1425 read_timeout = timeout 

1426 else: 

1427 conn.connect() 

1428 # Block indefinitely waiting for a pubsub message. timeout=None 

1429 # makes the socket layer call sock.settimeout(None) for this read 

1430 # (and restore the original socket_timeout afterwards), so the 

1431 # configured socket_timeout does not abort the read. 

1432 read_timeout = None 

1433 return conn.read_response( 

1434 disconnect_on_error=False, push_request=True, timeout=read_timeout 

1435 ) 

1436 

1437 response = self._execute(conn, try_read) 

1438 

1439 if self.is_health_check_response(response): 

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

1441 self.health_check_response_counter -= 1 

1442 return None 

1443 return response 

1444 

1445 def is_health_check_response(self, response) -> bool: 

1446 """ 

1447 Check if the response is a health check response. 

1448 If there are no subscriptions redis responds to PING command with a 

1449 bulk response, instead of a multi-bulk with "pong" and the response. 

1450 """ 

1451 if self.encoder.decode_responses: 

1452 return ( 

1453 response 

1454 in [ 

1455 self.health_check_response, # If there is a subscription 

1456 self.HEALTH_CHECK_MESSAGE, # If there are no subscriptions and decode_responses=True 

1457 ] 

1458 ) 

1459 else: 

1460 return ( 

1461 response 

1462 in [ 

1463 self.health_check_response, # If there is a subscription 

1464 self.health_check_response_b, # If there isn't a subscription and decode_responses=False 

1465 ] 

1466 ) 

1467 

1468 def check_health(self) -> None: 

1469 conn = self.connection 

1470 if conn is None: 

1471 raise RuntimeError( 

1472 "pubsub connection not set: " 

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

1474 ) 

1475 

1476 if conn.health_check_interval and time.monotonic() > conn.next_health_check: 

1477 conn.send_command("PING", self.HEALTH_CHECK_MESSAGE, check_health=False) 

1478 self.health_check_response_counter += 1 

1479 

1480 def _normalize_keys(self, data) -> Dict: 

1481 """ 

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

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

1484 from coercing the value for each message coming in. 

1485 """ 

1486 encode = self.encoder.encode 

1487 decode = self.encoder.decode 

1488 return {decode(encode(k)): v for k, v in data.items()} 

1489 

1490 def psubscribe( 

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

1492 ) -> None: 

1493 """ 

1494 Subscribe to channel patterns. 

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

1496 key and a callable as the value. 

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

1498 optional handler. 

1499 A pattern's callable will be invoked automatically 

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

1501 message via ``listen()``. 

1502 """ 

1503 new_patterns = parse_pubsub_subscriptions(args, kwargs) 

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

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

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

1507 # for the reconnection. 

1508 new_patterns = self._normalize_keys(new_patterns) 

1509 self.patterns.update(new_patterns) 

1510 if not self.subscribed: 

1511 # Set the subscribed_event flag to True 

1512 self.subscribed_event.set() 

1513 # Clear the health check counter 

1514 self.health_check_response_counter = 0 

1515 self.pending_unsubscribe_patterns.difference_update(new_patterns) 

1516 return ret_val 

1517 

1518 def punsubscribe(self, *args): 

1519 """ 

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

1521 all patterns. 

1522 """ 

1523 if args: 

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

1525 patterns = self._normalize_keys(dict.fromkeys(args)) 

1526 else: 

1527 patterns = self.patterns 

1528 self.pending_unsubscribe_patterns.update(patterns) 

1529 return self.execute_command("PUNSUBSCRIBE", *args) 

1530 

1531 def subscribe( 

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

1533 ) -> None: 

1534 """ 

1535 Subscribe to channels. 

1536 Channels supplied as keyword arguments expect 

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

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

1539 optional handler. 

1540 A channel's callable will be invoked automatically 

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

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

1543 """ 

1544 new_channels = parse_pubsub_subscriptions(args, kwargs) 

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

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

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

1548 # for the reconnection. 

1549 new_channels = self._normalize_keys(new_channels) 

1550 self.channels.update(new_channels) 

1551 if not self.subscribed: 

1552 # Set the subscribed_event flag to True 

1553 self.subscribed_event.set() 

1554 # Clear the health check counter 

1555 self.health_check_response_counter = 0 

1556 self.pending_unsubscribe_channels.difference_update(new_channels) 

1557 return ret_val 

1558 

1559 def unsubscribe(self, *args): 

1560 """ 

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

1562 all channels 

1563 """ 

1564 if args: 

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

1566 channels = self._normalize_keys(dict.fromkeys(args)) 

1567 else: 

1568 channels = self.channels 

1569 self.pending_unsubscribe_channels.update(channels) 

1570 return self.execute_command("UNSUBSCRIBE", *args) 

1571 

1572 def ssubscribe( 

1573 self, 

1574 *args: ChannelT | Subscription, 

1575 target_node: Any = None, 

1576 **kwargs: PubSubHandler, 

1577 ) -> None: 

1578 """ 

1579 Subscribes the client to the specified shard channels. 

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

1581 and a callable as the value. 

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

1583 with an optional handler. 

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

1585 is received on that channel rather than producing a message 

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

1587 """ 

1588 new_s_channels = parse_pubsub_subscriptions(args, kwargs) 

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

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

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

1592 # for the reconnection. 

1593 new_s_channels = self._normalize_keys(new_s_channels) 

1594 self.shard_channels.update(new_s_channels) 

1595 if not self.subscribed: 

1596 # Set the subscribed_event flag to True 

1597 self.subscribed_event.set() 

1598 # Clear the health check counter 

1599 self.health_check_response_counter = 0 

1600 self.pending_unsubscribe_shard_channels.difference_update(new_s_channels) 

1601 return ret_val 

1602 

1603 def sunsubscribe(self, *args, target_node=None): 

1604 """ 

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

1606 all shard_channels 

1607 """ 

1608 if args: 

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

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

1611 else: 

1612 s_channels = self.shard_channels 

1613 self.pending_unsubscribe_shard_channels.update(s_channels) 

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

1615 

1616 def listen(self): 

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

1618 

1619 Iteration ends once every channel and pattern has been unsubscribed 

1620 from. If nothing is subscribed when iteration begins it ends 

1621 immediately rather than waiting, so subscribe first: a listener 

1622 started before any subscription finishes without yielding anything. 

1623 """ 

1624 while self.subscribed: 

1625 response = self.handle_message(self.parse_response(block=True)) 

1626 if response is not None: 

1627 yield response 

1628 

1629 def get_message( 

1630 self, ignore_subscribe_messages: bool = False, timeout: float = 0.0 

1631 ): 

1632 """ 

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

1634 

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

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

1637 number, or None, to wait indefinitely. 

1638 """ 

1639 if not self.subscribed: 

1640 # Wait for subscription 

1641 start_time = time.monotonic() 

1642 if self.subscribed_event.wait(timeout) is True: 

1643 # The connection was subscribed during the timeout time frame. 

1644 # The timeout should be adjusted based on the time spent 

1645 # waiting for the subscription. timeout=None means "block 

1646 # indefinitely" and has nothing to charge against, so leave it 

1647 # alone rather than raising TypeError on the subtraction. 

1648 if timeout is not None: 

1649 time_spent = time.monotonic() - start_time 

1650 timeout = max(0.0, timeout - time_spent) 

1651 else: 

1652 # The connection isn't subscribed to any channels or patterns, 

1653 # so no messages are available 

1654 return None 

1655 

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

1657 

1658 if response: 

1659 return self.handle_message(response, ignore_subscribe_messages) 

1660 return None 

1661 

1662 get_sharded_message = get_message 

1663 

1664 def ping(self, message: Union[str, None] = None) -> bool: 

1665 """ 

1666 Ping the Redis server to test connectivity. 

1667 

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

1669 responds with "PONG". 

1670 """ 

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

1672 return self.execute_command(*args) 

1673 

1674 def handle_message(self, response, ignore_subscribe_messages=False): 

1675 """ 

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

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

1678 message being returned. 

1679 """ 

1680 if response is None: 

1681 return None 

1682 if isinstance(response, bytes): 

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

1684 

1685 message_type = str_if_bytes(response[0]) 

1686 if message_type == "pmessage": 

1687 message = { 

1688 "type": message_type, 

1689 "pattern": response[1], 

1690 "channel": response[2], 

1691 "data": response[3], 

1692 } 

1693 elif message_type == "pong": 

1694 message = { 

1695 "type": message_type, 

1696 "pattern": None, 

1697 "channel": None, 

1698 "data": response[1], 

1699 } 

1700 else: 

1701 message = { 

1702 "type": message_type, 

1703 "pattern": None, 

1704 "channel": response[1], 

1705 "data": response[2], 

1706 } 

1707 

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

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

1710 record_pubsub_message( 

1711 direction=PubSubDirection.RECEIVE, 

1712 channel=channel, 

1713 ) 

1714 elif message_type == "smessage": 

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

1716 record_pubsub_message( 

1717 direction=PubSubDirection.RECEIVE, 

1718 channel=channel, 

1719 sharded=True, 

1720 ) 

1721 

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

1723 # ``discard`` rather than ``remove``: the guard above already makes the 

1724 # removal conditional, so the two are equivalent for a single caller - 

1725 # but another writer can drop the same entry between the check and the 

1726 # removal, and ``remove`` would then raise ``KeyError`` out of a pubsub 

1727 # read that no caller catches. ``ClusterPubSub._detach_shard_channel`` 

1728 # is such a writer: it forgets a migrating shard channel locally, 

1729 # deliberately without the per-node I/O lock this bookkeeping runs 

1730 # under, because waiting for that lock stalls reconciliation behind a 

1731 # poll's whole retry budget on the node being migrated away from. 

1732 if message_type in self.UNSUBSCRIBE_MESSAGE_TYPES: 

1733 if message_type == "punsubscribe": 

1734 pattern = response[1] 

1735 if pattern in self.pending_unsubscribe_patterns: 

1736 self.pending_unsubscribe_patterns.discard(pattern) 

1737 self.patterns.pop(pattern, None) 

1738 elif message_type == "sunsubscribe": 

1739 s_channel = response[1] 

1740 if s_channel in self.pending_unsubscribe_shard_channels: 

1741 self.pending_unsubscribe_shard_channels.discard(s_channel) 

1742 self.shard_channels.pop(s_channel, None) 

1743 else: 

1744 channel = response[1] 

1745 if channel in self.pending_unsubscribe_channels: 

1746 self.pending_unsubscribe_channels.discard(channel) 

1747 self.channels.pop(channel, None) 

1748 if not self.channels and not self.patterns and not self.shard_channels: 

1749 # There are no subscriptions anymore, set subscribed_event flag 

1750 # to false 

1751 self.subscribed_event.clear() 

1752 

1753 if message_type in self.PUBLISH_MESSAGE_TYPES: 

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

1755 if message_type == "pmessage": 

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

1757 elif message_type == "smessage": 

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

1759 else: 

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

1761 if handler: 

1762 handler(message) 

1763 return None 

1764 elif message_type != "pong": 

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

1766 # want them 

1767 if ignore_subscribe_messages or self.ignore_subscribe_messages: 

1768 return None 

1769 

1770 return message 

1771 

1772 def run_in_thread( 

1773 self, 

1774 sleep_time: float = 0.0, 

1775 daemon: bool = False, 

1776 exception_handler: Optional[Callable] = None, 

1777 pubsub=None, 

1778 sharded_pubsub: bool = False, 

1779 ) -> "PubSubWorkerThread": 

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

1781 if handler is None: 

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

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

1784 if handler is None: 

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

1786 for s_channel, handler in self.shard_channels.items(): 

1787 if handler is None: 

1788 raise PubSubError( 

1789 f"Shard Channel: '{s_channel}' has no handler registered" 

1790 ) 

1791 

1792 pubsub = self if pubsub is None else pubsub 

1793 thread = PubSubWorkerThread( 

1794 pubsub, 

1795 sleep_time, 

1796 daemon=daemon, 

1797 exception_handler=exception_handler, 

1798 sharded_pubsub=sharded_pubsub, 

1799 ) 

1800 thread.start() 

1801 return thread 

1802 

1803 

1804class PubSubWorkerThread(threading.Thread): 

1805 def __init__( 

1806 self, 

1807 pubsub, 

1808 sleep_time: float, 

1809 daemon: bool = False, 

1810 exception_handler: Union[ 

1811 Callable[[Exception, "PubSub", "PubSubWorkerThread"], None], None 

1812 ] = None, 

1813 sharded_pubsub: bool = False, 

1814 ): 

1815 super().__init__() 

1816 self.daemon = daemon 

1817 self.pubsub = pubsub 

1818 self.sleep_time = sleep_time 

1819 self.exception_handler = exception_handler 

1820 self.sharded_pubsub = sharded_pubsub 

1821 self._running = threading.Event() 

1822 

1823 def run(self) -> None: 

1824 if self._running.is_set(): 

1825 return 

1826 self._running.set() 

1827 pubsub = self.pubsub 

1828 sleep_time = self.sleep_time 

1829 while self._running.is_set(): 

1830 try: 

1831 if not self.sharded_pubsub: 

1832 pubsub.get_message( 

1833 ignore_subscribe_messages=True, timeout=sleep_time 

1834 ) 

1835 else: 

1836 pubsub.get_sharded_message( 

1837 ignore_subscribe_messages=True, timeout=sleep_time 

1838 ) 

1839 except BaseException as e: 

1840 if self.exception_handler is None: 

1841 raise 

1842 self.exception_handler(e, pubsub, self) 

1843 pubsub.close() 

1844 

1845 def stop(self) -> None: 

1846 # trip the flag so the run loop exits. the run loop will 

1847 # close the pubsub connection, which disconnects the socket 

1848 # and returns the connection to the pool. 

1849 self._running.clear() 

1850 

1851 

1852class Pipeline(Redis): 

1853 """ 

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

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

1856 saving all the values in a list to Redis. 

1857 

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

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

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

1861 executed atomically. 

1862 

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

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

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

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

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

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

1869 on a key of a different datatype. 

1870 """ 

1871 

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

1873 

1874 def __init__( 

1875 self, 

1876 connection_pool: ConnectionPool, 

1877 response_callbacks, 

1878 transaction, 

1879 shard_hint, 

1880 ): 

1881 self.connection_pool = connection_pool 

1882 self.connection: Optional[Connection] = None 

1883 self.response_callbacks = response_callbacks 

1884 self.transaction = transaction 

1885 self.shard_hint = shard_hint 

1886 self.watching = False 

1887 self.command_stack = [] 

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

1889 self.explicit_transaction = False 

1890 

1891 def __enter__(self) -> "Pipeline": 

1892 return self 

1893 

1894 def __exit__(self, exc_type, exc_value, traceback): 

1895 self.reset() 

1896 

1897 def __del__(self): 

1898 try: 

1899 self.reset() 

1900 except Exception: 

1901 pass 

1902 

1903 def __len__(self) -> int: 

1904 return len(self.command_stack) 

1905 

1906 def __bool__(self) -> bool: 

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

1908 return True 

1909 

1910 def reset(self) -> None: 

1911 self.command_stack = [] 

1912 self.scripts = set() 

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

1914 # watching something 

1915 if self.watching and self.connection: 

1916 try: 

1917 # call this manually since our unwatch or 

1918 # immediate_execute_command methods can call reset() 

1919 self.connection.send_command("UNWATCH") 

1920 self.connection.read_response() 

1921 except ConnectionError: 

1922 # disconnect will also remove any previous WATCHes 

1923 self.connection.disconnect() 

1924 # clean up the other instance attributes 

1925 self.watching = False 

1926 self.explicit_transaction = False 

1927 

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

1929 # sure we're no longer WATCHing anything 

1930 if self.connection: 

1931 self.connection_pool.release(self.connection) 

1932 self.connection = None 

1933 

1934 def close(self) -> None: 

1935 """Close the pipeline""" 

1936 self.reset() 

1937 

1938 def multi(self) -> None: 

1939 """ 

1940 Start a transactional block of the pipeline after WATCH commands 

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

1942 """ 

1943 if self.explicit_transaction: 

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

1945 if self.command_stack: 

1946 raise RedisError( 

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

1948 ) 

1949 self.explicit_transaction = True 

1950 

1951 def execute_command(self, *args, **kwargs): 

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

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

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

1955 

1956 def _disconnect_reset_raise_on_watching( 

1957 self, 

1958 conn: AbstractConnection, 

1959 error: Exception, 

1960 failure_count: Optional[int] = None, 

1961 start_time: Optional[float] = None, 

1962 command_name: Optional[str] = None, 

1963 ) -> None: 

1964 """ 

1965 Close the connection reset watching state and 

1966 raise an exception if we were watching. 

1967 

1968 The supported exceptions are already checked in the 

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

1970 

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

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

1973 """ 

1974 if error and failure_count <= conn.retry.get_retries(): 

1975 record_operation_duration( 

1976 command_name=command_name, 

1977 duration_seconds=time.monotonic() - start_time, 

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

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

1980 db_namespace=str(conn.db), 

1981 error=error, 

1982 retry_attempts=failure_count, 

1983 ) 

1984 conn.disconnect() 

1985 

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

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

1988 # indicates the user should retry this transaction. 

1989 if self.watching: 

1990 self.reset() 

1991 raise WatchError( 

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

1993 ) 

1994 

1995 def immediate_execute_command(self, *args, **options): 

1996 """ 

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

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

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

2000 MULTI is called. 

2001 """ 

2002 command_name = args[0] 

2003 conn = self.connection 

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

2005 if not conn: 

2006 conn = self.connection_pool.get_connection() 

2007 self.connection = conn 

2008 

2009 # Start timing for observability 

2010 start_time = time.monotonic() 

2011 # Track actual retry attempts for error reporting 

2012 actual_retry_attempts = [0] 

2013 

2014 def failure_callback(error, failure_count): 

2015 if is_debug_log_enabled(): 

2016 add_debug_log_for_operation_failure(conn, error, args) 

2017 actual_retry_attempts[0] = failure_count 

2018 self._disconnect_reset_raise_on_watching( 

2019 conn, error, failure_count, start_time, command_name 

2020 ) 

2021 

2022 try: 

2023 response = conn.retry.call_with_retry( 

2024 lambda: self._send_command_parse_response( 

2025 conn, command_name, *args, **options 

2026 ), 

2027 failure_callback, 

2028 with_failure_count=True, 

2029 ) 

2030 

2031 record_operation_duration( 

2032 command_name=command_name, 

2033 duration_seconds=time.monotonic() - start_time, 

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

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

2036 db_namespace=str(conn.db), 

2037 ) 

2038 

2039 return response 

2040 except Exception as e: 

2041 record_error_count( 

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

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

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

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

2046 error_type=e, 

2047 retry_attempts=actual_retry_attempts[0], 

2048 is_internal=False, 

2049 ) 

2050 raise 

2051 

2052 def pipeline_execute_command(self, *args, **options) -> "Pipeline": 

2053 """ 

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

2055 

2056 Returns the current Pipeline object back so commands can be 

2057 chained together, such as: 

2058 

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

2060 

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

2062 which will execute all commands queued in the pipe. 

2063 """ 

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

2065 return self 

2066 

2067 def _himport_prepare_pipeline(self, conn, commands): 

2068 """Delegate to the shared sync HIMPORT executor.""" 

2069 _himport_exec.prepare_pipeline(self, conn, [args for args, _ in commands]) 

2070 

2071 def _execute_transaction( 

2072 self, connection: Connection, commands, raise_on_error 

2073 ) -> List: 

2074 # Ensure fieldsets referenced by buffered HIMPORT SETs are prepared on this 

2075 # connection before the MULTI/EXEC block (session state, not transactional). 

2076 self._himport_prepare_pipeline(connection, commands) 

2077 cmds = chain([(("MULTI",), {})], commands, [(("EXEC",), {})]) 

2078 all_cmds = connection.pack_commands( 

2079 [args for args, options in cmds if EMPTY_RESPONSE not in options] 

2080 ) 

2081 connection.send_packed_command(all_cmds) 

2082 errors = [] 

2083 

2084 # parse off the response for MULTI 

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

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

2087 # the socket 

2088 try: 

2089 self.parse_response(connection, "_") 

2090 except ResponseError as e: 

2091 errors.append((0, e)) 

2092 

2093 # and all the other commands 

2094 for i, command in enumerate(commands): 

2095 if EMPTY_RESPONSE in command[1]: 

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

2097 else: 

2098 try: 

2099 self.parse_response(connection, "_") 

2100 except ResponseError as e: 

2101 self.annotate_exception(e, i + 1, command[0]) 

2102 errors.append((i, e)) 

2103 

2104 # parse the EXEC. 

2105 try: 

2106 response = self.parse_response(connection, "_") 

2107 except ExecAbortError: 

2108 if errors: 

2109 raise errors[0][1] 

2110 raise 

2111 

2112 # EXEC clears any watched keys 

2113 self.watching = False 

2114 

2115 if response is None: 

2116 raise WatchError("Watched variable changed.") 

2117 

2118 # put any parse errors into the response 

2119 for i, e in errors: 

2120 response.insert(i, e) 

2121 

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

2123 self.connection.disconnect() 

2124 raise ResponseError( 

2125 "Wrong number of response items from pipeline execution" 

2126 ) 

2127 

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

2129 if raise_on_error: 

2130 self.raise_first_error(commands, response) 

2131 

2132 # We have to run response callbacks manually 

2133 data = [] 

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

2135 if not isinstance(r, Exception): 

2136 args, options = cmd 

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

2138 options.pop("keys", None) 

2139 command_name = args[0] 

2140 if command_name in self.response_callbacks: 

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

2142 data.append(r) 

2143 

2144 return data 

2145 

2146 def _execute_pipeline(self, connection, commands, raise_on_error): 

2147 # Fold any first-use HIMPORT PREPAREs for referenced fieldsets into the same 

2148 # packed write as the queued commands, so a pipeline that lands on a fresh or 

2149 # reconnected connection stays a single round trip (the batched write bypasses 

2150 # the per-command lazy PREPARE path). Deferred-discard reconciliation happens 

2151 # inside pipeline_prepares and only touches the socket when discards are 

2152 # actually pending. 

2153 fieldsets = _himport_exec.pipeline_prepares( 

2154 self, connection, [args for args, _ in commands] 

2155 ) 

2156 preflight = _himport_exec.prepare_wire_commands(fieldsets) 

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

2158 all_cmds = connection.pack_commands(preflight + [args for args, _ in commands]) 

2159 connection.send_packed_command(all_cmds) 

2160 

2161 # Drain the leading PREPARE replies (bookkeeping + capture the first error) 

2162 # before the queued replies. Everything on the wire is read before raising so 

2163 # the pooled socket never desyncs. 

2164 prep_error = _himport_exec.drain_pipeline_prepares(self, connection, fieldsets) 

2165 

2166 responses = [] 

2167 for args, options in commands: 

2168 try: 

2169 responses.append(self.parse_response(connection, args[0], **options)) 

2170 except ResponseError as e: 

2171 responses.append(e) 

2172 

2173 # A PREPARE failure (rare: an invalid fieldset definition) is a hard error, 

2174 # raised regardless of raise_on_error as it was before folding -- only now 

2175 # every reply has already been drained. 

2176 if prep_error is not None: 

2177 raise prep_error 

2178 if raise_on_error: 

2179 self.raise_first_error(commands, responses) 

2180 

2181 return responses 

2182 

2183 def raise_first_error(self, commands, response): 

2184 for i, r in enumerate(response): 

2185 if isinstance(r, ResponseError): 

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

2187 raise r 

2188 

2189 def annotate_exception(self, exception, number, command): 

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

2191 msg = ( 

2192 f"Command # {number} ({truncate_text(cmd)}) of pipeline " 

2193 f"caused error: {exception.args[0]}" 

2194 ) 

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

2196 

2197 def parse_response(self, connection, command_name, **options): 

2198 result = Redis.parse_response(self, connection, command_name, **options) 

2199 if command_name in self.UNWATCH_COMMANDS: 

2200 self.watching = False 

2201 elif command_name == "WATCH": 

2202 self.watching = True 

2203 return result 

2204 

2205 def load_scripts(self): 

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

2207 scripts = list(self.scripts) 

2208 immediate = self.immediate_execute_command 

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

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

2211 # get buffered in the pipeline. 

2212 exists = immediate("SCRIPT EXISTS", *shas) 

2213 if not all(exists): 

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

2215 if not exist: 

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

2217 

2218 def _disconnect_raise_on_watching( 

2219 self, 

2220 conn: AbstractConnection, 

2221 error: Exception, 

2222 failure_count: Optional[int] = None, 

2223 start_time: Optional[float] = None, 

2224 command_name: Optional[str] = None, 

2225 ) -> None: 

2226 """ 

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

2228 

2229 The supported exceptions are already checked in the 

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

2231 

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

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

2234 """ 

2235 if error and failure_count <= conn.retry.get_retries(): 

2236 record_operation_duration( 

2237 command_name=command_name, 

2238 duration_seconds=time.monotonic() - start_time, 

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

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

2241 db_namespace=str(conn.db), 

2242 error=error, 

2243 retry_attempts=failure_count, 

2244 ) 

2245 conn.disconnect() 

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

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

2248 # indicates the user should retry this transaction. 

2249 if self.watching: 

2250 raise WatchError( 

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

2252 ) 

2253 

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

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

2256 stack = self.command_stack 

2257 if not stack and not self.watching: 

2258 return [] 

2259 if self.scripts: 

2260 self.load_scripts() 

2261 if self.transaction or self.explicit_transaction: 

2262 execute = self._execute_transaction 

2263 operation_name = "MULTI" 

2264 else: 

2265 execute = self._execute_pipeline 

2266 operation_name = "PIPELINE" 

2267 

2268 conn = self.connection 

2269 if not conn: 

2270 conn = self.connection_pool.get_connection() 

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

2272 # back to the pool after we're done 

2273 self.connection = conn 

2274 

2275 # Start timing for observability 

2276 start_time = time.monotonic() 

2277 # Track actual retry attempts for error reporting 

2278 actual_retry_attempts = [0] 

2279 

2280 def failure_callback(error, failure_count): 

2281 if is_debug_log_enabled(): 

2282 add_debug_log_for_operation_failure(conn, error, (operation_name,)) 

2283 actual_retry_attempts[0] = failure_count 

2284 self._disconnect_raise_on_watching( 

2285 conn, error, failure_count, start_time, operation_name 

2286 ) 

2287 

2288 try: 

2289 response = conn.retry.call_with_retry( 

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

2291 failure_callback, 

2292 with_failure_count=True, 

2293 ) 

2294 

2295 record_operation_duration( 

2296 command_name=operation_name, 

2297 duration_seconds=time.monotonic() - start_time, 

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

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

2300 db_namespace=str(conn.db), 

2301 ) 

2302 return response 

2303 except Exception as e: 

2304 record_error_count( 

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

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

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

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

2309 error_type=e, 

2310 retry_attempts=actual_retry_attempts[0], 

2311 is_internal=False, 

2312 ) 

2313 raise 

2314 

2315 finally: 

2316 # in reset() the connection is disconnected before returned to the pool if 

2317 # it is marked for reconnect. 

2318 self.reset() 

2319 

2320 def discard(self): 

2321 """ 

2322 Flushes all previously queued commands 

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

2324 """ 

2325 self.execute_command("DISCARD") 

2326 

2327 def watch(self, *names): 

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

2329 if self.explicit_transaction: 

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

2331 return self.execute_command("WATCH", *names) 

2332 

2333 def unwatch(self) -> bool: 

2334 """Unwatches all previously specified keys""" 

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