Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/redis/_cache.py: 42%
106 statements
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-23 06:16 +0000
« prev ^ index » next coverage.py v7.4.4, created at 2024-04-23 06:16 +0000
1import copy
2import random
3import time
4from abc import ABC, abstractmethod
5from collections import OrderedDict, defaultdict
6from enum import Enum
7from typing import List
9from redis.typing import KeyT, ResponseT
11DEFAULT_EVICTION_POLICY = "lru"
14DEFAULT_BLACKLIST = [
15 "BF.CARD",
16 "BF.DEBUG",
17 "BF.EXISTS",
18 "BF.INFO",
19 "BF.MEXISTS",
20 "BF.SCANDUMP",
21 "CF.COMPACT",
22 "CF.COUNT",
23 "CF.DEBUG",
24 "CF.EXISTS",
25 "CF.INFO",
26 "CF.MEXISTS",
27 "CF.SCANDUMP",
28 "CMS.INFO",
29 "CMS.QUERY",
30 "DUMP",
31 "EXPIRETIME",
32 "FT.AGGREGATE",
33 "FT.ALIASADD",
34 "FT.ALIASDEL",
35 "FT.ALIASUPDATE",
36 "FT.CURSOR",
37 "FT.EXPLAIN",
38 "FT.EXPLAINCLI",
39 "FT.GET",
40 "FT.INFO",
41 "FT.MGET",
42 "FT.PROFILE",
43 "FT.SEARCH",
44 "FT.SPELLCHECK",
45 "FT.SUGGET",
46 "FT.SUGLEN",
47 "FT.SYNDUMP",
48 "FT.TAGVALS",
49 "FT._ALIASADDIFNX",
50 "FT._ALIASDELIFX",
51 "HRANDFIELD",
52 "JSON.DEBUG",
53 "PEXPIRETIME",
54 "PFCOUNT",
55 "PTTL",
56 "SRANDMEMBER",
57 "TDIGEST.BYRANK",
58 "TDIGEST.BYREVRANK",
59 "TDIGEST.CDF",
60 "TDIGEST.INFO",
61 "TDIGEST.MAX",
62 "TDIGEST.MIN",
63 "TDIGEST.QUANTILE",
64 "TDIGEST.RANK",
65 "TDIGEST.REVRANK",
66 "TDIGEST.TRIMMED_MEAN",
67 "TOPK.INFO",
68 "TOPK.LIST",
69 "TOPK.QUERY",
70 "TOUCH",
71 "TTL",
72]
75DEFAULT_WHITELIST = [
76 "BITCOUNT",
77 "BITFIELD_RO",
78 "BITPOS",
79 "EXISTS",
80 "GEODIST",
81 "GEOHASH",
82 "GEOPOS",
83 "GEORADIUSBYMEMBER_RO",
84 "GEORADIUS_RO",
85 "GEOSEARCH",
86 "GET",
87 "GETBIT",
88 "GETRANGE",
89 "HEXISTS",
90 "HGET",
91 "HGETALL",
92 "HKEYS",
93 "HLEN",
94 "HMGET",
95 "HSTRLEN",
96 "HVALS",
97 "JSON.ARRINDEX",
98 "JSON.ARRLEN",
99 "JSON.GET",
100 "JSON.MGET",
101 "JSON.OBJKEYS",
102 "JSON.OBJLEN",
103 "JSON.RESP",
104 "JSON.STRLEN",
105 "JSON.TYPE",
106 "LCS",
107 "LINDEX",
108 "LLEN",
109 "LPOS",
110 "LRANGE",
111 "MGET",
112 "SCARD",
113 "SDIFF",
114 "SINTER",
115 "SINTERCARD",
116 "SISMEMBER",
117 "SMEMBERS",
118 "SMISMEMBER",
119 "SORT_RO",
120 "STRLEN",
121 "SUBSTR",
122 "SUNION",
123 "TS.GET",
124 "TS.INFO",
125 "TS.RANGE",
126 "TS.REVRANGE",
127 "TYPE",
128 "XLEN",
129 "XPENDING",
130 "XRANGE",
131 "XREAD",
132 "XREVRANGE",
133 "ZCARD",
134 "ZCOUNT",
135 "ZDIFF",
136 "ZINTER",
137 "ZINTERCARD",
138 "ZLEXCOUNT",
139 "ZMSCORE",
140 "ZRANGE",
141 "ZRANGEBYLEX",
142 "ZRANGEBYSCORE",
143 "ZRANK",
144 "ZREVRANGE",
145 "ZREVRANGEBYLEX",
146 "ZREVRANGEBYSCORE",
147 "ZREVRANK",
148 "ZSCORE",
149 "ZUNION",
150]
152_RESPONSE = "response"
153_KEYS = "keys"
154_CTIME = "ctime"
155_ACCESS_COUNT = "access_count"
158class EvictionPolicy(Enum):
159 LRU = "lru"
160 LFU = "lfu"
161 RANDOM = "random"
164class AbstractCache(ABC):
165 """
166 An abstract base class for client caching implementations.
167 If you want to implement your own cache you must support these methods.
168 """
170 @abstractmethod
171 def set(self, command: str, response: ResponseT, keys_in_command: List[KeyT]):
172 pass
174 @abstractmethod
175 def get(self, command: str) -> ResponseT:
176 pass
178 @abstractmethod
179 def delete_command(self, command: str):
180 pass
182 @abstractmethod
183 def delete_many(self, commands):
184 pass
186 @abstractmethod
187 def flush(self):
188 pass
190 @abstractmethod
191 def invalidate_key(self, key: KeyT):
192 pass
195class _LocalCache(AbstractCache):
196 """
197 A caching mechanism for storing redis commands and their responses.
199 Args:
200 max_size (int): The maximum number of commands to be stored in the cache.
201 ttl (int): The time-to-live for each command in seconds.
202 eviction_policy (EvictionPolicy): The eviction policy to use for removing commands when the cache is full.
204 Attributes:
205 max_size (int): The maximum number of commands to be stored in the cache.
206 ttl (int): The time-to-live for each command in seconds.
207 eviction_policy (EvictionPolicy): The eviction policy used for cache management.
208 cache (OrderedDict): The ordered dictionary to store commands and their metadata.
209 key_commands_map (defaultdict): A mapping of keys to the set of commands that use each key.
210 commands_ttl_list (list): A list to keep track of the commands in the order they were added. # noqa
211 """
213 def __init__(
214 self,
215 max_size: int = 10000,
216 ttl: int = 0,
217 eviction_policy: EvictionPolicy = DEFAULT_EVICTION_POLICY,
218 **kwargs,
219 ):
220 self.max_size = max_size
221 self.ttl = ttl
222 self.eviction_policy = eviction_policy
223 self.cache = OrderedDict()
224 self.key_commands_map = defaultdict(set)
225 self.commands_ttl_list = []
227 def set(self, command: str, response: ResponseT, keys_in_command: List[KeyT]):
228 """
229 Set a redis command and its response in the cache.
231 Args:
232 command (str): The redis command.
233 response (ResponseT): The response associated with the command.
234 keys_in_command (List[KeyT]): The list of keys used in the command.
235 """
236 if len(self.cache) >= self.max_size:
237 self._evict()
238 self.cache[command] = {
239 _RESPONSE: response,
240 _KEYS: keys_in_command,
241 _CTIME: time.monotonic(),
242 _ACCESS_COUNT: 0, # Used only for LFU
243 }
244 self._update_key_commands_map(keys_in_command, command)
245 self.commands_ttl_list.append(command)
247 def get(self, command: str) -> ResponseT:
248 """
249 Get the response for a redis command from the cache.
251 Args:
252 command (str): The redis command.
254 Returns:
255 ResponseT: The response associated with the command, or None if the command is not in the cache. # noqa
256 """
257 if command in self.cache:
258 if self._is_expired(command):
259 self.delete_command(command)
260 return
261 self._update_access(command)
262 return copy.deepcopy(self.cache[command]["response"])
264 def delete_command(self, command: str):
265 """
266 Delete a redis command and its metadata from the cache.
268 Args:
269 command (str): The redis command to be deleted.
270 """
271 if command in self.cache:
272 keys_in_command = self.cache[command].get("keys")
273 self._del_key_commands_map(keys_in_command, command)
274 self.commands_ttl_list.remove(command)
275 del self.cache[command]
277 def delete_many(self, commands):
278 pass
280 def flush(self):
281 """Clear the entire cache, removing all redis commands and metadata."""
282 self.cache.clear()
283 self.key_commands_map.clear()
284 self.commands_ttl_list = []
286 def _is_expired(self, command: str) -> bool:
287 """
288 Check if a redis command has expired based on its time-to-live.
290 Args:
291 command (str): The redis command.
293 Returns:
294 bool: True if the command has expired, False otherwise.
295 """
296 if self.ttl == 0:
297 return False
298 return time.monotonic() - self.cache[command]["ctime"] > self.ttl
300 def _update_access(self, command: str):
301 """
302 Update the access information for a redis command based on the eviction policy.
304 Args:
305 command (str): The redis command.
306 """
307 if self.eviction_policy == EvictionPolicy.LRU.value:
308 self.cache.move_to_end(command)
309 elif self.eviction_policy == EvictionPolicy.LFU.value:
310 self.cache[command]["access_count"] = (
311 self.cache.get(command, {}).get("access_count", 0) + 1
312 )
313 self.cache.move_to_end(command)
314 elif self.eviction_policy == EvictionPolicy.RANDOM.value:
315 pass # Random eviction doesn't require updates
317 def _evict(self):
318 """Evict a redis command from the cache based on the eviction policy."""
319 if self._is_expired(self.commands_ttl_list[0]):
320 self.delete_command(self.commands_ttl_list[0])
321 elif self.eviction_policy == EvictionPolicy.LRU.value:
322 self.cache.popitem(last=False)
323 elif self.eviction_policy == EvictionPolicy.LFU.value:
324 min_access_command = min(
325 self.cache, key=lambda k: self.cache[k].get("access_count", 0)
326 )
327 self.cache.pop(min_access_command)
328 elif self.eviction_policy == EvictionPolicy.RANDOM.value:
329 random_command = random.choice(list(self.cache.keys()))
330 self.cache.pop(random_command)
332 def _update_key_commands_map(self, keys: List[KeyT], command: str):
333 """
334 Update the key_commands_map with command that uses the keys.
336 Args:
337 keys (List[KeyT]): The list of keys used in the command.
338 command (str): The redis command.
339 """
340 for key in keys:
341 self.key_commands_map[key].add(command)
343 def _del_key_commands_map(self, keys: List[KeyT], command: str):
344 """
345 Remove a redis command from the key_commands_map.
347 Args:
348 keys (List[KeyT]): The list of keys used in the redis command.
349 command (str): The redis command.
350 """
351 for key in keys:
352 self.key_commands_map[key].remove(command)
354 def invalidate_key(self, key: KeyT):
355 """
356 Invalidate (delete) all redis commands associated with a specific key.
358 Args:
359 key (KeyT): The key to be invalidated.
360 """
361 if key not in self.key_commands_map:
362 return
363 commands = list(self.key_commands_map[key])
364 for command in commands:
365 self.delete_command(command)