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

224 statements  

1from abc import ABC, abstractmethod 

2from collections import OrderedDict 

3from dataclasses import dataclass 

4from enum import Enum 

5from typing import Any, List, Optional, Union 

6 

7 

8class CacheEntryStatus(Enum): 

9 VALID = "VALID" 

10 IN_PROGRESS = "IN_PROGRESS" 

11 

12 

13class EvictionPolicyType(Enum): 

14 time_based = "time_based" 

15 frequency_based = "frequency_based" 

16 

17 

18@dataclass(frozen=True) 

19class CacheKey: 

20 """ 

21 Represents a unique key for a cache entry. 

22 

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 """ 

31 

32 command: str 

33 redis_keys: tuple 

34 redis_args: tuple = () # Additional arguments for the Redis command; affects cache key uniqueness. 

35 

36 

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 

49 

50 def __hash__(self): 

51 return hash( 

52 (self.cache_key, self.cache_value, self.status, self.connection_ref) 

53 ) 

54 

55 def __eq__(self, other): 

56 return hash(self) == hash(other) 

57 

58 

59class EvictionPolicyInterface(ABC): 

60 @property 

61 @abstractmethod 

62 def cache(self): 

63 pass 

64 

65 @cache.setter 

66 @abstractmethod 

67 def cache(self, value): 

68 pass 

69 

70 @property 

71 @abstractmethod 

72 def type(self) -> EvictionPolicyType: 

73 pass 

74 

75 @abstractmethod 

76 def evict_next(self) -> CacheKey: 

77 pass 

78 

79 @abstractmethod 

80 def evict_many(self, count: int) -> List[CacheKey]: 

81 pass 

82 

83 @abstractmethod 

84 def touch(self, cache_key: CacheKey) -> None: 

85 pass 

86 

87 

88class CacheConfigurationInterface(ABC): 

89 @abstractmethod 

90 def get_cache_class(self): 

91 pass 

92 

93 @abstractmethod 

94 def get_max_size(self) -> int: 

95 pass 

96 

97 @abstractmethod 

98 def get_eviction_policy(self): 

99 pass 

100 

101 @abstractmethod 

102 def is_exceeds_max_size(self, count: int) -> bool: 

103 pass 

104 

105 @abstractmethod 

106 def is_allowed_to_cache(self, command: str) -> bool: 

107 pass 

108 

109 

110class CacheInterface(ABC): 

111 @property 

112 @abstractmethod 

113 def collection(self) -> OrderedDict: 

114 pass 

115 

116 @property 

117 @abstractmethod 

118 def config(self) -> CacheConfigurationInterface: 

119 pass 

120 

121 @property 

122 @abstractmethod 

123 def eviction_policy(self) -> EvictionPolicyInterface: 

124 pass 

125 

126 @property 

127 @abstractmethod 

128 def size(self) -> int: 

129 pass 

130 

131 @abstractmethod 

132 def get(self, key: CacheKey) -> Union[CacheEntry, None]: 

133 pass 

134 

135 @abstractmethod 

136 def set(self, entry: CacheEntry) -> bool: 

137 pass 

138 

139 @abstractmethod 

140 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]: 

141 pass 

142 

143 @abstractmethod 

144 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]: 

145 pass 

146 

147 @abstractmethod 

148 def flush(self) -> int: 

149 pass 

150 

151 @abstractmethod 

152 def is_cachable(self, key: CacheKey) -> bool: 

153 pass 

154 

155 

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 

165 

166 @property 

167 def collection(self) -> OrderedDict: 

168 return self._cache 

169 

170 @property 

171 def config(self) -> CacheConfigurationInterface: 

172 return self._cache_config 

173 

174 @property 

175 def eviction_policy(self) -> EvictionPolicyInterface: 

176 return self._eviction_policy 

177 

178 @property 

179 def size(self) -> int: 

180 return len(self._cache) 

181 

182 def set(self, entry: CacheEntry) -> bool: 

