Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/redis/commands/metadata.py: 39%

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

338 statements  

1""" 

2Normalized Redis command metadata. 

3 

4This module owns the record types for everything the client derives from a ``COMMAND`` 

5reply. ``CommandPolicies`` carries the request/response policies used for cluster routing; 

6``CommandMetadata`` is the superset record that adds the inputs to the 

7client-side-caching (CSC) eligibility rules. 

8 

9A command is cacheable only when it is ``readonly``, is not ``blocking``, takes at least one 

10key name argument, and carries none of the ``nondeterministic_output``, ``script_runner`` or 

11``dont_cache`` markers - see :func:`_is_client_side_cacheable`, which is the one normative 

12implementation of those rules and also requires the source metadata to have been complete. 

13Every boolean below therefore defaults to the fail-closed value, so a partially populated 

14record is never mistaken for a cacheable one. 

15 

16``MetadataResolver`` serves those records by command name. Resolvers chain through 

17``with_fallback``, first match wins, and an exhausted chain resolves to None - so an 

18unknown command fails closed. A cluster client reads the routing policies off the resolved 

19record; a client-side cache asks ``is_cacheable``, which applies the rules above to it. Either 

20way a consumer needs a reference to nothing but the resolver. 

21 

22A record may also withhold its routing policies by leaving them None, which says that the 

23metadata does not describe how to route the command and the client must resolve the target 

24itself - see :func:`_to_command_policies`. The cacheability inputs of such a record are still 

25authoritative, so withholding routing costs nothing on the caching side. 

26 

27``_STATIC_COMMAND_METADATA`` is the static resolver table, and the single source of truth for 

28what this client routes by: the commands eligible for client-side caching plus the commands the 

297.1.0 ``redis.commands.policies.STATIC_POLICIES`` table carried. That table is now a frozen, 

30unread copy of its 7.1.0 self - edit routing here, never there. Values here are validated 

31against a live 

32``COMMAND`` reply - see the provenance note on the constant, which records the server it 

33was checked against and the entries that diverge on purpose, before editing it by hand. 

34""" 

35 

36from abc import ABC, abstractmethod 

37from collections.abc import Mapping 

38from dataclasses import dataclass, replace 

39from enum import Enum 

40from types import MappingProxyType 

41 

42__all__ = [ 

43 "AsyncBaseMetadataResolver", 

44 "AsyncDynamicMetadataResolver", 

45 "AsyncMetadataResolver", 

46 "AsyncStaticMetadataResolver", 

47 "BaseMetadataResolver", 

48 "CommandMetadata", 

49 "CommandMetadataRecordsCache", 

50 "CommandPolicies", 

51 "DynamicMetadataResolver", 

52 "MetadataResolver", 

53 "PolicyRecords", 

54 "RequestPolicy", 

55 "ResponsePolicy", 

56 "StaticMetadataResolver", 

57] 

58 

59 

60class RequestPolicy(Enum): 

61 ALL_NODES = "all_nodes" 

62 ALL_SHARDS = "all_shards" 

63 ALL_REPLICAS = "all_replicas" 

64 MULTI_SHARD = "multi_shard" 

65 SPECIAL = "special" 

66 DEFAULT_KEYLESS = "default_keyless" 

67 DEFAULT_KEYED = "default_keyed" 

68 DEFAULT_NODE = "default_node" 

69 

70 

71class ResponsePolicy(Enum): 

72 ONE_SUCCEEDED = "one_succeeded" 

73 ALL_SUCCEEDED = "all_succeeded" 

74 AGG_LOGICAL_AND = "agg_logical_and" 

75 AGG_LOGICAL_OR = "agg_logical_or" 

76 AGG_MIN = "agg_min" 

77 AGG_MAX = "agg_max" 

78 AGG_SUM = "agg_sum" 

79 SPECIAL = "special" 

80 DEFAULT_KEYLESS = "default_keyless" 

81 DEFAULT_KEYED = "default_keyed" 

82 

83 

84class CommandPolicies: 

85 """ 

86 The routing view of a command's metadata: how to dispatch it, how to aggregate replies. 

87 

88 Kept as a mutable class rather than folded into :class:`CommandMetadata` because this is 

89 the shape that shipped in 7.1.0. 

90 

91 Compared by value, because a policy resolver serves the projection of the record it 

92 proxies rather than a record a caller handed it - so identity does not hold, and a caller 

93 matching what a resolver served against what it supplied has only the values to go by. 

94 """ 

95 

96 def __init__( 

97 self, 

98 request_policy: RequestPolicy = RequestPolicy.DEFAULT_KEYLESS, 

99 response_policy: ResponsePolicy = ResponsePolicy.DEFAULT_KEYLESS, 

100 ): 

101 self.request_policy = request_policy 

102 self.response_policy = response_policy 

103 

104 def __eq__(self, other: object) -> bool: 

105 if not isinstance(other, CommandPolicies): 

106 return NotImplemented 

107 

108 return ( 

109 self.request_policy == other.request_policy 

110 and self.response_policy == other.response_policy 

111 ) 

112 

113 def __hash__(self) -> int: 

114 # Defined alongside ``__eq__`` because Python would otherwise set it to None, which 

115 # would make a type that shipped hashable in 7.1.0 unhashable. 

116 return hash((self.request_policy, self.response_policy)) 

117 

118 def __repr__(self) -> str: 

119 return ( 

120 f"{type(self).__name__}(request_policy={self.request_policy!r}, " 

121 f"response_policy={self.response_policy!r})" 

122 ) 

123 

124 

125PolicyRecords = dict[str, dict[str, CommandPolicies]] 

126 

127 

128@dataclass(frozen=True, slots=True) 

129class CommandMetadata: 

130 """ 

131 Normalized metadata for a single Redis command or container subcommand. 

132 

133 Instances are immutable so the static table below can be shared without copying. A 

134 resolver that refines a record builds a new one with :func:`dataclasses.replace`. 

135 

136 Attributes: 

137 request_policy: 

138 How the cluster client should route the command. Derived from the 

139 ``request_policy`` command tip, defaulting to keyed/keyless based on whether 

140 the command takes keys. None means the record withholds its routing opinion: 

141 the metadata does not describe how to route this command, so the client resolves 

142 the target itself. That is distinct from ``DEFAULT_KEYLESS``, which is a routing 

143 decision - send the command to any one node. 

144 response_policy: 

145 How the cluster client should aggregate replies from several nodes. Derived 

146 from the ``response_policy`` command tip, with the same default. None has the 

147 same meaning as it does for ``request_policy``; the two are withheld together, 

148 because a policy resolver serves them as one record. 

149 is_readonly: 

150 Whether the command has the ``readonly`` command flag. Required for 

151 cacheability. 

152 is_blocking: 

153 Whether the command has the ``blocking`` command flag, which marks a command 

154 that may block the connection until data arrives. A blocking read serves the 

155 reply its caller waited for rather than a snapshot of keyspace state, so 

156 re-serving it from a cache would break the command's execution-time contract: 

157 ``XREAD BLOCK`` in a loop would stop blocking once a timed-out reply was cached. 

158 The exclusion is by command name, so ``XREAD`` is ineligible even when a 

159 particular call omits ``BLOCK``. It is the only readonly, keyed command the flag 

160 excludes. 

161 has_key_argument: 

162 Whether the command accepts at least one Redis key name argument. True when 

163 ``key_specifications`` holds at least one spec not flagged ``not_key``, 

164 falling back to ``first_key_pos > 0 and step_count > 0`` when key specs are 

165 absent. ``last_key_pos`` is never consulted: it is ``-1`` for variadic 

166 commands and must not disqualify one. 

167 

168 This is not the inverse of ``_is_keyless_command``, which tests 

169 ``first_key_pos`` alone and so reports every ``movablekeys`` command as 

170 keyless. It is also recorded independently of ``request_policy``, because a 

171 command tip overwrites the keyed/keyless default, after which the resolved 

172 policy no longer indicates whether the command takes keys. 

173 has_nondeterministic_output: 

174 Whether the command has the ``nondeterministic_output`` command tip, which 

175 marks a command whose reply may differ between calls over the same keyspace 

176 state. Matched exactly rather than by prefix: ``nondeterministic_output_order`` 

177 is a distinct tip, denoting only that element order varies. 

178 is_script_runner: 

179 Whether the command has the ``script_runner`` command flag, which marks a 

180 command that executes a user-supplied script or function. Reported by Redis 

181 8.10 and later; earlier servers do not report the flag at all. 

182 is_dont_cache: 

183 Whether the command has the ``dont_cache`` command tip, which marks a command 

184 whose reply the server states must not be cached client-side. It is a negative 

185 override: it decides on its own, even when every positive rule matches. 

186 has_complete_metadata: 

187 Whether the source metadata was complete enough to trust, meaning the command 

188 flags were present *and* the ``tips`` key was present. ``COMMAND`` replies 

189 older than Redis 7.0 carry neither ``tips`` nor ``key_specifications``, which 

190 makes ``nondeterministic_output`` and ``dont_cache`` undetectable. An empty 

191 tips list is a real answer; a missing tips key is not. An incomplete record 

192 must not be treated as authoritative. 

193 """ 

194 

195 request_policy: RequestPolicy | None = RequestPolicy.DEFAULT_KEYLESS 

196 response_policy: ResponsePolicy | None = ResponsePolicy.DEFAULT_KEYLESS 

