Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/redis/cache.py: 53%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1from abc import ABC, abstractmethod
2from collections import OrderedDict
3from dataclasses import dataclass
4from enum import Enum
5from typing import Any, List, Optional, Union
8class CacheEntryStatus(Enum):
9 VALID = "VALID"
10 IN_PROGRESS = "IN_PROGRESS"
13class EvictionPolicyType(Enum):
14 time_based = "time_based"
15 frequency_based = "frequency_based"
18@dataclass(frozen=True)
19class CacheKey:
20 """
21 Represents a unique key for a cache entry.
23 Attributes:
24 command (str): The Redis command being cached.
25 redis_keys (tuple): The Redis keys involved in the command.
26 redis_args (tuple): Additional arguments for the Redis command.
27 This field is included in the cache key to ensure uniqueness
28 when commands have the same keys but different arguments.
29 Changing this field will affect cache key uniqueness.
30 """
32 command: str
33 redis_keys: tuple
34 redis_args: tuple = () # Additional arguments for the Redis command; affects cache key uniqueness.
37class CacheEntry:
38 def __init__(
39 self,
40 cache_key: CacheKey,
41 cache_value: bytes,
42 status: CacheEntryStatus,
43 connection_ref,
44 ):
45 self.cache_key = cache_key
46 self.cache_value = cache_value
47 self.status = status
48 self.connection_ref = connection_ref
50 def __hash__(self):
51 return hash(
52 (self.cache_key, self.cache_value, self.status, self.connection_ref)
53 )
55 def __eq__(self, other):
56 return hash(self) == hash(other)
59class EvictionPolicyInterface(ABC):
60 @property
61 @abstractmethod
62 def cache(self):
63 pass
65 @cache.setter
66 @abstractmethod
67 def cache(self, value):
68 pass
70 @property
71 @abstractmethod
72 def type(self) -> EvictionPolicyType:
73 pass
75 @abstractmethod
76 def evict_next(self) -> CacheKey:
77 pass
79 @abstractmethod
80 def evict_many(self, count: int) -> List[CacheKey]:
81 pass
83 @abstractmethod
84 def touch(self, cache_key: CacheKey) -> None:
85 pass
88class CacheConfigurationInterface(ABC):
89 @abstractmethod
90 def get_cache_class(self):
91 pass
93 @abstractmethod
94 def get_max_size(self) -> int:
95 pass
97 @abstractmethod
98 def get_eviction_policy(self):
99 pass
101 @abstractmethod
102 def is_exceeds_max_size(self, count: int) -> bool:
103 pass
105 @abstractmethod
106 def is_allowed_to_cache(self, command: str) -> bool:
107 pass
110class CacheInterface(ABC):
111 @property
112 @abstractmethod
113 def collection(self) -> OrderedDict:
114 pass
116 @property
117 @abstractmethod
118 def config(self) -> CacheConfigurationInterface:
119 pass
121 @property
122 @abstractmethod
123 def eviction_policy(self) -> EvictionPolicyInterface:
124 pass
126 @property
127 @abstractmethod
128 def size(self) -> int:
129 pass
131 @abstractmethod
132 def get(self, key: CacheKey) -> Union[CacheEntry, None]:
133 pass
135 @abstractmethod
136 def set(self, entry: CacheEntry) -> bool:
137 pass
139 @abstractmethod
140 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]:
141 pass
143 @abstractmethod
144 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]:
145 pass
147 @abstractmethod
148 def flush(self) -> int:
149 pass
151 @abstractmethod
152 def is_cachable(self, key: CacheKey) -> bool:
153 pass
156class DefaultCache(CacheInterface):
157 def __init__(
158 self,
159 cache_config: CacheConfigurationInterface,
160 ) -> None:
161 self._cache = OrderedDict()
162 self._cache_config = cache_config
163 self._eviction_policy = self._cache_config.get_eviction_policy().value()
164 self._eviction_policy.cache = self
166 @property
167 def collection(self) -> OrderedDict:
168 return self._cache
170 @property
171 def config(self) -> CacheConfigurationInterface:
172 return self._cache_config
174 @property
175 def eviction_policy(self) -> EvictionPolicyInterface:
176 return self._eviction_policy
178 @property
179 def size(self) -> int:
180 return len(self._cache)
182 def set(self, entry: CacheEntry) -> bool:
183 if not self.is_cachable(entry.cache_key):
184 return False
186 self._cache[entry.cache_key] = entry
187 self._eviction_policy.touch(entry.cache_key)
189 if self._cache_config.is_exceeds_max_size(len(self._cache)):
190 self._eviction_policy.evict_next()
192 return True
194 def get(self, key: CacheKey) -> Union[CacheEntry, None]:
195 entry = self._cache.get(key, None)
197 if entry is None:
198 return None
200 self._eviction_policy.touch(key)
201 return entry
203 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]:
204 response = []
206 for key in cache_keys:
207 if self.get(key) is not None:
208 self._cache.pop(key)
209 response.append(True)
210 else:
211 response.append(False)
213 return response
215 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]:
216 response = []
217 keys_to_delete = []
219 for redis_key in redis_keys:
220 if isinstance(redis_key, bytes):
221 redis_key = redis_key.decode()
222 for cache_key in self._cache:
223 if redis_key in cache_key.redis_keys:
224 keys_to_delete.append(cache_key)
225 response.append(True)
227 for key in keys_to_delete:
228 self._cache.pop(key)
230 return response
232 def flush(self) -> int:
233 elem_count = len(self._cache)
234 self._cache.clear()
235 return elem_count
237 def is_cachable(self, key: CacheKey) -> bool:
238 return self._cache_config.is_allowed_to_cache(key.command)
241class LRUPolicy(EvictionPolicyInterface):
242 def __init__(self):
243 self.cache = None
245 @property
246 def cache(self):
247 return self._cache
249 @cache.setter
250 def cache(self, cache: CacheInterface):
251 self._cache = cache
253 @property
254 def type(self) -> EvictionPolicyType:
255 return EvictionPolicyType.time_based
257 def evict_next(self) -> CacheKey:
258 self._assert_cache()
259 popped_entry = self._cache.collection.popitem(last=False)
260 return popped_entry[0]
262 def evict_many(self, count: int) -> List[CacheKey]:
263 self._assert_cache()
264 if count > len(self._cache.collection):
265 raise ValueError("Evictions count is above cache size")
267 popped_keys = []
269 for _ in range(count):
270 popped_entry = self._cache.collection.popitem(last=False)
271 popped_keys.append(popped_entry[0])
273 return popped_keys
275 def touch(self, cache_key: CacheKey) -> None:
276 self._assert_cache()
278 if self._cache.collection.get(cache_key) is None:
279 raise ValueError("Given entry does not belong to the cache")
281 self._cache.collection.move_to_end(cache_key)
283 def _assert_cache(self):
284 if self.cache is None or not isinstance(self.cache, CacheInterface):
285 raise ValueError("Eviction policy should be associated with valid cache.")
288class EvictionPolicy(Enum):
289 LRU = LRUPolicy
292class CacheConfig(CacheConfigurationInterface):
293 DEFAULT_CACHE_CLASS = DefaultCache
294 DEFAULT_EVICTION_POLICY = EvictionPolicy.LRU
295 DEFAULT_MAX_SIZE = 10000
297 DEFAULT_ALLOW_LIST = [
298 "BITCOUNT",
299 "BITFIELD_RO",
300 "BITPOS",
301 "EXISTS",
302 "GEODIST",
303 "GEOHASH",
304 "GEOPOS",
305 "GEORADIUSBYMEMBER_RO",
306 "GEORADIUS_RO",
307 "GEOSEARCH",
308 "GET",
309 "GETBIT",
310 "GETRANGE",
311 "HEXISTS",
312 "HGET",
313 "HGETALL",
314 "HKEYS",
315 "HLEN",
316 "HMGET",
317 "HSTRLEN",
318 "HVALS",
319 "JSON.ARRINDEX",
320 "JSON.ARRLEN",
321 "JSON.GET",
322 "JSON.MGET",
323 "JSON.OBJKEYS",
324 "JSON.OBJLEN",
325 "JSON.RESP",
326 "JSON.STRLEN",
327 "JSON.TYPE",
328 "LCS",
329 "LINDEX",
330 "LLEN",
331 "LPOS",
332 "LRANGE",
333 "MGET",
334 "SCARD",
335 "SDIFF",
336 "SINTER",
337 "SINTERCARD",
338 "SISMEMBER",
339 "SMEMBERS",
340 "SMISMEMBER",
341 "SORT_RO",
342 "STRLEN",
343 "SUBSTR",
344 "SUNION",
345 "TS.GET",
346 "TS.INFO",
347 "TS.RANGE",
348 "TS.REVRANGE",
349 "TYPE",
350 "XLEN",
351 "XPENDING",
352 "XRANGE",
353 "XREAD",
354 "XREVRANGE",
355 "ZCARD",
356 "ZCOUNT",
357 "ZDIFF",
358 "ZINTER",
359 "ZINTERCARD",
360 "ZLEXCOUNT",
361 "ZMSCORE",
362 "ZRANGE",
363 "ZRANGEBYLEX",
364 "ZRANGEBYSCORE",
365 "ZRANK",
366 "ZREVRANGE",
367 "ZREVRANGEBYLEX",
368 "ZREVRANGEBYSCORE",
369 "ZREVRANK",
370 "ZSCORE",
371 "ZUNION",
372 ]
374 def __init__(
375 self,
376 max_size: int = DEFAULT_MAX_SIZE,
377 cache_class: Any = DEFAULT_CACHE_CLASS,
378 eviction_policy: EvictionPolicy = DEFAULT_EVICTION_POLICY,
379 ):
380 self._cache_class = cache_class
381 self._max_size = max_size
382 self._eviction_policy = eviction_policy
384 def get_cache_class(self):
385 return self._cache_class
387 def get_max_size(self) -> int:
388 return self._max_size
390 def get_eviction_policy(self) -> EvictionPolicy:
391 return self._eviction_policy
393 def is_exceeds_max_size(self, count: int) -> bool:
394 return count > self._max_size
396 def is_allowed_to_cache(self, command: str) -> bool:
397 return command in self.DEFAULT_ALLOW_LIST
400class CacheFactoryInterface(ABC):
401 @abstractmethod
402 def get_cache(self) -> CacheInterface:
403 pass
406class CacheFactory(CacheFactoryInterface):
407 def __init__(self, cache_config: Optional[CacheConfig] = None):
408 self._config = cache_config
410 if self._config is None:
411 self._config = CacheConfig()
413 def get_cache(self) -> CacheInterface:
414 cache_class = self._config.get_cache_class()
415 return cache_class(cache_config=self._config)