183 if not self.is_cachable(entry.cache_key): 

184 return False 

185 

186 self._cache[entry.cache_key] = entry 

187 self._eviction_policy.touch(entry.cache_key) 

188 

189 if self._cache_config.is_exceeds_max_size(len(self._cache)): 

190 self._eviction_policy.evict_next() 

191 

192 return True 

193 

194 def get(self, key: CacheKey) -> Union[CacheEntry, None]: 

195 entry = self._cache.get(key, None) 

196 

197 if entry is None: 

198 return None 

199 

200 self._eviction_policy.touch(key) 

201 return entry 

202 

203 def delete_by_cache_keys(self, cache_keys: List[CacheKey]) -> List[bool]: 

204 response = [] 

205 

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) 

212 

213 return response 

214 

215 def delete_by_redis_keys(self, redis_keys: List[bytes]) -> List[bool]: 

216 response = [] 

217 keys_to_delete = [] 

218 

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) 

226 

227 for key in keys_to_delete: 

228 self._cache.pop(key) 

229 

230 return response 

231 

232 def flush(self) -> int: 

233 elem_count = len(self._cache) 

234 self._cache.clear() 

235 return elem_count 

236 

237 def is_cachable(self, key: CacheKey) -> bool: 

238 return self._cache_config.is_allowed_to_cache(key.command) 

239 

240 

241class LRUPolicy(EvictionPolicyInterface): 

242 def __init__(self): 

243 self.cache = None 

244 

245 @property 

246 def cache(self): 

247 return self._cache 

248 

249 @cache.setter 

250 def cache(self, cache: CacheInterface): 

251 self._cache = cache 

252 

253 @property 

254 def type(self) -> EvictionPolicyType: 

255 return EvictionPolicyType.time_based 

256 

257 def evict_next(self) -> CacheKey: 

258 self._assert_cache() 

259 popped_entry = self._cache.collection.popitem(last=False) 

260 return popped_entry[0] 

261 

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") 

266 

267 popped_keys = [] 

268 

269 for _ in range(count): 

270 popped_entry = self._cache.collection.popitem(last=False) 

271 popped_keys.append(popped_entry[0]) 

272 

273 return popped_keys 

274 

275 def touch(self, cache_key: CacheKey) -> None: 

276 self._assert_cache() 

277 

278 if self._cache.collection.get(cache_key) is None: 

279 raise ValueError("Given entry does not belong to the cache") 

280 

281 self._cache.collection.move_to_end(cache_key) 

282 

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.") 

286 

287 

288class EvictionPolicy(Enum): 

289 LRU = LRUPolicy 

290 

291 

292class CacheConfig(CacheConfigurationInterface): 

293 DEFAULT_CACHE_CLASS = DefaultCache 

294 DEFAULT_EVICTION_POLICY = EvictionPolicy.LRU 

295 DEFAULT_MAX_SIZE = 10000 

296 

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 ] 

373 

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 

383 

384 def get_cache_class(self): 

385 return self._cache_class 

386 

387 def get_max_size(self) -> int: 

388 return self._max_size 

389 

390 def get_eviction_policy(self) -> EvictionPolicy: 

391 return self._eviction_policy 

392 

393 def is_exceeds_max_size(self, count: int) -> bool: 

394 return count > self._max_size 

395 

396 def is_allowed_to_cache(self, command: str) -> bool: 

397 return command in self.DEFAULT_ALLOW_LIST 

398 

399 

400class CacheFactoryInterface(ABC): 

401 @abstractmethod 

402 def get_cache(self) -> CacheInterface: 

403 pass 

404 

405 

406class CacheFactory(CacheFactoryInterface): 

407 def __init__(self, cache_config: Optional[CacheConfig] = None): 

408 self._config = cache_config 

409 

410 if self._config is None: 

411 self._config = CacheConfig() 

412 

413 def get_cache(self) -> CacheInterface: 

414 cache_class = self._config.get_cache_class() 

415 return cache_class(cache_config=self._config)