197 is_readonly: bool = False 

198 is_blocking: bool = False 

199 has_key_argument: bool = False 

200 has_nondeterministic_output: bool = False 

201 is_script_runner: bool = False 

202 is_dont_cache: bool = False 

203 has_complete_metadata: bool = False 

204 

205 

206CommandMetadataRecordsCache = Mapping[str, Mapping[str, CommandMetadata]] 

207"""Command metadata keyed by module name, then by command name. 

208 

209Mirrors the shape of ``PolicyRecords``: non-module commands live under ``"core"``, module 

210commands under their lowercased prefix (``"json"``, ``"ts"``, ``"ft"``), and container 

211subcommands under their space-joined name (``"memory usage"``), which is the form 

212``execute_command`` receives as ``args[0]``. 

213""" 

214 

215 

216def _lowercase_keyed( 

217 records: CommandMetadataRecordsCache, 

218) -> CommandMetadataRecordsCache: 

219 """ 

220 Key the given records lowercase, which is how :func:`_split_command_name` looks one up. 

221 

222 Records built from a ``COMMAND`` reply already are - both parsers lowercase every command 

223 name as they read it - so the common case returns the argument untouched rather than 

224 rebuilding it. Caller-supplied records carry whatever spelling the caller chose, and a 

225 resolver that silently answers nothing for a table keyed ``"ZCOUNT"`` would be worse than 

226 one that normalizes it. 

227 """ 

228 if all( 

229 module_name == module_name.lower() 

230 and all(command == command.lower() for command in commands) 

231 for module_name, commands in records.items() 

232 ): 

233 return records 

234 

235 return { 

236 module_name.lower(): { 

237 command.lower(): metadata for command, metadata in commands.items() 

238 } 

239 for module_name, commands in records.items() 

240 } 

241 

242 

243def _split_command_name(command_name: str) -> tuple[str, str]: 

244 """ 

245 Split a command name into the ``(module, command)`` pair the record tables are keyed by. 

246 

247 Non-module commands resolve under ``"core"``. Container subcommands are not split here: 

248 they arrive space-joined (``"memory usage"``) and are looked up under that whole name. 

249 

250 The name is lowercased, because the record tables are keyed lowercase while callers 

251 spell a command the way they send it: the cluster client lowercases before resolving, 

252 but the client-side cache is handed the command name as the command methods spell it 

253 (``"GET"``, ``"JSON.GET"``, ``"MEMORY USAGE"``). 

254 

255 Raises: 

256 ValueError: If the name carries more than one module prefix. 

257 """ 

258 parts = command_name.lower().split(".") 

259 

260 if len(parts) > 2: 

261 raise ValueError(f"Wrong command or module name: {command_name}") 

262 

263 if len(parts) == 2: 

264 return parts[0], parts[1] 

265 

266 return "core", parts[0] 

267 

268 

269# Upper bound on each of a resolver's memos. Far above the number of distinct command names 

270# an application issues - the whole core surface plus every module's is a few hundred - but 

271# bounded, because the default resolver a client gets is a single instance evaluated at import 

272# and shared by every client in the process, ``execute_command`` accepts an arbitrary command 

273# name, and a name that resolves to nothing is memoized too. Past the cap the views still 

274# answer correctly; they just recompute. 

275_MEMO_MAX_ENTRIES = 4096 

276 

277# Commands whose ``readonly`` flag is not sufficient to make replica routing safe. TOUCH 

278# changes key idle-time state even though Redis reports it readonly, so it must continue to 

279# reach a primary. Keep this exception narrow: caller-supplied metadata remains authoritative 

280# for ordinary core and module reads. 

281# 

282# Loaded into every resolver's ``_replica_safe`` memo at construction, which is the whole 

283# mechanism: the entry is in place before any command is looked up, so these answer from the 

284# same single dict lookup as every other command and ``is_replica_safe`` needs no test of its 

285# own. Nothing clears or evicts from that memo - the cap only declines to add - so a seeded 

286# entry cannot be lost. Names are lower case, the form command names are keyed by throughout 

287# this module. 

288_REPLICA_UNSAFE_COMMANDS = frozenset({"touch"}) 

289 

290 

291class MetadataResolver(ABC): 

292 @abstractmethod 

293 def resolve(self, command_name: str) -> CommandMetadata | None: 

294 """ 

295 Resolves the command name and determines the associated command metadata. 

296 

297 Args: 

298 command_name: The name of the command to resolve, in any case. 

299 

300 Returns: 

301 CommandMetadata: The metadata associated with the specified command, or None 

302 when no resolver in the chain knows the command. 

303 """ 

304 pass 

305 

306 @abstractmethod 

307 def resolve_policies(self, command_name: str) -> CommandPolicies | None: 

308 """ 

309 Resolves the command name and determines the associated routing policies. 

310 

311 The routing view of :meth:`resolve`: the request/response policies the resolved 

312 record carries, and nothing else about it. 

313 

314 Args: 

315 command_name: The name of the command to resolve, in any case. 

316 

317 Returns: 

318 CommandPolicies: The policies associated with the specified command, or None 

319 when no resolver in the chain knows the command. 

320 """ 

321 pass 

322 

323 @abstractmethod 

324 def is_cacheable(self, command_name: str) -> bool: 

325 """ 

326 Determines whether the reply of a command may be served from a client-side cache. 

327 

328 The client-side-caching view of :meth:`resolve`, decided by 

329 :func:`_is_client_side_cacheable`. Fails closed: an unknown command, a name the 

330 record tables cannot be keyed by, and a record built from incomplete metadata all 

331 resolve to False. 

332 

333 Args: 

334 command_name: The name of the command to check, in any case. 

335 

336 Returns: 

337 bool: True only when every eligibility rule is satisfied. 

338 """ 

339 pass 

340 

341 @abstractmethod 

342 def is_replica_safe(self, command_name: str) -> bool: 

343 """ 

344 Determines whether a command is safe to execute on a replica. 

345 

346 Args: 

347 command_name: The name of the command to check, in any case. 

348 

349 Returns: 

350 bool: True if the command is replica safe. 

351 """ 

352 pass 

353 

354 @abstractmethod 

355 def is_trackable_read(self, command_name: str) -> bool: 

356 """ 

357 Determines whether the server would remember this command's keys while tracking. 

358 

359 The server-side-tracking view of :meth:`resolve`, decided by 

360 :func:`_is_trackable_read`. Never affects what may be stored: it only decides whether 

361 a ``CLIENT CACHING NO`` in front of a read is worth sending under ``optout`` tracking. 

362 When in doubt it returns False, so the ``NO`` is skipped and the server keeps 

363 tracking the keys. That is the safe side: an extra tracked key only costs an entry in 

364 the server's invalidation table, but a stored reply that the server does not track is 

365 never invalidated and can go stale. 

366 

367 Args: 

368 command_name: The name of the command to check, in any case. 

369 

370 Returns: 

371 bool: True only when the command carries the ``readonly`` command flag. 

372 """ 

373 pass 

374 

375 @abstractmethod 

376 def with_fallback(self, fallback: "MetadataResolver") -> "MetadataResolver": 

377 """ 

378 Factory method to instantiate a metadata resolver with a fallback resolver. 

379 

380 Args: 

381 fallback: Fallback resolver 

382 

383 Returns: 

384 MetadataResolver: Returns a new metadata resolver with the specified fallback resolver. 

385 """ 

386 pass 

387 

388 

389class AsyncMetadataResolver(ABC): 

390 @abstractmethod 

391 async def resolve(self, command_name: str) -> CommandMetadata | None: 

392 """ 

393 Resolves the command name and determines the associated command metadata. 

394 

395 Args: 

396 command_name: The name of the command to resolve, in any case. 

397 

398 Returns: 

399 CommandMetadata: The metadata associated with the specified command, or None 

400 when no resolver in the chain knows the command. 

401 """ 

402 pass 

403 

404 @abstractmethod 

405 async def resolve_policies(self, command_name: str) -> CommandPolicies | None: 

406 """ 

407 Resolves the command name and determines the associated routing policies. 

408 

409 The routing view of :meth:`resolve`: the request/response policies the resolved 

410 record carries, and nothing else about it. 

411 

412 Args: 

413 command_name: The name of the command to resolve, in any case. 

414 

415 Returns: 

416 CommandPolicies: The policies associated with the specified command, or None 

417 when no resolver in the chain knows the command. 

418 """ 

419 pass 

420 

421 @abstractmethod 

422 async def is_cacheable(self, command_name: str) -> bool: 

423 """ 

424 Determines whether the reply of a command may be served from a client-side cache. 

425 

426 The client-side-caching view of :meth:`resolve`, decided by 

427 :func:`_is_client_side_cacheable`. Fails closed: an unknown command, a name the 

428 record tables cannot be keyed by, and a record built from incomplete metadata all 

429 resolve to False. 

430 

431 Args: 

432 command_name: The name of the command to check, in any case. 

433 

434 Returns: 

435 bool: True only when every eligibility rule is satisfied. 

436 """ 

437 pass 

438 

439 @abstractmethod 

440 async def is_replica_safe(self, command_name: str) -> bool: 

441 """ 

442 Determines whether a command is safe to execute on a replica. 

443 

444 Args: 

445 command_name: The name of the command to check, in any case. 

446 

447 Returns: 

448 bool: True if the command is replica safe. 

449 """ 

