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)