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 with_fallback(self, fallback: "MetadataResolver") -> "MetadataResolver":
356 """
357 Factory method to instantiate a metadata resolver with a fallback resolver.
358
359 Args:
360 fallback: Fallback resolver
361
362 Returns:
363 MetadataResolver: Returns a new metadata resolver with the specified fallback resolver.
364 """
365 pass
366
367
368class AsyncMetadataResolver(ABC):
369 @abstractmethod
370 async def resolve(self, command_name: str) -> CommandMetadata | None:
371 """
372 Resolves the command name and determines the associated command metadata.
373
374 Args:
375 command_name: The name of the command to resolve, in any case.
376
377 Returns:
378 CommandMetadata: The metadata associated with the specified command, or None
379 when no resolver in the chain knows the command.
380 """
381 pass
382
383 @abstractmethod
384 async def resolve_policies(self, command_name: str) -> CommandPolicies | None:
385 """
386 Resolves the command name and determines the associated routing policies.
387
388 The routing view of :meth:`resolve`: the request/response policies the resolved
389 record carries, and nothing else about it.
390
391 Args:
392 command_name: The name of the command to resolve, in any case.
393
394 Returns:
395 CommandPolicies: The policies associated with the specified command, or None
396 when no resolver in the chain knows the command.
397 """
398 pass
399
400 @abstractmethod
401 async def is_cacheable(self, command_name: str) -> bool:
402 """
403 Determines whether the reply of a command may be served from a client-side cache.
404
405 The client-side-caching view of :meth:`resolve`, decided by
406 :func:`_is_client_side_cacheable`. Fails closed: an unknown command, a name the
407 record tables cannot be keyed by, and a record built from incomplete metadata all
408 resolve to False.
409
410 Args:
411 command_name: The name of the command to check, in any case.
412
413 Returns:
414 bool: True only when every eligibility rule is satisfied.
415 """
416 pass
417
418 @abstractmethod
419 async def is_replica_safe(self, command_name: str) -> bool:
420 """
421 Determines whether a command is safe to execute on a replica.
422
423 Args:
424 command_name: The name of the command to check, in any case.
425
426 Returns:
427 bool: True if the command is replica safe.
428 """
429 pass
430
431 @abstractmethod
432 def with_fallback(
433 self, fallback: "AsyncMetadataResolver"
434 ) -> "AsyncMetadataResolver":
435 """
436 Factory method to instantiate an async metadata resolver with a fallback resolver.
437
438 Args:
439 fallback: Fallback resolver
440
441 Returns:
442 AsyncMetadataResolver: Returns a new metadata resolver with the specified fallback resolver.
443 """
444 pass
445
446
447class BaseMetadataResolver(MetadataResolver):
448 """
449 Base class for metadata resolvers.
450
451 Lookup is first-match-wins: a command the records do not carry falls through to the
452 fallback resolver, and a chain that ends without a match resolves to None. Besides the
453 whole record, each consumer gets the view it needs of the same resolved metadata:
454 ``resolve_policies`` for cluster routing, ``is_cacheable`` for client-side caching.
455
456 Both views are memoized under the lowercased command name, so every spelling of one
457 command - ``GET`` as the command methods write it, ``get`` as the cluster client lowers it
458 - shares a single entry instead of taking one each. The metadata a resolver serves is a
459 snapshot taken when the resolver is built, so a command's view cannot change, and the memo
460 keeps the record lookup, the projection and the walk down the fallback chain off the
461 command execution path. Concurrent resolves of the same command may each compute it once;
462 the memo is idempotent, so the duplicated work is harmless. The memos grow with the set of
463 distinct commands a caller asks about, and are capped at ``_MEMO_MAX_ENTRIES`` so that a
464 caller asking about unbounded many names - a name that resolves to nothing is memoized
465 too - cannot grow them without bound.
466 """
467
468 def __init__(
469 self,
470 metadata: CommandMetadataRecordsCache,
471 fallback: MetadataResolver | None = None,
472 ) -> None:
473 self._metadata = metadata
474 self._fallback = fallback
475 self._policies: dict[str, CommandPolicies | None] = {}
476 self._cacheable: dict[str, bool] = {}
477 self._replica_safe: dict[str, bool] = {
478 cmd: False for cmd in _REPLICA_UNSAFE_COMMANDS
479 }
480
481 def resolve(self, command_name: str) -> CommandMetadata | None:
482 module, command = _split_command_name(command_name)
483
484 commands = self._metadata.get(module)
485 metadata = commands.get(command) if commands is not None else None
486
487 if metadata is None:
488 if self._fallback is not None:
489 return self._fallback.resolve(command_name)
490 return None
491
492 return metadata
493
494 def resolve_policies(self, command_name: str) -> CommandPolicies | None:
495 # Memoized under the lowercased name, so the spellings of one command share an
496 # entry rather than taking one each. ``resolve`` is still asked with the name as
497 # given, so an unresolvable one is reported the way the caller spelled it.
498 memo_key = command_name.lower()
499
500 try:
501 return self._policies[memo_key]
502 except KeyError:
503 pass
504
505 metadata = self.resolve(command_name)
506 policies = _to_command_policies(metadata) if metadata is not None else None
507
508 if len(self._policies) < _MEMO_MAX_ENTRIES:
509 self._policies[memo_key] = policies
510
511 return policies
512
513 def is_cacheable(self, command_name: str) -> bool:
514 # A name the record tables cannot be keyed by is not a command this client can decide
515 # on. Fail closed rather than raise into the command execution path, where a raw
516 # command with such a name must still reach the server and come back with the
517 # server's own error. ``execute_command`` accepts an arbitrary first argument, and a
518 # non-str one - ``bytes``, which the request encoder accepts - reaches here
519 # unchanged; it is refused rather than decoded, because deciding it would start
520 # caching a command whose name this client never resolved.
521 if not isinstance(command_name, str):
522 return False
523
524 # Memoized under the lowercased name, so the spellings of one command share an
525 # entry rather than taking one each.
526 memo_key = command_name.lower()
527
528 try:
529 return self._cacheable[memo_key]
530 except KeyError:
531 pass
532
533 try:
534 metadata = self.resolve(command_name)
535 except ValueError:
536 # More than one module prefix, which ``_split_command_name`` refuses.
537 metadata = None
538
539 cacheable = _is_client_side_cacheable(metadata)
540
541 if len(self._cacheable) < _MEMO_MAX_ENTRIES:
542 self._cacheable[memo_key] = cacheable
543
544 return cacheable
545
546 def is_replica_safe(self, command_name: str) -> bool:
547 if not isinstance(command_name, str):
548 return False
549
550 memo_key = command_name.lower()
551
552 try:
553 return self._replica_safe[memo_key]
554 except KeyError:
555 pass
556
557 try:
558 metadata = self.resolve(command_name)
559 except ValueError:
560 metadata = None
561
562 replica_safe = _is_replica_safe(metadata)
563
564 if len(self._replica_safe) < _MEMO_MAX_ENTRIES:
565 self._replica_safe[memo_key] = replica_safe
566
567 return replica_safe
568
569 @abstractmethod
570 def with_fallback(self, fallback: "MetadataResolver") -> "MetadataResolver":
571 pass
572
573
574class AsyncBaseMetadataResolver(AsyncMetadataResolver):
575 """
576 Async base class for metadata resolvers.
577
578 Lookup is first-match-wins: a command the records do not carry falls through to the
579 fallback resolver, and a chain that ends without a match resolves to None. Besides the
580 whole record, each consumer gets the view it needs of the same resolved metadata:
581 ``resolve_policies`` for cluster routing, ``is_cacheable`` for client-side caching.
582
583 Both views are memoized under the lowercased command name, so every spelling of one
584 command - ``GET`` as the command methods write it, ``get`` as the cluster client lowers it
585 - shares a single entry instead of taking one each. The metadata a resolver serves is a
586 snapshot taken when the resolver is built, so a command's view cannot change, and the memo
587 keeps the record lookup, the projection and the walk down the fallback chain off the
588 command execution path. Concurrent resolves of the same command may each compute it once;
589 the memo is idempotent, so the duplicated work is harmless. The memos grow with the set of
590 distinct commands a caller asks about, and are capped at ``_MEMO_MAX_ENTRIES`` so that a
591 caller asking about unbounded many names - a name that resolves to nothing is memoized
592 too - cannot grow them without bound.
593 """
594
595 def __init__(
596 self,
597 metadata: CommandMetadataRecordsCache,
598 fallback: AsyncMetadataResolver | None = None,
599 ) -> None:
600 self._metadata = metadata
601 self._fallback = fallback
602 self._policies: dict[str, CommandPolicies | None] = {}
603 self._cacheable: dict[str, bool] = {}
604 self._replica_safe: dict[str, bool] = {
605 cmd: False for cmd in _REPLICA_UNSAFE_COMMANDS
606 }
607
608 async def resolve(self, command_name: str) -> CommandMetadata | None:
609 module, command = _split_command_name(command_name)
610
611 commands = self._metadata.get(module)
612 metadata = commands.get(command) if commands is not None else None
613
614 if metadata is None:
615 if self._fallback is not None:
616 return await self._fallback.resolve(command_name)
617 return None
618
619 return metadata
620
621 async def resolve_policies(self, command_name: str) -> CommandPolicies | None:
622 # Memoized under the lowercased name, so the spellings of one command share an
623 # entry rather than taking one each. ``resolve`` is still asked with the name as
624 # given, so an unresolvable one is reported the way the caller spelled it.
625 memo_key = command_name.lower()
626
627 try:
628 return self._policies[memo_key]
629 except KeyError:
630 pass
631
632 metadata = await self.resolve(command_name)
633 policies = _to_command_policies(metadata) if metadata is not None else None
634
635 if len(self._policies) < _MEMO_MAX_ENTRIES:
636 self._policies[memo_key] = policies
637
638 return policies
639
640 async def is_cacheable(self, command_name: str) -> bool:
641 # A name the record tables cannot be keyed by is not a command this client can decide
642 # on. Fail closed rather than raise into the command execution path, where a raw
643 # command with such a name must still reach the server and come back with the
644 # server's own error. ``execute_command`` accepts an arbitrary first argument, and a
645 # non-str one - ``bytes``, which the request encoder accepts - reaches here
646 # unchanged; it is refused rather than decoded, because deciding it would start
647 # caching a command whose name this client never resolved.
648 if not isinstance(command_name, str):
649 return False
650
651 # Memoized under the lowercased name, so the spellings of one command share an
652 # entry rather than taking one each.
653 memo_key = command_name.lower()
654
655 try:
656 return self._cacheable[memo_key]
657 except KeyError:
658 pass
659
660 try:
661 metadata = await self.resolve(command_name)
662 except ValueError:
663 # More than one module prefix, which ``_split_command_name`` refuses.
664 metadata = None
665
666 cacheable = _is_client_side_cacheable(metadata)
667
668 if len(self._cacheable) < _MEMO_MAX_ENTRIES:
669 self._cacheable[memo_key] = cacheable
670
671 return cacheable
672
673 async def is_replica_safe(self, command_name: str) -> bool:
674 if not isinstance(command_name, str):
675 return False
676
677 memo_key = command_name.lower()
678
679 try:
680 return self._replica_safe[memo_key]
681 except KeyError:
682 pass
683
684 try:
685 metadata = await self.resolve(command_name)
686 except ValueError:
687 metadata = None
688
689 replica_safe = _is_replica_safe(metadata)
690
691 if len(self._replica_safe) < _MEMO_MAX_ENTRIES:
692 self._replica_safe[memo_key] = replica_safe
693
694 return replica_safe
695
696 @abstractmethod
697 def with_fallback(
698 self, fallback: "AsyncMetadataResolver"
699 ) -> "AsyncMetadataResolver":
700 pass
701
702
703class DynamicMetadataResolver(BaseMetadataResolver):
704 """
705 Resolves metadata dynamically based on the provided metadata records
706 (they can be extracted either from COMMAND output, or provided by user).
707
708 Note: Takes the records rather than the parser that produced them, so that this and
709 ``AsyncDynamicMetadataResolver`` accept the same argument: the async parser's
710 ``get_commands_metadata_cache`` is a coroutine and cannot be awaited in a constructor, so records
711 are the one shape both stacks can be built from. :func:`_load_commands_metadata_cache` turns a
712 parser into records at the one place that owns one, ``DynamicPolicyResolver``.
713 """
714
715 def __init__(
716 self,
717 metadata_records: CommandMetadataRecordsCache,
718 fallback: MetadataResolver | None = None,
719 ) -> None:
720 """
721 Parameters:
722 metadata_records (CommandMetadataRecordsCache): Command metadata records,
723 keyed the way ``CommandMetadataRecordsCache`` documents. Keys are
724 lowercased if they are not already, because that is how a resolved
725 command name is looked up.
726 fallback (Optional[MetadataResolver]): An optional resolver to be used when the
727 primary metadata cannot handle a specific request.
728 """
729 super().__init__(_lowercase_keyed(metadata_records), fallback)
730
731 def with_fallback(self, fallback: "MetadataResolver") -> "MetadataResolver":
732 return DynamicMetadataResolver(self._metadata, fallback)
733
734
735class StaticMetadataResolver(BaseMetadataResolver):
736 """
737 Resolves metadata from a static list, provided by the library,
738 containing command metadata records.
739 """
740
741 def __init__(self, fallback: MetadataResolver | None = None) -> None:
742 """
743 Parameters:
744 fallback (Optional[MetadataResolver]): An optional fallback metadata resolver
745 used for resolving metadata if static metadata is inadequate.
746 """
747 super().__init__(_STATIC_COMMAND_METADATA, fallback)
748
749 def with_fallback(self, fallback: "MetadataResolver") -> "MetadataResolver":
750 return StaticMetadataResolver(fallback)
751
752
753class AsyncDynamicMetadataResolver(AsyncBaseMetadataResolver):
754 """
755 Async version of DynamicMetadataResolver.
756
757 Takes the records rather than the parser that produced them, because
758 ``AsyncCommandsParser.get_commands_metadata_cache`` is a coroutine and cannot be awaited in a
759 constructor.
760 """
761
762 def __init__(
763 self,
764 metadata_records: CommandMetadataRecordsCache,
765 fallback: AsyncMetadataResolver | None = None,
766 ) -> None:
767 """
768 Parameters:
769 metadata_records (CommandMetadataRecordsCache): Command metadata records,
770 keyed the way ``CommandMetadataRecordsCache`` documents. Keys are
771 lowercased if they are not already, because that is how a resolved
772 command name is looked up.
773 fallback (Optional[AsyncMetadataResolver]): An optional resolver to be used when the
774 primary metadata cannot handle a specific request.
775 """
776 super().__init__(_lowercase_keyed(metadata_records), fallback)
777
778 def with_fallback(
779 self, fallback: "AsyncMetadataResolver"
780 ) -> "AsyncMetadataResolver":
781 return AsyncDynamicMetadataResolver(self._metadata, fallback)
782
783
784class AsyncStaticMetadataResolver(AsyncBaseMetadataResolver):
785 """
786 Async version of StaticMetadataResolver.
787 """
788
789 def __init__(self, fallback: AsyncMetadataResolver | None = None) -> None:
790 """
791 Parameters:
792 fallback (Optional[AsyncMetadataResolver]): An optional fallback metadata resolver
793 used for resolving metadata if static metadata is inadequate.
794 """
795 super().__init__(_STATIC_COMMAND_METADATA, fallback)
796
797 def with_fallback(
798 self, fallback: "AsyncMetadataResolver"
799 ) -> "AsyncMetadataResolver":
800 return AsyncStaticMetadataResolver(fallback)
801
802
803def _to_command_policies(metadata: CommandMetadata) -> CommandPolicies | None:
804 """
805 Project a single metadata record down to the routing policies a policy resolver serves.
806
807 Drops every field a ``CommandPolicies`` record does not carry, so the policies a
808 command routes by stay derived from its metadata rather than tracked beside it.
809
810 A record that withholds its routing policies projects to None, which a policy resolver
811 reports the same way it reports a command it does not carry. Withholding therefore
812 reproduces, exactly, what a command absent from the records resolves to: a policy-level
813 fallback still gets its turn, and a resolver with none leaves the cluster client to resolve
814 the target itself - which for a ``movablekeys`` command is the only path that finds its
815 keys. The *metadata* chain is not re-walked, though: the record was found, so a metadata
816 resolver behind this one is never asked, and cannot answer with the very policies the
817 record withholds.
818 """
819 if metadata.request_policy is None or metadata.response_policy is None:
820 return None
821
822 return CommandPolicies(
823 request_policy=metadata.request_policy,
824 response_policy=metadata.response_policy,
825 )
826
827
828def _is_client_side_cacheable(metadata: CommandMetadata | None) -> bool:
829 """
830 Decide whether the reply of the command a record describes may be cached client-side.
831
832 The one normative implementation of the eligibility rules, applied in the order they are
833 specified. Every rule is a veto, so the verdict does not depend on the order; the order
834 is kept to match the specification.
835
836 Takes ``None`` - what an exhausted resolver chain resolves to - so the unknown-command
837 case is decided here as well, rather than by every caller.
838
839 An incomplete record is refused for the same reason an unknown command is: a ``COMMAND``
840 reply that carries no tips cannot express ``nondeterministic_output`` or ``dont_cache``,
841 so a record built from one does not prove the command is cacheable. Note that a resolver
842 answers from the first record it finds, so an incomplete record decides the command even
843 when a complete one sits behind it in the chain - an override table therefore belongs in
844 front of the resolver that may serve incomplete records, not behind it.
845 """
846 if metadata is None or not metadata.has_complete_metadata:
847 return False
848
849 # A negative override: the server states the reply must not be cached, which decides on
850 # its own even when every positive rule matches.
851 if metadata.is_dont_cache:
852 return False
853
854 if not metadata.is_readonly:
855 return False
856
857 if metadata.is_blocking:
858 return False
859
860 if not metadata.has_key_argument:
861 return False
862
863 if metadata.has_nondeterministic_output:
864 return False
865
866 if metadata.is_script_runner:
867 return False
868
869 return True
870
871
872def _is_replica_safe(metadata: CommandMetadata | None) -> bool:
873 """
874 Decide whether a command is safe to execute on a replica based on the readonly flag.
875
876 Takes ``None`` - what an exhausted resolver chain resolves to - so the
877 unknown-command case is decided here as well.
878 """
879 if metadata is None:
880 return False
881
882 return metadata.is_readonly
883
884
885def _build_commands_metadata_cache_from_policies(
886 policy_records: PolicyRecords,
887) -> CommandMetadataRecordsCache:
888 """
889 Build the metadata records cache from policy records.
890
891 Unlike ``_build_commands_metadata_cache``, which builds the cache from a raw ``COMMAND``
892 reply, this lifts the narrower 7.1.0 routing view into the same shape.
893
894 Only the routing policies of each command are known, so every other field keeps its
895 fail-closed default. This backs the ``PolicyRecords`` arguments of the policy resolvers:
896 a resolver built from policy records serves exactly the policies it was given, and
897 reports no command as client-side-cacheable, which is the conservative answer for
898 metadata that was never supplied.
899
900 Keys are lowercased on the way through - the records are rebuilt here anyway - because
901 that is how a resolved command name is looked up.
902 """
903 return {
904 module_name.lower(): {
905 command_name.lower(): CommandMetadata(
906 request_policy=policies.request_policy,
907 response_policy=policies.response_policy,
908 )
909 for command_name, policies in commands.items()
910 }
911 for module_name, commands in policy_records.items()
912 }
913
914
915def _load_commands_metadata_cache(
916 commands_parser: object,
917) -> CommandMetadataRecordsCache:
918 """
919 Load the metadata records cache of a ``COMMAND`` parser.
920
921 ``get_commands_metadata_cache`` is what a parser serves, and is what this loads.
922
923 The ``get_command_policies`` branch is a backwards-compatibility shim, not a second
924 supported shape. ``DynamicPolicyResolver`` called nothing but that method in 7.1.0, so code
925 that passed an object duck-typing it would otherwise break at construction; the branch keeps
926 that code working and nothing more. It is deliberately not advertised as an extension point:
927 ``CommandsParser`` lives in the private ``redis._parsers`` package, and the supported way to
928 decide routing yourself is to implement the public ``PolicyResolver`` ABC and pass it as the
929 ``policy_resolver`` of a cluster client. Policy records also carry no cacheability metadata,
930 so everything a parser reaching this branch serves reports as non-cacheable.
931
932 The async stack needs no counterpart: ``AsyncDynamicMetadataResolver`` takes records rather
933 than a parser, and ``AsyncDynamicPolicyResolver`` still accepts ``policy_records`` directly.
934
935 Raises:
936 TypeError: If the parser serves neither method.
937 """
938 get_metadata = getattr(commands_parser, "get_commands_metadata_cache", None)
939 if get_metadata is not None:
940 return get_metadata()
941
942 get_policies = getattr(commands_parser, "get_command_policies", None)
943 if get_policies is None:
944 raise TypeError(
945 f"{type(commands_parser).__name__} serves neither get_commands_metadata_cache() nor "
946 "get_command_policies(); a commands parser must serve one of them"
947 )
948
949 return _build_commands_metadata_cache_from_policies(get_policies())
950
951
952# The records the cluster clients fall back to when the policy resolver does not know a
953# command, which is the common case: the default resolver is backed by the static table
954# below, so every write and every read outside it falls through on every execution. Reused
955# rather than constructed there, both to keep an allocation off that path and because a
956# frozen record is safe to share.
957#
958# Only the routing policies are known for a command the client had to fall back on, so
959# every other field is recorded at its fail-closed value and none of these reports as
960# cacheable. Spelled out rather than left to the dataclass defaults, for the reason given
961# on the shared table shapes below.
962_DEFAULT_KEYLESS_METADATA = CommandMetadata(
963 request_policy=RequestPolicy.DEFAULT_KEYLESS,
964 response_policy=ResponsePolicy.DEFAULT_KEYLESS,
965 is_readonly=False,
966 is_blocking=False,
967 has_key_argument=False,
968 has_nondeterministic_output=False,
969 is_script_runner=False,
970 is_dont_cache=False,
971 has_complete_metadata=False,
972)
973_DEFAULT_KEYED_METADATA = CommandMetadata(
974 request_policy=RequestPolicy.DEFAULT_KEYED,
975 response_policy=ResponsePolicy.DEFAULT_KEYED,
976 is_readonly=False,
977 is_blocking=False,
978 has_key_argument=False,
979 has_nondeterministic_output=False,
980 is_script_runner=False,
981 is_dont_cache=False,
982 has_complete_metadata=False,
983)
984
985# Same, for the node-flag fallback, which resolves a request policy and leaves the response
986# policy at its keyless default.
987_METADATA_BY_REQUEST_POLICY: Mapping[RequestPolicy, CommandMetadata] = MappingProxyType(
988 {
989 policy: CommandMetadata(
990 request_policy=policy,
991 response_policy=ResponsePolicy.DEFAULT_KEYLESS,
992 is_readonly=False,
993 is_blocking=False,
994 has_key_argument=False,
995 has_nondeterministic_output=False,
996 is_script_runner=False,
997 is_dont_cache=False,
998 has_complete_metadata=False,
999 )
1000 for policy in RequestPolicy
1001 }
1002)
1003
1004
1005# Every record in this module spells out all nine fields, including the ones that happen to
1006# equal the dataclass default. The nine are not interchangeable: seven of them
1007# (``is_readonly``, ``is_blocking``, ``has_key_argument``, ``has_nondeterministic_output``,
1008# ``is_script_runner``, ``is_dont_cache``, ``has_complete_metadata``) are the inputs to
1009# ``_is_client_side_cacheable``, so a shape that leaves one implicit lets a command added
1010# for an unrelated reason - replica-safe read routing, say, which needs only ``is_readonly``
1011# - become client-side cacheable as a side effect, with nothing in the diff saying so.
1012# Listing every field makes each new entry state its own cacheability.
1013#
1014# Shapes derived with ``dataclasses.replace`` below inherit from a fully explicit base, so
1015# they name only the field that differs and still leave nothing to a default.
1016#
1017# Record shared by every command the client-side cache may serve: readonly, takes a key
1018# name argument, and reports nothing that forbids caching. Spelled out once because the
1019# vast majority of the table below is one of these shapes.
1020_CACHEABLE_KEYED = CommandMetadata(
1021 request_policy=RequestPolicy.DEFAULT_KEYED,
1022 response_policy=ResponsePolicy.DEFAULT_KEYED,
1023 is_readonly=True,
1024 is_blocking=False,
1025 has_key_argument=True,
1026 has_nondeterministic_output=False,
1027 is_script_runner=False,
1028 is_dont_cache=False,
1029 has_complete_metadata=True,
1030)
1031
1032# Same, for the ``movablekeys`` commands, with the routing policies withheld. Their keys are
1033# only discoverable through key specs, so ``first_key_pos`` is 0 and the derived policies come
1034# out keyless even though the command genuinely takes keys. This divergence is exactly why
1035# ``has_key_argument`` is recorded separately from ``request_policy``.
1036#
1037# Recording those derived policies here would route the command to an arbitrary node, so they
1038# are withheld: a policy resolver then reports the command as unresolved and the cluster client
1039# resolves the target itself through ``determine_slot``, which asks the server for the keys with
1040# ``COMMAND GETKEYS``. That is the only path that finds the keys of a ``movablekeys`` command,
1041# and it is what the routing fallback did before this table backed the static resolver.
1042# TODO(pslavova): record the real request/response policies once routing derives keyed/keyless
1043# from the key specs (``has_key_argument``) rather than from ``first_key_pos``.
1044_CACHEABLE_MOVABLE_KEYS = CommandMetadata(
1045 request_policy=None,
1046 response_policy=None,
1047 is_readonly=True,
1048 is_blocking=False,
1049 has_key_argument=True,
1050 has_nondeterministic_output=False,
1051 is_script_runner=False,
1052 is_dont_cache=False,
1053 has_complete_metadata=True,
1054)
1055
1056# Same, for a command that also carries the ``blocking`` flag, which makes it ineligible:
1057# its reply is what the caller waited for rather than a snapshot the cache may re-serve.
1058# XREAD is the only readonly, keyed command the flag excludes. The withheld routing policies
1059# are inherited: XREAD is ``movablekeys`` too, so it must be routed by its resolved keys.
1060_BLOCKING_MOVABLE_KEYS = replace(_CACHEABLE_MOVABLE_KEYS, is_blocking=True)
1061
1062# Same, for the read-only script runners - EVAL_RO, EVALSHA_RO, FCALL_RO. The
1063# ``script_runner`` flag makes them ineligible: the reply is whatever the script computed,
1064# which the client cannot tie to the keys the script happened to touch. They are
1065# ``movablekeys`` too - their keys are counted by ``numkeys`` - so the withheld routing
1066# policies are inherited for the same reason.
1067#
1068# Recorded rather than left absent because the flag only reaches the client from Redis 8.10
1069# on: on 7.4.x-8.8.x all three report readonly and keyed with no flag that excludes them,
1070# i.e. cacheable, so a resolver reading a live COMMAND reply from one of those servers would
1071# admit them. This record states what the command is, independently of what a given server
1072# version reports. Removable once the minimum CSC-supported server reports ``script_runner``.
1073_SCRIPT_RUNNER_MOVABLE_KEYS = replace(_CACHEABLE_MOVABLE_KEYS, is_script_runner=True)
1074
1075# Readonly but keyless, so not cacheable: the command observes keyspace state without
1076# taking a key name argument. Note that this is what makes a search or aggregation query
1077# uncacheable - being a module command is not itself a reason to exclude one.
1078_READONLY_KEYLESS = CommandMetadata(
1079 request_policy=RequestPolicy.DEFAULT_KEYLESS,
1080 response_policy=ResponsePolicy.DEFAULT_KEYLESS,
1081 is_readonly=True,
1082 is_blocking=False,
1083 has_key_argument=False,
1084 has_nondeterministic_output=False,
1085 is_script_runner=False,
1086 is_dont_cache=False,
1087 has_complete_metadata=True,
1088)
1089
1090# Readonly and keyless with routing policies withheld, so the cluster client keeps
1091# resolving its target node via COMMAND_FLAGS (e.g. DEFAULT_NODE for DBSIZE/KEYS/RANDOMKEY
1092# or PRIMARIES for SCAN).
1093_READONLY_KEYLESS_WITHHELD_ROUTING = CommandMetadata(
1094 request_policy=None,
1095 response_policy=None,
1096 is_readonly=True,
1097 is_blocking=False,
1098 has_key_argument=False,
1099 has_nondeterministic_output=False,
1100 is_script_runner=False,
1101 is_dont_cache=False,
1102 has_complete_metadata=True,
1103)
1104
1105# Readonly and keyed, but tipped nondeterministic_output, which makes it ineligible
1106# for client-side caching.
1107_NONDETERMINISTIC_KEYED = replace(_CACHEABLE_KEYED, has_nondeterministic_output=True)
1108
1109# Same tip on the keyless reads that withhold their routing. Carries no cacheability
1110# consequence of its own - a keyless command is already ineligible for want of a key
1111# argument - but the table records what the server reports, and both RANDOMKEY and SCAN are
1112# tipped ``nondeterministic_output``: one returns an arbitrary key, the other an arbitrary
1113# page of the keyspace.
1114_NONDETERMINISTIC_KEYLESS_WITHHELD_ROUTING = replace(
1115 _READONLY_KEYLESS_WITHHELD_ROUTING, has_nondeterministic_output=True
1116)
1117
1118# Write commands. Rule 1 excludes them, so nothing else about them matters to the cache.
1119_WRITE_KEYLESS = CommandMetadata(
1120 request_policy=RequestPolicy.DEFAULT_KEYLESS,
1121 response_policy=ResponsePolicy.DEFAULT_KEYLESS,
1122 is_readonly=False,
1123 is_blocking=False,
1124 has_key_argument=False,
1125 has_nondeterministic_output=False,
1126 is_script_runner=False,
1127 is_dont_cache=False,
1128 has_complete_metadata=True,
1129)
1130
1131_WRITE_KEYLESS_WITHHELD_ROUTING = CommandMetadata(
1132 request_policy=None,
1133 response_policy=None,
1134 is_readonly=False,
1135 is_blocking=False,
1136 has_key_argument=False,
1137 has_nondeterministic_output=False,
1138 is_script_runner=False,
1139 is_dont_cache=False,
1140 has_complete_metadata=True,
1141)
1142
1143_WRITE_KEYED = CommandMetadata(
1144 request_policy=RequestPolicy.DEFAULT_KEYED,
1145 response_policy=ResponsePolicy.DEFAULT_KEYED,
1146 is_readonly=False,
1147 is_blocking=False,
1148 has_key_argument=True,
1149 has_nondeterministic_output=False,
1150 is_script_runner=False,
1151 is_dont_cache=False,
1152 has_complete_metadata=True,
1153)
1154
1155# The same three shapes for the commands the server tips ``dont_cache``. The search module
1156# tips nearly its whole surface that way, including the write commands, where the marker is
1157# redundant - rule 1 already excludes them. It is recorded anyway so the table keeps
1158# reporting what the server reports.
1159_DONT_CACHE_READONLY_KEYLESS = replace(_READONLY_KEYLESS, is_dont_cache=True)
1160_DONT_CACHE_WRITE_KEYLESS = replace(_WRITE_KEYLESS, is_dont_cache=True)
1161_DONT_CACHE_WRITE_KEYED = replace(_WRITE_KEYED, is_dont_cache=True)
1162
1163
1164# =============================================================================
1165# Static command metadata table
1166# =============================================================================
1167# This table is seeded from old ``CacheConfig.DEFAULT_ALLOW_LIST`` and ``redis.commands.policies.STATIC_POLICIES``.
1168#
1169# Every entry below was validated against a live ``COMMAND`` reply from Redis 8.10.0 with
1170# search, timeseries, ReJSON, bf and vectorset loaded (the ``redislabs/client-libs-test``
1171# stack image), with the core entries also compared against 7.4.2 to ensure the minimum
1172# supported version does not disagree on the fields that decide cacheability.
1173#
1174# The table is not exhaustive: on 8.10.0 the server reports 127 cacheable commands and this
1175# covers 80 of them. The count is version-qualified because it moves between releases - a
1176# later server reports more. The uncovered ones are whole module surfaces the CSC allow-list
1177# never carried - ``bf.*``, ``cf.*``, ``cms.*``, ``topk.*``, ``tdigest.*``, the vectorset
1178# reads - plus core reads such as ``pfcount``. A command absent from the table fails closed,
1179# so the uncovered commands are simply treated as not cacheable.
1180#
1181# Every command on the default allow-list of the 7.1.0 client-side cache is present.
1182# So are all of their module counterparts, plus a selection of write commands.
1183# Write commands carry the metadata that makes them *ineligible*, so the reason
1184# is documented in one place rather than inferred from an absence - and, because
1185# a resolver answers from the first record it finds, so that a live resolver
1186# chained behind this one cannot re-admit them. ``xpending``, ``ts.info`` and
1187# ``xread`` are on today's ``CacheConfig.DEFAULT_ALLOW_LIST``, and the server
1188# confirms all three are defects in that list: ``xpending`` is tipped
1189# ``nondeterministic_output``, ``ts.info`` ``dont_cache``, and ``xread`` carries
1190# the ``blocking`` command flag.
1191#
1192# Five entries diverge from the live reply, each for a reason spelled out where it occurs:
1193# ``exists`` and ``mget`` stand in the keyed defaults for the unimplemented ``multi_shard``
1194# tips, ``ft.cursor`` is SPECIAL, a client-side decision the server does not tip, ``touch`` is
1195# recorded ``dont_cache`` where the server reports it cacheable, and ``vrandmember`` is
1196# recorded ``nondeterministic_output`` where the server tips it nothing. The ``movablekeys``
1197# reads - ``eval_ro``, ``evalsha_ro``, ``fcall_ro``, ``sdiffcard``, ``sintercard``,
1198# ``sunioncard``, ``xread``, ``zdiff``, ``zinter``, ``zintercard`` and ``zunion`` - plus
1199# ``command``, ``dbsize``, ``keys``, ``randomkey``, ``scan``, ``touch`` and ``vrandmember``
1200# withhold their routing policies entirely, so the cluster client keeps resolving their
1201# targets itself. Their cacheability inputs are unaffected.
1202_STATIC_COMMAND_METADATA: CommandMetadataRecordsCache = MappingProxyType(
1203 {
1204 "core": MappingProxyType(
1205 {
1206 "bitcount": _CACHEABLE_KEYED,
1207 "bitfield_ro": _CACHEABLE_KEYED,
1208 "bitpos": _CACHEABLE_KEYED,
1209 # From STATIC_POLICIES. COMMAND is flagged loading/stale, not readonly,
1210 # and takes no keys. Routing policies are withheld so the cluster client preserves
1211 # the 2-word COMMAND COUNT, COMMAND LIST, COMMAND GETKEYS default-node flags.
1212 "command": _WRITE_KEYLESS_WITHHELD_ROUTING,
1213 "dbsize": _READONLY_KEYLESS_WITHHELD_ROUTING,
1214 "digest": _CACHEABLE_KEYED,
1215 "dump": _NONDETERMINISTIC_KEYED,
1216 # Not cacheable: script_runner. See the shape's note for why all three are
1217 # recorded rather than left to the server to report.
1218 "eval_ro": _SCRIPT_RUNNER_MOVABLE_KEYS,
1219 "evalsha_ro": _SCRIPT_RUNNER_MOVABLE_KEYS,
1220 # The server tips EXISTS request_policy:multi_shard and
1221 # response_policy:agg_sum, but ``RequestPolicy.MULTI_SHARD`` is not
1222 # implemented in either client stack: ``_split_multi_shard_command``
1223 # returns per-key command descriptors where every caller of
1224 # ``_determine_nodes`` expects ``ClusterNode``, and the async client has no
1225 # MULTI_SHARD entry in ``_policies_callback_mapping`` at all. Recording the
1226 # real tips here therefore breaks the command, so the keyed defaults stand
1227 # in - which is what the routing fallback resolved to before this table
1228 # backed the static resolver.
1229 # TODO(pslavova): record the real tips once MULTI_SHARD is implemented
1230 # across both stacks (main path and pipeline).
1231 "exists": _CACHEABLE_KEYED,
1232 "expiretime": _CACHEABLE_KEYED,
1233 "fcall_ro": _SCRIPT_RUNNER_MOVABLE_KEYS,
1234 "geodist": _CACHEABLE_KEYED,
1235 "geohash": _CACHEABLE_KEYED,
1236 "geopos": _CACHEABLE_KEYED,
1237 "georadius_ro": _CACHEABLE_KEYED,
1238 "georadiusbymember_ro": _CACHEABLE_KEYED,
1239 "geosearch": _CACHEABLE_KEYED,
1240 "get": _CACHEABLE_KEYED,
1241 "getbit": _CACHEABLE_KEYED,
1242 "getrange": _CACHEABLE_KEYED,
1243 "hexists": _CACHEABLE_KEYED,
1244 "hexpiretime": _CACHEABLE_KEYED,
1245 "hget": _CACHEABLE_KEYED,
1246 # HGETALL, HKEYS, HVALS, SDIFF, SINTER, SMEMBERS and SUNION all carry
1247 # nondeterministic_output_order, which is a different tip from
1248 # nondeterministic_output and does not prevent caching. Matching tips by
1249 # prefix would silently drop all seven.
1250 "hgetall": _CACHEABLE_KEYED,
1251 "hkeys": _CACHEABLE_KEYED,
1252 "hlen": _CACHEABLE_KEYED,
1253 "hmget": _CACHEABLE_KEYED,
1254 "hpexpiretime": _CACHEABLE_KEYED,
1255 "hpttl": _NONDETERMINISTIC_KEYED,
1256 "hrandfield": _NONDETERMINISTIC_KEYED,
1257 "hscan": _NONDETERMINISTIC_KEYED,
1258 "hstrlen": _CACHEABLE_KEYED,
1259 "httl": _NONDETERMINISTIC_KEYED,
1260 "hvals": _CACHEABLE_KEYED,
1261 "keys": _READONLY_KEYLESS_WITHHELD_ROUTING,
1262 "lcs": _CACHEABLE_KEYED,
1263 "lindex": _CACHEABLE_KEYED,
1264 "llen": _CACHEABLE_KEYED,
1265 "lpos": _CACHEABLE_KEYED,
1266 "lrange": _CACHEABLE_KEYED,
1267 # Tipped request_policy:multi_shard by the server, withheld for the reason
1268 # spelled out on ``exists`` above.
1269 # TODO: record the real tip once MULTI_SHARD is implemented.
1270 "mget": _CACHEABLE_KEYED,
1271 "pexpiretime": _CACHEABLE_KEYED,
1272 "pttl": _NONDETERMINISTIC_KEYED,
1273 "randomkey": _NONDETERMINISTIC_KEYLESS_WITHHELD_ROUTING,
1274 # Readonly and keyless. Routing policies are withheld so the cluster client keeps
1275 # routing SCAN to all primary nodes (PRIMARIES) rather than a single random node.
1276 "scan": _NONDETERMINISTIC_KEYLESS_WITHHELD_ROUTING,
1277 "scard": _CACHEABLE_KEYED,
1278 "sdiff": _CACHEABLE_KEYED,
1279 "sdiffcard": _CACHEABLE_MOVABLE_KEYS,
1280 "sinter": _CACHEABLE_KEYED,
1281 "sintercard": _CACHEABLE_MOVABLE_KEYS,
1282 "sismember": _CACHEABLE_KEYED,
1283 "smembers": _CACHEABLE_KEYED,
1284 "smismember": _CACHEABLE_KEYED,
1285 # Unreachable today: ``CoreCommands.sort_ro`` delegates to ``sort()``, which
1286 # sends SORT rather than SORT_RO, so nothing resolves this entry. Recorded
1287 # because SORT_RO is genuinely cacheable, and the old allow-list carried it
1288 # just as ineffectively.
1289 # TODO: drop this note once the command method sends its own name.
1290 "sort_ro": _CACHEABLE_KEYED,
1291 "srandmember": _NONDETERMINISTIC_KEYED,
1292 "sscan": _NONDETERMINISTIC_KEYED,
1293 "strlen": _CACHEABLE_KEYED,
1294 "substr": _CACHEABLE_KEYED,
1295 "sunion": _CACHEABLE_KEYED,
1296 "sunioncard": _CACHEABLE_MOVABLE_KEYS,
1297 # Not cacheable, and the one entry in this table that records a client-side
1298 # judgement instead of what the server reports: TOUCH has a server-side
1299 # effect - it refreshes each key's idle time, which is what LRU/LFU
1300 # eviction and OBJECT IDLETIME read - so it must reach the server on every
1301 # call. No server flag or tip expresses that; measured on 8.10.0 the server
1302 # reports it readonly and keyed with no ``dont_cache`` tip, i.e. cacheable.
1303 # Recorded here as ``dont_cache`` so the reason is stated once rather than
1304 # left to every resolver in a chain. Removable once the server tips it
1305 # ``dont_cache``.
1306 #
1307 # Routing policies are withheld, not defaulted: the server tips TOUCH
1308 # request_policy:multi_shard / response_policy:agg_sum, and MULTI_SHARD is
1309 # unimplemented for the reason spelled out on ``exists`` above. Withholding
1310 # keeps the cluster client resolving the target itself, exactly as it does
1311 # today for a command this table does not carry.
1312 "touch": CommandMetadata(
1313 request_policy=None,
1314 response_policy=None,
1315 is_readonly=True,
1316 is_blocking=False,
1317 has_key_argument=True,
1318 has_nondeterministic_output=False,
1319 is_script_runner=False,
1320 is_dont_cache=True,
1321 has_complete_metadata=True,
1322 ),
1323 "ttl": _NONDETERMINISTIC_KEYED,
1324 "type": _CACHEABLE_KEYED,
1325 # Not cacheable: its reply is a random sample of the vector set, so re-serving
1326 # it from a cache would stop it varying. Every core random read - SRANDMEMBER,
1327 # ZRANDMEMBER, HRANDFIELD, RANDOMKEY - is tipped ``nondeterministic_output``
1328 # by the server; measured on 8.10.0 VRANDMEMBER reports no tips at all, so the
1329 # algorithm would admit it. Recorded as nondeterministic because that is what
1330 # it is, and it is the one other command node-redis hard-codes ineligible
1331 # besides TOUCH. Removable once the server tips it like its core siblings.
1332 #
1333 # Routing policies are withheld for the same reason as ``touch`` above: the
1334 # record exists for its cacheability inputs, and must not start routing a
1335 # command the cluster client resolves for itself today.
1336 "vrandmember": CommandMetadata(
1337 request_policy=None,
1338 response_policy=None,
1339 is_readonly=True,
1340 is_blocking=False,
1341 has_key_argument=True,
1342 has_nondeterministic_output=True,
1343 is_script_runner=False,
1344 is_dont_cache=False,
1345 has_complete_metadata=True,
1346 ),
1347 "xlen": _CACHEABLE_KEYED,
1348 # Not cacheable: nondeterministic_output. It is on today's allow-list,
1349 # which is a defect in that list rather than a reason to keep caching it.
1350 "xpending": _NONDETERMINISTIC_KEYED,
1351 "xrange": _CACHEABLE_KEYED,
1352 "xread": _BLOCKING_MOVABLE_KEYS,
1353 "xrevrange": _CACHEABLE_KEYED,
1354 "zcard": _CACHEABLE_KEYED,
1355 "zcount": _CACHEABLE_KEYED,
1356 "zdiff": _CACHEABLE_MOVABLE_KEYS,
1357 "zinter": _CACHEABLE_MOVABLE_KEYS,
1358 "zintercard": _CACHEABLE_MOVABLE_KEYS,
1359 "zlexcount": _CACHEABLE_KEYED,
1360 "zmscore": _CACHEABLE_KEYED,
1361 "zrange": _CACHEABLE_KEYED,
1362 "zrangebylex": _CACHEABLE_KEYED,
1363 "zrangebyscore": _CACHEABLE_KEYED,
1364 "zrank": _CACHEABLE_KEYED,
1365 "zrandmember": _NONDETERMINISTIC_KEYED,
1366 "zrevrange": _CACHEABLE_KEYED,
1367 "zrevrangebylex": _CACHEABLE_KEYED,
1368 "zrevrangebyscore": _CACHEABLE_KEYED,
1369 "zrevrank": _CACHEABLE_KEYED,
1370 "zscan": _NONDETERMINISTIC_KEYED,
1371 "zscore": _CACHEABLE_KEYED,
1372 "zunion": _CACHEABLE_MOVABLE_KEYS,
1373 }
1374 ),
1375 "json": MappingProxyType(
1376 {
1377 "arrindex": _CACHEABLE_KEYED,
1378 "arrlen": _CACHEABLE_KEYED,
1379 "get": _CACHEABLE_KEYED,
1380 "mget": _CACHEABLE_KEYED,
1381 "objkeys": _CACHEABLE_KEYED,
1382 "objlen": _CACHEABLE_KEYED,
1383 "resp": _CACHEABLE_KEYED,
1384 "strlen": _CACHEABLE_KEYED,
1385 "type": _CACHEABLE_KEYED,
1386 }
1387 ),
1388 "ts": MappingProxyType(
1389 {
1390 "get": _CACHEABLE_KEYED,
1391 # Not cacheable: the server reports dont_cache for TS.INFO, contradicting
1392 # today's allow-list, which caches it.
1393 "info": CommandMetadata(
1394 request_policy=RequestPolicy.DEFAULT_KEYED,
1395 response_policy=ResponsePolicy.DEFAULT_KEYED,
1396 is_readonly=True,
1397 is_blocking=False,
1398 has_key_argument=True,
1399 has_nondeterministic_output=False,
1400 is_script_runner=False,
1401 is_dont_cache=True,
1402 has_complete_metadata=True,
1403 ),
1404 "range": _CACHEABLE_KEYED,
1405 "revrange": _CACHEABLE_KEYED,
1406 }
1407 ),
1408 # From STATIC_POLICIES. Only the suggestion-dictionary commands take a key name
1409 # argument, so FT.SUGGET and FT.SUGLEN are the only cacheable entries here -
1410 # FT.SEARCH and FT.AGGREGATE are readonly but keyless.
1411 #
1412 # The search module tips almost its whole surface ``dont_cache``. The exceptions
1413 # are FT.CURSOR, FT.DROP, FT.SUGGET and FT.SUGLEN, which report no tips at all -
1414 # which is what keeps the two suggestion-dictionary reads cacheable.
1415 "ft": MappingProxyType(
1416 {
1417 "aggregate": _DONT_CACHE_READONLY_KEYLESS,
1418 "aliasadd": _DONT_CACHE_WRITE_KEYLESS,
1419 "aliasdel": _DONT_CACHE_WRITE_KEYLESS,
1420 "aliaslist": _DONT_CACHE_READONLY_KEYLESS,
1421 "aliasupdate": _DONT_CACHE_WRITE_KEYLESS,
1422 "alter": _DONT_CACHE_WRITE_KEYLESS,
1423 "create": _DONT_CACHE_WRITE_KEYLESS,
1424 # SPECIAL is a client-side routing decision, not a server tip: FT.CURSOR
1425 # must reach the node that ran the FT.AGGREGATE it continues, which
1426 # ``get_special_nodes`` resolves. The server reports no tips for it, so a
1427 # generated table would say DEFAULT_KEYLESS and break cursor routing.
1428 "cursor": CommandMetadata(
1429 request_policy=RequestPolicy.SPECIAL,
1430 response_policy=ResponsePolicy.DEFAULT_KEYLESS,
1431 is_readonly=True,
1432 is_blocking=False,
1433 has_key_argument=False,
1434 has_nondeterministic_output=False,
1435 is_script_runner=False,
1436 is_dont_cache=False,
1437 has_complete_metadata=True,
1438 ),
1439 "dictadd": _DONT_CACHE_WRITE_KEYLESS,
1440 "dictdel": _DONT_CACHE_WRITE_KEYLESS,
1441 "dictdump": _DONT_CACHE_READONLY_KEYLESS,
1442 "drop": _WRITE_KEYLESS,
1443 "dropindex": _DONT_CACHE_WRITE_KEYLESS,
1444 "explain": _DONT_CACHE_READONLY_KEYLESS,
1445 "explaincli": _DONT_CACHE_READONLY_KEYLESS,
1446 "info": _DONT_CACHE_READONLY_KEYLESS,
1447 "profile": _DONT_CACHE_READONLY_KEYLESS,
1448 "search": _DONT_CACHE_READONLY_KEYLESS,
1449 "spellcheck": _DONT_CACHE_READONLY_KEYLESS,
1450 "sugadd": _DONT_CACHE_WRITE_KEYED,
1451 "sugdel": _DONT_CACHE_WRITE_KEYED,
1452 "sugget": _CACHEABLE_KEYED,
1453 "suglen": _CACHEABLE_KEYED,
1454 "syndump": _DONT_CACHE_READONLY_KEYLESS,
1455 "synupdate": _DONT_CACHE_WRITE_KEYLESS,
1456 "tagvals": _DONT_CACHE_READONLY_KEYLESS,
1457 }
1458 ),
1459 }
1460)