450 pass 

451 

452 @abstractmethod 

453 async def is_trackable_read(self, command_name: str) -> bool: 

454 """ 

455 Determines whether the server would remember this command's keys while tracking. 

456 

457 The server-side-tracking view of :meth:`resolve`, decided by 

458 :func:`_is_trackable_read`. Never affects what may be stored: it only decides whether 

459 a ``CLIENT CACHING NO`` in front of a read is worth sending under ``optout`` tracking. 

460 When in doubt it returns False, so the ``NO`` is skipped and the server keeps 

461 tracking the keys. That is the safe side: an extra tracked key only costs an entry in 

462 the server's invalidation table, but a stored reply that the server does not track is 

463 never invalidated and can go stale. 

464 

465 Args: 

466 command_name: The name of the command to check, in any case. 

467 

468 Returns: 

469 bool: True only when the command carries the ``readonly`` command flag. 

470 """ 

471 pass 

472 

473 @abstractmethod 

474 def with_fallback( 

475 self, fallback: "AsyncMetadataResolver" 

476 ) -> "AsyncMetadataResolver": 

477 """ 

478 Factory method to instantiate an async metadata resolver with a fallback resolver. 

479 

480 Args: 

481 fallback: Fallback resolver 

482 

483 Returns: 

484 AsyncMetadataResolver: Returns a new metadata resolver with the specified fallback resolver. 

485 """ 

486 pass 

487 

488 

489class BaseMetadataResolver(MetadataResolver): 

490 """ 

491 Base class for metadata resolvers. 

492 

493 Lookup is first-match-wins: a command the records do not carry falls through to the 

494 fallback resolver, and a chain that ends without a match resolves to None. Besides the 

495 whole record, each consumer gets the view it needs of the same resolved metadata: 

496 ``resolve_policies`` for cluster routing, ``is_cacheable`` for client-side caching. 

497 

498 Both views are memoized under the lowercased command name, so every spelling of one 

499 command - ``GET`` as the command methods write it, ``get`` as the cluster client lowers it 

500 - shares a single entry instead of taking one each. The metadata a resolver serves is a 

501 snapshot taken when the resolver is built, so a command's view cannot change, and the memo 

502 keeps the record lookup, the projection and the walk down the fallback chain off the 

503 command execution path. Concurrent resolves of the same command may each compute it once; 

504 the memo is idempotent, so the duplicated work is harmless. The memos grow with the set of 

505 distinct commands a caller asks about, and are capped at ``_MEMO_MAX_ENTRIES`` so that a 

506 caller asking about unbounded many names - a name that resolves to nothing is memoized 

507 too - cannot grow them without bound. 

508 """ 

509 

510 def __init__( 

511 self, 

512 metadata: CommandMetadataRecordsCache, 

513 fallback: MetadataResolver | None = None, 

514 ) -> None: 

515 self._metadata = metadata 

516 self._fallback = fallback 

517 self._policies: dict[str, CommandPolicies | None] = {} 

518 self._cacheable: dict[str, bool] = {} 

519 self._replica_safe: dict[str, bool] = { 

520 cmd: False for cmd in _REPLICA_UNSAFE_COMMANDS 

521 } 

522 # No ``_REPLICA_UNSAFE_COMMANDS`` pre-seed: trackability is the readonly flag alone, 

523 # and TOUCH - the one command that seed excludes - is precisely the trackable read 

524 # ``optout`` most wants to exempt. 

525 self._trackable_read: dict[str, bool] = {} 

526 

527 def resolve(self, command_name: str) -> CommandMetadata | None: 

528 module, command = _split_command_name(command_name) 

529 

530 commands = self._metadata.get(module) 

531 metadata = commands.get(command) if commands is not None else None 

532 

533 if metadata is None: 

534 if self._fallback is not None: 

535 return self._fallback.resolve(command_name) 

536 return None 

537 

538 return metadata 

539 

540 def resolve_policies(self, command_name: str) -> CommandPolicies | None: 

541 # Memoized under the lowercased name, so the spellings of one command share an 

542 # entry rather than taking one each. ``resolve`` is still asked with the name as 

543 # given, so an unresolvable one is reported the way the caller spelled it. 

544 memo_key = command_name.lower() 

545 

546 try: 

547 return self._policies[memo_key] 

548 except KeyError: 

549 pass 

550 

551 metadata = self.resolve(command_name) 

552 policies = _to_command_policies(metadata) if metadata is not None else None 

553 

554 if len(self._policies) < _MEMO_MAX_ENTRIES: 

555 self._policies[memo_key] = policies 

556 

557 return policies 

558 

559 def is_cacheable(self, command_name: str) -> bool: 

560 # A name the record tables cannot be keyed by is not a command this client can decide 

561 # on. Fail closed rather than raise into the command execution path, where a raw 

562 # command with such a name must still reach the server and come back with the 

563 # server's own error. ``execute_command`` accepts an arbitrary first argument, and a 

564 # non-str one - ``bytes``, which the request encoder accepts - reaches here 

565 # unchanged; it is refused rather than decoded, because deciding it would start 

566 # caching a command whose name this client never resolved. 

567 if not isinstance(command_name, str): 

568 return False 

569 

570 # Memoized under the lowercased name, so the spellings of one command share an 

571 # entry rather than taking one each. 

572 memo_key = command_name.lower() 

573 

574 try: 

575 return self._cacheable[memo_key] 

576 except KeyError: 

577 pass 

578 

579 try: 

580 metadata = self.resolve(command_name) 

581 except ValueError: 

582 # More than one module prefix, which ``_split_command_name`` refuses. 

583 metadata = None 

584 

585 cacheable = _is_client_side_cacheable(metadata) 

586 

587 if len(self._cacheable) < _MEMO_MAX_ENTRIES: 

588 self._cacheable[memo_key] = cacheable 

589 

590 return cacheable 

591 

592 def is_replica_safe(self, command_name: str) -> bool: 

593 if not isinstance(command_name, str): 

594 return False 

595 

596 memo_key = command_name.lower() 

597 

598 try: 

599 return self._replica_safe[memo_key] 

600 except KeyError: 

601 pass 

602 

603 try: 

604 metadata = self.resolve(command_name) 

605 except ValueError: 

606 metadata = None 

607 

608 replica_safe = _is_replica_safe(metadata) 

609 

610 if len(self._replica_safe) < _MEMO_MAX_ENTRIES: 

611 self._replica_safe[memo_key] = replica_safe 

612 

613 return replica_safe 

614 

615 def is_trackable_read(self, command_name: str) -> bool: 

616 if not isinstance(command_name, str): 

617 return False 

618 

619 memo_key = command_name.lower() 

620 

621 try: 

622 return self._trackable_read[memo_key] 

623 except KeyError: 

624 pass 

625 

626 try: 

627 metadata = self.resolve(command_name) 

628 except ValueError: 

629 metadata = None 

630 

631 trackable_read = _is_trackable_read(metadata) 

632 

633 if len(self._trackable_read) < _MEMO_MAX_ENTRIES: 

634 self._trackable_read[memo_key] = trackable_read 

635 

636 return trackable_read 

637 

638 @abstractmethod 

639 def with_fallback(self, fallback: "MetadataResolver") -> "MetadataResolver": 

640 pass 

641 

642 

643class AsyncBaseMetadataResolver(AsyncMetadataResolver): 

644 """ 

645 Async base class for metadata resolvers. 

646 

647 Lookup is first-match-wins: a command the records do not carry falls through to the 

648 fallback resolver, and a chain that ends without a match resolves to None. Besides the 

649 whole record, each consumer gets the view it needs of the same resolved metadata: 

650 ``resolve_policies`` for cluster routing, ``is_cacheable`` for client-side caching. 

651 

652 Both views are memoized under the lowercased command name, so every spelling of one 

653 command - ``GET`` as the command methods write it, ``get`` as the cluster client lowers it 

654 - shares a single entry instead of taking one each. The metadata a resolver serves is a 

655 snapshot taken when the resolver is built, so a command's view cannot change, and the memo 

656 keeps the record lookup, the projection and the walk down the fallback chain off the 

657 command execution path. Concurrent resolves of the same command may each compute it once; 

658 the memo is idempotent, so the duplicated work is harmless. The memos grow with the set of 

659 distinct commands a caller asks about, and are capped at ``_MEMO_MAX_ENTRIES`` so that a 

660 caller asking about unbounded many names - a name that resolves to nothing is memoized 

661 too - cannot grow them without bound. 

662 """ 

663 

664 def __init__( 

665 self, 

666 metadata: CommandMetadataRecordsCache, 

667 fallback: AsyncMetadataResolver | None = None, 

668 ) -> None: 

669 self._metadata = metadata 

670 self._fallback = fallback 

671 self._policies: dict[str, CommandPolicies | None] = {} 

672 self._cacheable: dict[str, bool] = {} 

673 self._replica_safe: dict[str, bool] = { 

674 cmd: False for cmd in _REPLICA_UNSAFE_COMMANDS 

675 } 

676 # No ``_REPLICA_UNSAFE_COMMANDS`` pre-seed: trackability is the readonly flag alone, 

677 # and TOUCH - the one command that seed excludes - is precisely the trackable read 

678 # ``optout`` most wants to exempt. 

679 self._trackable_read: dict[str, bool] = {} 

680 

681 async def resolve(self, command_name: str) -> CommandMetadata | None: 

682 module, command = _split_command_name(command_name) 

683 

684 commands = self._metadata.get(module) 

685 metadata = commands.get(command) if commands is not None else None 

686 

687 if metadata is None: 

688 if self._fallback is not None: 

689 return await self._fallback.resolve(command_name) 

690 return None 

691 

692 return metadata 

693 

694 async def resolve_policies(self, command_name: str) -> CommandPolicies | None: 

695 # Memoized under the lowercased name, so the spellings of one command share an 

696 # entry rather than taking one each. ``resolve`` is still asked with the name as 

697 # given, so an unresolvable one is reported the way the caller spelled it. 

698 memo_key = command_name.lower() 

699 

700 try: 

701 return self._policies[memo_key] 

702 except KeyError: 

703 pass 

704 

705 metadata = await self.resolve(command_name) 

706 policies = _to_command_policies(metadata) if metadata is not None else None 

707 

708 if len(self._policies) < _MEMO_MAX_ENTRIES: 

709 self._policies[memo_key] = policies 

710 

711 return policies 

712 

713 async def is_cacheable(self, command_name: str) -> bool: 

714 # A name the record tables cannot be keyed by is not a command this client can decide 

715 # on. Fail closed rather than raise into the command execution path, where a raw 

716 # command with such a name must still reach the server and come back with the 

717 # server's own error. ``execute_command`` accepts an arbitrary first argument, and a 

718 # non-str one - ``bytes``, which the request encoder accepts - reaches here 

719 # unchanged; it is refused rather than decoded, because deciding it would start 

720 # caching a command whose name this client never resolved. 

721 if not isinstance(command_name, str): 

722 return False 

723 

724 # Memoized under the lowercased name, so the spellings of one command share an 

725 # entry rather than taking one each. 

726 memo_key = command_name.lower() 

727 

728 try: 

729 return self._cacheable[memo_key] 

730 except KeyError: 

731 pass 

732 

733 try: 

734 metadata = await self.resolve(command_name) 

735 except ValueError: 

736 # More than one module prefix, which ``_split_command_name`` refuses. 

737 metadata = None 

738 

739 cacheable = _is_client_side_cacheable(metadata) 

740 

741 if len(self._cacheable) < _MEMO_MAX_ENTRIES: 

742 self._cacheable[memo_key] = cacheable 

743 

744 return cacheable 

745 

746 async def is_replica_safe(self, command_name: str) -> bool: 

747 if not isinstance(command_name, str): 

748 return False 

749 

750 memo_key = command_name.lower() 

751 

752 try: 

753 return self._replica_safe[memo_key] 

754 except KeyError: 

755 pass 

756 

757 try: 

758 metadata = await self.resolve(command_name) 

759 except ValueError: 

760 metadata = None 

761 

762 replica_safe = _is_replica_safe(metadata) 

763 

764 if len(self._replica_safe) < _MEMO_MAX_ENTRIES: 

765 self._replica_safe[memo_key] = replica_safe 

766 

767 return replica_safe 

768 

769 async def is_trackable_read(self, command_name: str) -> bool: 

770 if not isinstance(command_name, str): 

771 return False 

772 

773 memo_key = command_name.lower() 

774 

775 try: 

776 return self._trackable_read[memo_key] 

777 except KeyError: 

778 pass 

779 

780 try: 

781 metadata = await self.resolve(command_name) 

782 except ValueError: 

783 metadata = None 

784 

785 trackable_read = _is_trackable_read(metadata) 

786 

787 if len(self._trackable_read) < _MEMO_MAX_ENTRIES: 

788 self._trackable_read[memo_key] = trackable_read 

789 

790 return trackable_read 

791 

792 @abstractmethod 

793 def with_fallback( 

794 self, fallback: "AsyncMetadataResolver" 

795 ) -> "AsyncMetadataResolver": 

796 pass 

797 

798 

799class DynamicMetadataResolver(BaseMetadataResolver): 

800 """ 

801 Resolves metadata dynamically based on the provided metadata records 

802 (they can be extracted either from COMMAND output, or provided by user). 

803 

804 Note: Takes the records rather than the parser that produced them, so that this and 

805 ``AsyncDynamicMetadataResolver`` accept the same argument: the async parser's 

806 ``get_commands_metadata_cache`` is a coroutine and cannot be awaited in a constructor, so records 

807 are the one shape both stacks can be built from. :func:`_load_commands_metadata_cache` turns a 

808 parser into records at the one place that owns one, ``DynamicPolicyResolver``. 

809 """ 

810 

811 def __init__( 

812 self, 

813 metadata_records: CommandMetadataRecordsCache, 

814 fallback: MetadataResolver | None = None, 

815 ) -> None: 

816 """ 

817 Parameters: 

818 metadata_records (CommandMetadataRecordsCache): Command metadata records, 

819 keyed the way ``CommandMetadataRecordsCache`` documents. Keys are 

820 lowercased if they are not already, because that is how a resolved 

821 command name is looked up. 

822 fallback (Optional[MetadataResolver]): An optional resolver to be used when the 

823 primary metadata cannot handle a specific request. 

824 """ 

825 super().__init__(_lowercase_keyed(metadata_records), fallback) 

826 

827 def with_fallback(self, fallback: "MetadataResolver") -> "MetadataResolver": 

828 return DynamicMetadataResolver(self._metadata, fallback) 

829 

830 

831class StaticMetadataResolver(BaseMetadataResolver): 

832 """ 

833 Resolves metadata from a static list, provided by the library, 

834 containing command metadata records. 

835 """ 

836 

837 def __init__(self, fallback: MetadataResolver | None = None) -> None: 

838 """ 

839 Parameters: 

840 fallback (Optional[MetadataResolver]): An optional fallback metadata resolver 

841 used for resolving metadata if static metadata is inadequate. 

842 """ 

843 super().__init__(_STATIC_COMMAND_METADATA, fallback) 

844 

845 def with_fallback(self, fallback: "MetadataResolver") -> "MetadataResolver": 

846 return StaticMetadataResolver(fallback) 

847 

848 

849class AsyncDynamicMetadataResolver(AsyncBaseMetadataResolver): 

850 """ 

851 Async version of DynamicMetadataResolver. 

852 

853 Takes the records rather than the parser that produced them, because 

854 ``AsyncCommandsParser.get_commands_metadata_cache`` is a coroutine and cannot be awaited in a 

855 constructor. 

856 """ 

857 

858 def __init__( 

859 self, 

860 metadata_records: CommandMetadataRecordsCache, 

861 fallback: AsyncMetadataResolver | None = None, 

862 ) -> None: 

863 """ 

864 Parameters: 

865 metadata_records (CommandMetadataRecordsCache): Command metadata records, 

866 keyed the way ``CommandMetadataRecordsCache`` documents. Keys are 

867 lowercased if they are not already, because that is how a resolved 

868 command name is looked up. 

869 fallback (Optional[AsyncMetadataResolver]): An optional resolver to be used when the 

870 primary metadata cannot handle a specific request. 

871 """ 

872 super().__init__(_lowercase_keyed(metadata_records), fallback) 

873 

874 def with_fallback( 

875 self, fallback: "AsyncMetadataResolver" 

876 ) -> "AsyncMetadataResolver": 

877 return AsyncDynamicMetadataResolver(self._metadata, fallback) 

878 

879 

880class AsyncStaticMetadataResolver(AsyncBaseMetadataResolver): 

881 """ 

882 Async version of StaticMetadataResolver. 

883 """ 

884 

885 def __init__(self, fallback: AsyncMetadataResolver | None = None) -> None: 

886 """ 

887 Parameters: 

888 fallback (Optional[AsyncMetadataResolver]): An optional fallback metadata resolver 

889 used for resolving metadata if static metadata is inadequate. 

890 """ 

891 super().__init__(_STATIC_COMMAND_METADATA, fallback) 

892 

893 def with_fallback( 

894 self, fallback: "AsyncMetadataResolver" 

895 ) -> "AsyncMetadataResolver": 

896 return AsyncStaticMetadataResolver(fallback) 

897 

898 

899def _to_command_policies(metadata: CommandMetadata) -> CommandPolicies | None: 

900 """ 

901 Project a single metadata record down to the routing policies a policy resolver serves. 

902 

903 Drops every field a ``CommandPolicies`` record does not carry, so the policies a 

904 command routes by stay derived from its metadata rather than tracked beside it. 

905 

906 A record that withholds its routing policies projects to None, which a policy resolver 

907 reports the same way it reports a command it does not carry. Withholding therefore 

908 reproduces, exactly, what a command absent from the records resolves to: a policy-level 

909 fallback still gets its turn, and a resolver with none leaves the cluster client to resolve 

910 the target itself - which for a ``movablekeys`` command is the only path that finds its 

911 keys. The *metadata* chain is not re-walked, though: the record was found, so a metadata 

912 resolver behind this one is never asked, and cannot answer with the very policies the 

913 record withholds. 

914 """ 

915 if metadata.request_policy is None or metadata.response_policy is None: 

916 return None 

917 

918 return CommandPolicies( 

919 request_policy=metadata.request_policy, 

920 response_policy=metadata.response_policy, 

921 ) 

922 

923 

924def _is_client_side_cacheable(metadata: CommandMetadata | None) -> bool: 

925 """ 

926 Decide whether the reply of the command a record describes may be cached client-side. 

927 

928 The one normative implementation of the eligibility rules, applied in the order they are 

929 specified. Every rule is a veto, so the verdict does not depend on the order; the order 

930 is kept to match the specification. 

931 

932 Takes ``None`` - what an exhausted resolver chain resolves to - so the unknown-command 

933 case is decided here as well, rather than by every caller. 

934 

935 An incomplete record is refused for the same reason an unknown command is: a ``COMMAND`` 

936 reply that carries no tips cannot express ``nondeterministic_output`` or ``dont_cache``, 

937 so a record built from one does not prove the command is cacheable. Note that a resolver 

938 answers from the first record it finds, so an incomplete record decides the command even 

939 when a complete one sits behind it in the chain - an override table therefore belongs in 

940 front of the resolver that may serve incomplete records, not behind it. 

941 """ 

942 if metadata is None or not metadata.has_complete_metadata: 

943 return False 

944 

945 # A negative override: the server states the reply must not be cached, which decides on 

946 # its own even when every positive rule matches. 

947 if metadata.is_dont_cache: 

948 return False 

949 

950 if not metadata.is_readonly: 

951 return False 

952 

953 if metadata.is_blocking: 

954 return False 

955 

956 if not metadata.has_key_argument: 

957 return False 

958 

959 if metadata.has_nondeterministic_output: 

960 return False 

961 

962 if metadata.is_script_runner: 

963 return False 

964 

965 return True 

966 

967 

968def _is_replica_safe(metadata: CommandMetadata | None) -> bool: 

969 """ 

970 Decide whether a command is safe to execute on a replica based on the readonly flag. 

971 

972 Takes ``None`` - what an exhausted resolver chain resolves to - so the 

973 unknown-command case is decided here as well. 

974 """ 

975 if metadata is None: 

976 return False 

977 

978 return metadata.is_readonly 

979 

980 

981def _is_trackable_read(metadata: CommandMetadata | None) -> bool: 

982 """ 

983 Decide whether the server would remember the keys of the command a record describes. 

984 

985 The ``readonly`` command flag alone, because that plus the key names of the invocation is 

986 the whole gate the server applies before it records a read against a tracking client. The 

987 negative signals that make a command ineligible to store do not enter it: ``XPENDING`` is 

988 tipped ``nondeterministic_output`` and ``TOUCH`` is tipped ``dont_cache``, and the server 

989 tracks both. 

990 

991 Deliberately does not require ``has_complete_metadata``, unlike 

992 :func:`_is_client_side_cacheable`: the readonly flag comes from the command flags, which 

993 every ``COMMAND`` reply carries, and only the tips can be missing. 

994 

995 Takes ``None`` - what an exhausted resolver chain resolves to - so the unknown-command 

996 case is decided here as well. 

997 """ 

998 if metadata is None: 

999 return False 

1000 

1001 return metadata.is_readonly 

1002 

1003 

1004def _build_commands_metadata_cache_from_policies( 

1005 policy_records: PolicyRecords, 

1006) -> CommandMetadataRecordsCache: 

1007 """ 

1008 Build the metadata records cache from policy records. 

1009 

1010 Unlike ``_build_commands_metadata_cache``, which builds the cache from a raw ``COMMAND`` 

1011 reply, this lifts the narrower 7.1.0 routing view into the same shape. 

1012 

1013 Only the routing policies of each command are known, so every other field keeps its 

1014 fail-closed default. This backs the ``PolicyRecords`` arguments of the policy resolvers: 

1015 a resolver built from policy records serves exactly the policies it was given, and 

1016 reports no command as client-side-cacheable, which is the conservative answer for 

1017 metadata that was never supplied. 

1018 

1019 Keys are lowercased on the way through - the records are rebuilt here anyway - because 

1020 that is how a resolved command name is looked up. 

1021 """ 

1022 return { 

1023 module_name.lower(): { 

1024 command_name.lower(): CommandMetadata( 

1025 request_policy=policies.request_policy, 

1026 response_policy=policies.response_policy, 

1027 ) 

1028 for command_name, policies in commands.items() 

1029 } 

1030 for module_name, commands in policy_records.items() 

1031 } 

1032 

1033 

1034def _load_commands_metadata_cache( 

1035 commands_parser: object, 

1036) -> CommandMetadataRecordsCache: 

1037 """ 

1038 Load the metadata records cache of a ``COMMAND`` parser. 

1039 

1040 ``get_commands_metadata_cache`` is what a parser serves, and is what this loads. 

1041 

1042 The ``get_command_policies`` branch is a backwards-compatibility shim, not a second 

1043 supported shape. ``DynamicPolicyResolver`` called nothing but that method in 7.1.0, so code 

1044 that passed an object duck-typing it would otherwise break at construction; the branch keeps 

1045 that code working and nothing more. It is deliberately not advertised as an extension point: 

1046 ``CommandsParser`` lives in the private ``redis._parsers`` package, and the supported way to 

1047 decide routing yourself is to implement the public ``PolicyResolver`` ABC and pass it as the 

1048 ``policy_resolver`` of a cluster client. Policy records also carry no cacheability metadata, 

1049 so everything a parser reaching this branch serves reports as non-cacheable. 

1050 

1051 The async stack needs no counterpart: ``AsyncDynamicMetadataResolver`` takes records rather 

1052 than a parser, and ``AsyncDynamicPolicyResolver`` still accepts ``policy_records`` directly. 

1053 

1054 Raises: 

1055 TypeError: If the parser serves neither method. 

1056 """ 

1057 get_metadata = getattr(commands_parser, "get_commands_metadata_cache", None) 

1058 if get_metadata is not None: 

1059 return get_metadata() 

1060 

1061 get_policies = getattr(commands_parser, "get_command_policies", None) 

1062 if get_policies is None: 

1063 raise TypeError( 

1064 f"{type(commands_parser).__name__} serves neither get_commands_metadata_cache() nor " 

1065 "get_command_policies(); a commands parser must serve one of them" 

1066 ) 

1067 

1068 return _build_commands_metadata_cache_from_policies(get_policies()) 

1069 

1070 

1071# The records the cluster clients fall back to when the policy resolver does not know a 

1072# command, which is the common case: the default resolver is backed by the static table 

1073# below, so every write and every read outside it falls through on every execution. Reused 

1074# rather than constructed there, both to keep an allocation off that path and because a 

1075# frozen record is safe to share. 

1076# 

1077# Only the routing policies are known for a command the client had to fall back on, so 

1078# every other field is recorded at its fail-closed value and none of these reports as 

1079# cacheable. Spelled out rather than left to the dataclass defaults, for the reason given 

1080# on the shared table shapes below. 

1081_DEFAULT_KEYLESS_METADATA = CommandMetadata( 

1082 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

1083 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

1084 is_readonly=False, 

1085 is_blocking=False, 

1086 has_key_argument=False, 

1087 has_nondeterministic_output=False, 

1088 is_script_runner=False, 

1089 is_dont_cache=False, 

1090 has_complete_metadata=False, 

1091) 

1092_DEFAULT_KEYED_METADATA = CommandMetadata( 

1093 request_policy=RequestPolicy.DEFAULT_KEYED, 

1094 response_policy=ResponsePolicy.DEFAULT_KEYED, 

1095 is_readonly=False, 

1096 is_blocking=False, 

1097 has_key_argument=False, 

1098 has_nondeterministic_output=False, 

1099 is_script_runner=False, 

1100 is_dont_cache=False, 

1101 has_complete_metadata=False, 

1102) 

1103 

1104# Same, for the node-flag fallback, which resolves a request policy and leaves the response 

1105# policy at its keyless default. 

1106_METADATA_BY_REQUEST_POLICY: Mapping[RequestPolicy, CommandMetadata] = MappingProxyType( 

1107 { 

1108 policy: CommandMetadata( 

1109 request_policy=policy, 

1110 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

1111 is_readonly=False, 

1112 is_blocking=False, 

1113 has_key_argument=False, 

1114 has_nondeterministic_output=False, 

1115 is_script_runner=False, 

1116 is_dont_cache=False, 

1117 has_complete_metadata=False, 

1118 ) 

1119 for policy in RequestPolicy 

1120 } 

1121) 

1122 

1123 

1124# Every record in this module spells out all nine fields, including the ones that happen to 

1125# equal the dataclass default. The nine are not interchangeable: seven of them 

1126# (``is_readonly``, ``is_blocking``, ``has_key_argument``, ``has_nondeterministic_output``, 

1127# ``is_script_runner``, ``is_dont_cache``, ``has_complete_metadata``) are the inputs to 

1128# ``_is_client_side_cacheable``, so a shape that leaves one implicit lets a command added 

1129# for an unrelated reason - replica-safe read routing, say, which needs only ``is_readonly`` 

1130# - become client-side cacheable as a side effect, with nothing in the diff saying so. 

1131# Listing every field makes each new entry state its own cacheability. 

1132# 

1133# Shapes derived with ``dataclasses.replace`` below inherit from a fully explicit base, so 

1134# they name only the field that differs and still leave nothing to a default. 

1135# 

1136# Record shared by every command the client-side cache may serve: readonly, takes a key 

1137# name argument, and reports nothing that forbids caching. Spelled out once because the 

1138# vast majority of the table below is one of these shapes. 

1139_CACHEABLE_KEYED = CommandMetadata( 

1140 request_policy=RequestPolicy.DEFAULT_KEYED, 

1141 response_policy=ResponsePolicy.DEFAULT_KEYED, 

1142 is_readonly=True, 

1143 is_blocking=False, 

1144 has_key_argument=True, 

1145 has_nondeterministic_output=False, 

1146 is_script_runner=False, 

1147 is_dont_cache=False, 

1148 has_complete_metadata=True, 

1149) 

1150 

1151# Same, for the ``movablekeys`` commands, with the routing policies withheld. Their keys are 

1152# only discoverable through key specs, so ``first_key_pos`` is 0 and the derived policies come 

1153# out keyless even though the command genuinely takes keys. This divergence is exactly why 

1154# ``has_key_argument`` is recorded separately from ``request_policy``. 

1155# 

1156# Recording those derived policies here would route the command to an arbitrary node, so they 

1157# are withheld: a policy resolver then reports the command as unresolved and the cluster client 

1158# resolves the target itself through ``determine_slot``, which asks the server for the keys with 

1159# ``COMMAND GETKEYS``. That is the only path that finds the keys of a ``movablekeys`` command, 

1160# and it is what the routing fallback did before this table backed the static resolver. 

1161# TODO(pslavova): record the real request/response policies once routing derives keyed/keyless 

1162# from the key specs (``has_key_argument``) rather than from ``first_key_pos``. 

1163_CACHEABLE_MOVABLE_KEYS = CommandMetadata( 

1164 request_policy=None, 

1165 response_policy=None, 

1166 is_readonly=True, 

1167 is_blocking=False, 

1168 has_key_argument=True, 

1169 has_nondeterministic_output=False, 

1170 is_script_runner=False, 

1171 is_dont_cache=False, 

1172 has_complete_metadata=True, 

1173) 

1174 

1175# Same, for a command that also carries the ``blocking`` flag, which makes it ineligible: 

1176# its reply is what the caller waited for rather than a snapshot the cache may re-serve. 

1177# XREAD is the only readonly, keyed command the flag excludes. The withheld routing policies 

1178# are inherited: XREAD is ``movablekeys`` too, so it must be routed by its resolved keys. 

1179_BLOCKING_MOVABLE_KEYS = replace(_CACHEABLE_MOVABLE_KEYS, is_blocking=True) 

1180 

1181# Same, for the read-only script runners - EVAL_RO, EVALSHA_RO, FCALL_RO. The 

1182# ``script_runner`` flag makes them ineligible: the reply is whatever the script computed, 

1183# which the client cannot tie to the keys the script happened to touch. They are 

1184# ``movablekeys`` too - their keys are counted by ``numkeys`` - so the withheld routing 

1185# policies are inherited for the same reason. 

1186# 

1187# Recorded rather than left absent because the flag only reaches the client from Redis 8.10 

1188# on: on 7.4.x-8.8.x all three report readonly and keyed with no flag that excludes them, 

1189# i.e. cacheable, so a resolver reading a live COMMAND reply from one of those servers would 

1190# admit them. This record states what the command is, independently of what a given server 

1191# version reports. Removable once the minimum CSC-supported server reports ``script_runner``. 

1192_SCRIPT_RUNNER_MOVABLE_KEYS = replace(_CACHEABLE_MOVABLE_KEYS, is_script_runner=True) 

1193 

1194# Readonly but keyless, so not cacheable: the command observes keyspace state without 

1195# taking a key name argument. Note that this is what makes a search or aggregation query 

1196# uncacheable - being a module command is not itself a reason to exclude one. 

1197_READONLY_KEYLESS = CommandMetadata( 

1198 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

1199 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

1200 is_readonly=True, 

1201 is_blocking=False, 

1202 has_key_argument=False, 

1203 has_nondeterministic_output=False, 

1204 is_script_runner=False, 

1205 is_dont_cache=False, 

1206 has_complete_metadata=True, 

1207) 

1208 

1209# Readonly and keyless with routing policies withheld, so the cluster client keeps 

1210# resolving its target node via COMMAND_FLAGS (e.g. DEFAULT_NODE for DBSIZE/KEYS/RANDOMKEY 

1211# or PRIMARIES for SCAN). 

1212_READONLY_KEYLESS_WITHHELD_ROUTING = CommandMetadata( 

1213 request_policy=None, 

1214 response_policy=None, 

1215 is_readonly=True, 

1216 is_blocking=False, 

1217 has_key_argument=False, 

1218 has_nondeterministic_output=False, 

1219 is_script_runner=False, 

1220 is_dont_cache=False, 

1221 has_complete_metadata=True, 

1222) 

1223 

1224# Readonly and keyed, but tipped nondeterministic_output, which makes it ineligible 

1225# for client-side caching. 

1226_NONDETERMINISTIC_KEYED = replace(_CACHEABLE_KEYED, has_nondeterministic_output=True) 

1227 

1228# Same tip on the keyless reads that withhold their routing. Carries no cacheability 

1229# consequence of its own - a keyless command is already ineligible for want of a key 

1230# argument - but the table records what the server reports, and both RANDOMKEY and SCAN are 

1231# tipped ``nondeterministic_output``: one returns an arbitrary key, the other an arbitrary 

1232# page of the keyspace. 

1233_NONDETERMINISTIC_KEYLESS_WITHHELD_ROUTING = replace( 

1234 _READONLY_KEYLESS_WITHHELD_ROUTING, has_nondeterministic_output=True 

1235) 

1236 

1237# Write commands. Rule 1 excludes them, so nothing else about them matters to the cache. 

1238_WRITE_KEYLESS = CommandMetadata( 

1239 request_policy=RequestPolicy.DEFAULT_KEYLESS, 

1240 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

1241 is_readonly=False, 

1242 is_blocking=False, 

1243 has_key_argument=False, 

1244 has_nondeterministic_output=False, 

1245 is_script_runner=False, 

1246 is_dont_cache=False, 

1247 has_complete_metadata=True, 

1248) 

1249 

1250_WRITE_KEYLESS_WITHHELD_ROUTING = CommandMetadata( 

1251 request_policy=None, 

1252 response_policy=None, 

1253 is_readonly=False, 

1254 is_blocking=False, 

1255 has_key_argument=False, 

1256 has_nondeterministic_output=False, 

1257 is_script_runner=False, 

1258 is_dont_cache=False, 

1259 has_complete_metadata=True, 

1260) 

1261 

1262_WRITE_KEYED = CommandMetadata( 

1263 request_policy=RequestPolicy.DEFAULT_KEYED, 

1264 response_policy=ResponsePolicy.DEFAULT_KEYED, 

1265 is_readonly=False, 

1266 is_blocking=False, 

1267 has_key_argument=True, 

1268 has_nondeterministic_output=False, 

1269 is_script_runner=False, 

1270 is_dont_cache=False, 

1271 has_complete_metadata=True, 

1272) 

1273 

1274# The same three shapes for the commands the server tips ``dont_cache``. The search module 

1275# tips nearly its whole surface that way, including the write commands, where the marker is 

1276# redundant - rule 1 already excludes them. It is recorded anyway so the table keeps 

1277# reporting what the server reports. 

1278_DONT_CACHE_READONLY_KEYLESS = replace(_READONLY_KEYLESS, is_dont_cache=True) 

1279_DONT_CACHE_WRITE_KEYLESS = replace(_WRITE_KEYLESS, is_dont_cache=True) 

1280_DONT_CACHE_WRITE_KEYED = replace(_WRITE_KEYED, is_dont_cache=True) 

1281 

1282 

1283# ============================================================================= 

1284# Static command metadata table 

1285# ============================================================================= 

1286# This table is seeded from old ``CacheConfig.DEFAULT_ALLOW_LIST`` and ``redis.commands.policies.STATIC_POLICIES``. 

1287# 

1288# Every entry below was validated against a live ``COMMAND`` reply from Redis 8.10.0 with 

1289# search, timeseries, ReJSON, bf and vectorset loaded (the ``redislabs/client-libs-test`` 

1290# stack image), with the core entries also compared against 7.4.2 to ensure the minimum 

1291# supported version does not disagree on the fields that decide cacheability. The one 

1292# exception is the ``bless`` container, which first ships in 8.11 and was validated against 

1293# that release's ``COMMAND INFO`` reply. 

1294# 

1295# The table is not exhaustive: on 8.10.0 the server reports 127 cacheable commands and this 

1296# covers 80 of them. The count is version-qualified because it moves between releases - a 

1297# later server reports more. The uncovered ones are whole module surfaces the CSC allow-list 

1298# never carried - ``bf.*``, ``cf.*``, ``cms.*``, ``topk.*``, ``tdigest.*``, the vectorset 

1299# reads - plus core reads such as ``pfcount``. A command absent from the table fails closed, 

1300# so the uncovered commands are simply treated as not cacheable. 

1301# 

1302# Every command on the default allow-list of the 7.1.0 client-side cache is present. 

1303# So are all of their module counterparts, plus a selection of write commands. 

1304# Write commands carry the metadata that makes them *ineligible*, so the reason 

1305# is documented in one place rather than inferred from an absence - and, because 

1306# a resolver answers from the first record it finds, so that a live resolver 

1307# chained behind this one cannot re-admit them. ``xpending``, ``ts.info`` and 

1308# ``xread`` are on today's ``CacheConfig.DEFAULT_ALLOW_LIST``, and the server 

1309# confirms all three are defects in that list: ``xpending`` is tipped 

1310# ``nondeterministic_output``, ``ts.info`` ``dont_cache``, and ``xread`` carries 

1311# the ``blocking`` command flag. 

1312# 

1313# Five entries diverge from the live reply, each for a reason spelled out where it occurs: 

1314# ``exists`` and ``mget`` stand in the keyed defaults for the unimplemented ``multi_shard`` 

1315# tips, ``ft.cursor`` is SPECIAL, a client-side decision the server does not tip, ``touch`` is 

1316# recorded ``dont_cache`` where the server reports it cacheable, and ``vrandmember`` is 

1317# recorded ``nondeterministic_output`` where the server tips it nothing. The ``movablekeys`` 

1318# reads - ``eval_ro``, ``evalsha_ro``, ``fcall_ro``, ``sdiffcard``, ``sintercard``, 

1319# ``sunioncard``, ``xread``, ``zdiff``, ``zinter``, ``zintercard`` and ``zunion`` - plus 

1320# ``bless scan``, ``command``, ``dbsize``, ``keys``, ``randomkey``, ``scan``, ``touch`` and 

1321# ``vrandmember`` withhold their routing policies entirely, so the cluster client keeps 

1322# resolving their targets itself. Their cacheability inputs are unaffected. 

1323_STATIC_COMMAND_METADATA: CommandMetadataRecordsCache = MappingProxyType( 

1324 { 

1325 "core": MappingProxyType( 

1326 { 

1327 "bitcount": _CACHEABLE_KEYED, 

1328 "bitfield_ro": _CACHEABLE_KEYED, 

1329 "bitpos": _CACHEABLE_KEYED, 

1330 # The BLESS container, keyed by subcommand the way ``execute_command`` 

1331 # receives it. Validated against the 8.11 ``COMMAND INFO`` reply, the first 

1332 # release that reports it. 

1333 "bless clear": _WRITE_KEYED, 

1334 # Not cacheable and not replica-eligible: the server flags BLESS GET ``fast`` 

1335 # only - neither ``readonly`` nor ``write`` - and ``is_readonly`` records the 

1336 # command flag, not the ``RO`` key spec, so it fails closed on both counts. 

1337 # Recorded with the write shape for the same reason ``command`` is below. 

1338 "bless get": _WRITE_KEYED, 

1339 # Keyless, tipped nondeterministic_output and request_policy:special / 

1340 # response_policy:special, exactly like SCAN. Routing policies are withheld 

1341 # for the same reason as SCAN: the cluster client routes BLESS SCAN to all 

1342 # primary nodes (PRIMARIES) through its COMMAND_FLAGS entry and merges the 

1343 # per-node cursors. Unlike SCAN the server reports no flags at all, so it is 

1344 # not readonly. 

1345 "bless scan": CommandMetadata( 

1346 request_policy=None, 

1347 response_policy=None, 

1348 is_readonly=False, 

1349 is_blocking=False, 

1350 has_key_argument=False, 

1351 has_nondeterministic_output=True, 

1352 is_script_runner=False, 

1353 is_dont_cache=False, 

1354 has_complete_metadata=True, 

1355 ), 

1356 "bless set": _WRITE_KEYED, 

1357 # From STATIC_POLICIES. COMMAND is flagged loading/stale, not readonly, 

1358 # and takes no keys. Routing policies are withheld so the cluster client preserves 

1359 # the 2-word COMMAND COUNT, COMMAND LIST, COMMAND GETKEYS default-node flags. 

1360 "command": _WRITE_KEYLESS_WITHHELD_ROUTING, 

1361 "dbsize": _READONLY_KEYLESS_WITHHELD_ROUTING, 

1362 "digest": _CACHEABLE_KEYED, 

1363 "dump": _NONDETERMINISTIC_KEYED, 

1364 # Not cacheable: script_runner. See the shape's note for why all three are 

1365 # recorded rather than left to the server to report. 

1366 "eval_ro": _SCRIPT_RUNNER_MOVABLE_KEYS, 

1367 "evalsha_ro": _SCRIPT_RUNNER_MOVABLE_KEYS, 

1368 # The server tips EXISTS request_policy:multi_shard and 

1369 # response_policy:agg_sum, but ``RequestPolicy.MULTI_SHARD`` is not 

1370 # implemented in either client stack: ``_split_multi_shard_command`` 

1371 # returns per-key command descriptors where every caller of 

1372 # ``_determine_nodes`` expects ``ClusterNode``, and the async client has no 

1373 # MULTI_SHARD entry in ``_policies_callback_mapping`` at all. Recording the 

1374 # real tips here therefore breaks the command, so the keyed defaults stand 

1375 # in - which is what the routing fallback resolved to before this table 

1376 # backed the static resolver. 

1377 # TODO(pslavova): record the real tips once MULTI_SHARD is implemented 

1378 # across both stacks (main path and pipeline). 

1379 "exists": _CACHEABLE_KEYED, 

1380 "expiretime": _CACHEABLE_KEYED, 

1381 "fcall_ro": _SCRIPT_RUNNER_MOVABLE_KEYS, 

1382 "geodist": _CACHEABLE_KEYED, 

1383 "geohash": _CACHEABLE_KEYED, 

1384 "geopos": _CACHEABLE_KEYED, 

1385 "georadius_ro": _CACHEABLE_KEYED, 

1386 "georadiusbymember_ro": _CACHEABLE_KEYED, 

1387 "geosearch": _CACHEABLE_KEYED, 

1388 "get": _CACHEABLE_KEYED, 

1389 "getbit": _CACHEABLE_KEYED, 

1390 "getrange": _CACHEABLE_KEYED, 

1391 "hexists": _CACHEABLE_KEYED, 

1392 "hexpiretime": _CACHEABLE_KEYED, 

1393 "hget": _CACHEABLE_KEYED, 

1394 # HGETALL, HKEYS, HVALS, SDIFF, SINTER, SMEMBERS and SUNION all carry 

1395 # nondeterministic_output_order, which is a different tip from 

1396 # nondeterministic_output and does not prevent caching. Matching tips by 

1397 # prefix would silently drop all seven. 

1398 "hgetall": _CACHEABLE_KEYED, 

1399 "hkeys": _CACHEABLE_KEYED, 

1400 "hlen": _CACHEABLE_KEYED, 

1401 "hmget": _CACHEABLE_KEYED, 

1402 "hpexpiretime": _CACHEABLE_KEYED, 

1403 "hpttl": _NONDETERMINISTIC_KEYED, 

1404 "hrandfield": _NONDETERMINISTIC_KEYED, 

1405 "hscan": _NONDETERMINISTIC_KEYED, 

1406 "hstrlen": _CACHEABLE_KEYED, 

1407 "httl": _NONDETERMINISTIC_KEYED, 

1408 "hvals": _CACHEABLE_KEYED, 

1409 "keys": _READONLY_KEYLESS_WITHHELD_ROUTING, 

1410 "lcs": _CACHEABLE_KEYED, 

1411 "lindex": _CACHEABLE_KEYED, 

1412 "llen": _CACHEABLE_KEYED, 

1413 "lpos": _CACHEABLE_KEYED, 

1414 "lrange": _CACHEABLE_KEYED, 

1415 # Tipped request_policy:multi_shard by the server, withheld for the reason 

1416 # spelled out on ``exists`` above. 

1417 # TODO: record the real tip once MULTI_SHARD is implemented. 

1418 "mget": _CACHEABLE_KEYED, 

1419 "pexpiretime": _CACHEABLE_KEYED, 

1420 "pttl": _NONDETERMINISTIC_KEYED, 

1421 "randomkey": _NONDETERMINISTIC_KEYLESS_WITHHELD_ROUTING, 

1422 # Readonly and keyless. Routing policies are withheld so the cluster client keeps 

1423 # routing SCAN to all primary nodes (PRIMARIES) rather than a single random node. 

1424 "scan": _NONDETERMINISTIC_KEYLESS_WITHHELD_ROUTING, 

1425 "scard": _CACHEABLE_KEYED, 

1426 "sdiff": _CACHEABLE_KEYED, 

1427 "sdiffcard": _CACHEABLE_MOVABLE_KEYS, 

1428 "sinter": _CACHEABLE_KEYED, 

1429 "sintercard": _CACHEABLE_MOVABLE_KEYS, 

1430 "sismember": _CACHEABLE_KEYED, 

1431 "smembers": _CACHEABLE_KEYED, 

1432 "smismember": _CACHEABLE_KEYED, 

1433 # Unreachable today: ``CoreCommands.sort_ro`` delegates to ``sort()``, which 

1434 # sends SORT rather than SORT_RO, so nothing resolves this entry. Recorded 

1435 # because SORT_RO is genuinely cacheable, and the old allow-list carried it 

1436 # just as ineffectively. 

1437 # TODO: drop this note once the command method sends its own name. 

1438 "sort_ro": _CACHEABLE_KEYED, 

1439 "srandmember": _NONDETERMINISTIC_KEYED, 

1440 "sscan": _NONDETERMINISTIC_KEYED, 

1441 "strlen": _CACHEABLE_KEYED, 

1442 "substr": _CACHEABLE_KEYED, 

1443 "sunion": _CACHEABLE_KEYED, 

1444 "sunioncard": _CACHEABLE_MOVABLE_KEYS, 

1445 # Not cacheable, and the one entry in this table that records a client-side 

1446 # judgement instead of what the server reports: TOUCH has a server-side 

1447 # effect - it refreshes each key's idle time, which is what LRU/LFU 

1448 # eviction and OBJECT IDLETIME read - so it must reach the server on every 

1449 # call. No server flag or tip expresses that; measured on 8.10.0 the server 

1450 # reports it readonly and keyed with no ``dont_cache`` tip, i.e. cacheable. 

1451 # Recorded here as ``dont_cache`` so the reason is stated once rather than 

1452 # left to every resolver in a chain. Removable once the server tips it 

1453 # ``dont_cache``. 

1454 # 

1455 # Routing policies are withheld, not defaulted: the server tips TOUCH 

1456 # request_policy:multi_shard / response_policy:agg_sum, and MULTI_SHARD is 

1457 # unimplemented for the reason spelled out on ``exists`` above. Withholding 

1458 # keeps the cluster client resolving the target itself, exactly as it does 

1459 # today for a command this table does not carry. 

1460 "touch": CommandMetadata( 

1461 request_policy=None, 

1462 response_policy=None, 

1463 is_readonly=True, 

1464 is_blocking=False, 

1465 has_key_argument=True, 

1466 has_nondeterministic_output=False, 

1467 is_script_runner=False, 

1468 is_dont_cache=True, 

1469 has_complete_metadata=True, 

1470 ), 

1471 "ttl": _NONDETERMINISTIC_KEYED, 

1472 "type": _CACHEABLE_KEYED, 

1473 # Not cacheable: its reply is a random sample of the vector set, so re-serving 

1474 # it from a cache would stop it varying. Every core random read - SRANDMEMBER, 

1475 # ZRANDMEMBER, HRANDFIELD, RANDOMKEY - is tipped ``nondeterministic_output`` 

1476 # by the server; measured on 8.10.0 VRANDMEMBER reports no tips at all, so the 

1477 # algorithm would admit it. Recorded as nondeterministic because that is what 

1478 # it is, and it is the one other command node-redis hard-codes ineligible 

1479 # besides TOUCH. Removable once the server tips it like its core siblings. 

1480 # 

1481 # Routing policies are withheld for the same reason as ``touch`` above: the 

1482 # record exists for its cacheability inputs, and must not start routing a 

1483 # command the cluster client resolves for itself today. 

1484 "vrandmember": CommandMetadata( 

1485 request_policy=None, 

1486 response_policy=None, 

1487 is_readonly=True, 

1488 is_blocking=False, 

1489 has_key_argument=True, 

1490 has_nondeterministic_output=True, 

1491 is_script_runner=False, 

1492 is_dont_cache=False, 

1493 has_complete_metadata=True, 

1494 ), 

1495 "xlen": _CACHEABLE_KEYED, 

1496 # Not cacheable: nondeterministic_output. It is on today's allow-list, 

1497 # which is a defect in that list rather than a reason to keep caching it. 

1498 "xpending": _NONDETERMINISTIC_KEYED, 

1499 "xrange": _CACHEABLE_KEYED, 

1500 "xread": _BLOCKING_MOVABLE_KEYS, 

1501 "xrevrange": _CACHEABLE_KEYED, 

1502 "zcard": _CACHEABLE_KEYED, 

1503 "zcount": _CACHEABLE_KEYED, 

1504 "zdiff": _CACHEABLE_MOVABLE_KEYS, 

1505 "zinter": _CACHEABLE_MOVABLE_KEYS, 

1506 "zintercard": _CACHEABLE_MOVABLE_KEYS, 

1507 "zlexcount": _CACHEABLE_KEYED, 

1508 "zmscore": _CACHEABLE_KEYED, 

1509 "zrange": _CACHEABLE_KEYED, 

1510 "zrangebylex": _CACHEABLE_KEYED, 

1511 "zrangebyscore": _CACHEABLE_KEYED, 

1512 "zrank": _CACHEABLE_KEYED, 

1513 "zrandmember": _NONDETERMINISTIC_KEYED, 

1514 "zrevrange": _CACHEABLE_KEYED, 

1515 "zrevrangebylex": _CACHEABLE_KEYED, 

1516 "zrevrangebyscore": _CACHEABLE_KEYED, 

1517 "zrevrank": _CACHEABLE_KEYED, 

1518 "zscan": _NONDETERMINISTIC_KEYED, 

1519 "zscore": _CACHEABLE_KEYED, 

1520 "zunion": _CACHEABLE_MOVABLE_KEYS, 

1521 } 

1522 ), 

1523 "json": MappingProxyType( 

1524 { 

1525 "arrindex": _CACHEABLE_KEYED, 

1526 "arrlen": _CACHEABLE_KEYED, 

1527 "get": _CACHEABLE_KEYED, 

1528 "mget": _CACHEABLE_KEYED, 

1529 "objkeys": _CACHEABLE_KEYED, 

1530 "objlen": _CACHEABLE_KEYED, 

1531 "resp": _CACHEABLE_KEYED, 

1532 "strlen": _CACHEABLE_KEYED, 

1533 "type": _CACHEABLE_KEYED, 

1534 } 

1535 ), 

1536 "ts": MappingProxyType( 

1537 { 

1538 "get": _CACHEABLE_KEYED, 

1539 # Not cacheable: the server reports dont_cache for TS.INFO, contradicting 

1540 # today's allow-list, which caches it. 

1541 "info": CommandMetadata( 

1542 request_policy=RequestPolicy.DEFAULT_KEYED, 

1543 response_policy=ResponsePolicy.DEFAULT_KEYED, 

1544 is_readonly=True, 

1545 is_blocking=False, 

1546 has_key_argument=True, 

1547 has_nondeterministic_output=False, 

1548 is_script_runner=False, 

1549 is_dont_cache=True, 

1550 has_complete_metadata=True, 

1551 ), 

1552 "range": _CACHEABLE_KEYED, 

1553 "revrange": _CACHEABLE_KEYED, 

1554 } 

1555 ), 

1556 # From STATIC_POLICIES. Only the suggestion-dictionary commands take a key name 

1557 # argument, so FT.SUGGET and FT.SUGLEN are the only cacheable entries here - 

1558 # FT.SEARCH and FT.AGGREGATE are readonly but keyless. 

1559 # 

1560 # The search module tips almost its whole surface ``dont_cache``. The exceptions 

1561 # are FT.CURSOR, FT.DROP, FT.SUGGET and FT.SUGLEN, which report no tips at all - 

1562 # which is what keeps the two suggestion-dictionary reads cacheable. 

1563 "ft": MappingProxyType( 

1564 { 

1565 "aggregate": _DONT_CACHE_READONLY_KEYLESS, 

1566 "aliasadd": _DONT_CACHE_WRITE_KEYLESS, 

1567 "aliasdel": _DONT_CACHE_WRITE_KEYLESS, 

1568 "aliaslist": _DONT_CACHE_READONLY_KEYLESS, 

1569 "aliasupdate": _DONT_CACHE_WRITE_KEYLESS, 

1570 "alter": _DONT_CACHE_WRITE_KEYLESS, 

1571 "create": _DONT_CACHE_WRITE_KEYLESS, 

1572 # SPECIAL is a client-side routing decision, not a server tip: FT.CURSOR 

1573 # must reach the node that ran the FT.AGGREGATE it continues, which 

1574 # ``get_special_nodes`` resolves. The server reports no tips for it, so a 

1575 # generated table would say DEFAULT_KEYLESS and break cursor routing. 

1576 "cursor": CommandMetadata( 

1577 request_policy=RequestPolicy.SPECIAL, 

1578 response_policy=ResponsePolicy.DEFAULT_KEYLESS, 

1579 is_readonly=True, 

1580 is_blocking=False, 

1581 has_key_argument=False, 

1582 has_nondeterministic_output=False, 

1583 is_script_runner=False, 

1584 is_dont_cache=False, 

1585 has_complete_metadata=True, 

1586 ), 

1587 "dictadd": _DONT_CACHE_WRITE_KEYLESS, 

1588 "dictdel": _DONT_CACHE_WRITE_KEYLESS, 

1589 "dictdump": _DONT_CACHE_READONLY_KEYLESS, 

1590 "drop": _WRITE_KEYLESS, 

1591 "dropindex": _DONT_CACHE_WRITE_KEYLESS, 

1592 "explain": _DONT_CACHE_READONLY_KEYLESS, 

1593 "explaincli": _DONT_CACHE_READONLY_KEYLESS, 

1594 "info": _DONT_CACHE_READONLY_KEYLESS, 

1595 "profile": _DONT_CACHE_READONLY_KEYLESS, 

1596 "search": _DONT_CACHE_READONLY_KEYLESS, 

1597 "spellcheck": _DONT_CACHE_READONLY_KEYLESS, 

1598 "sugadd": _DONT_CACHE_WRITE_KEYED, 

1599 "sugdel": _DONT_CACHE_WRITE_KEYED, 

1600 "sugget": _CACHEABLE_KEYED, 

1601 "suglen": _CACHEABLE_KEYED, 

1602 "syndump": _DONT_CACHE_READONLY_KEYLESS, 

1603 "synupdate": _DONT_CACHE_WRITE_KEYLESS, 

1604 "tagvals": _DONT_CACHE_READONLY_KEYLESS, 

1605 } 

1606 ), 

1607 } 

1608)