1"Base Cache class."
2import time
3import warnings
4
5from asgiref.sync import sync_to_async
6
7from django.core.exceptions import ImproperlyConfigured
8from django.utils.module_loading import import_string
9from django.utils.regex_helper import _lazy_re_compile
10
11
12class InvalidCacheBackendError(ImproperlyConfigured):
13 pass
14
15
16class CacheKeyWarning(RuntimeWarning):
17 pass
18
19
20class InvalidCacheKey(ValueError):
21 pass
22
23
24# Stub class to ensure not passing in a `timeout` argument results in
25# the default timeout
26DEFAULT_TIMEOUT = object()
27
28# Memcached does not accept keys longer than this.
29MEMCACHE_MAX_KEY_LENGTH = 250
30
31
32def default_key_func(key, key_prefix, version):
33 """
34 Default function to generate keys.
35
36 Construct the key used by all other methods. By default, prepend
37 the `key_prefix`. KEY_FUNCTION can be used to specify an alternate
38 function with custom key making behavior.
39 """
40 return "%s:%s:%s" % (key_prefix, version, key)
41
42
43def get_key_func(key_func):
44 """
45 Function to decide which key function to use.
46
47 Default to ``default_key_func``.
48 """
49 if key_func is not None:
50 if callable(key_func):
51 return key_func
52 else:
53 return import_string(key_func)
54 return default_key_func
55
56
57class BaseCache:
58 _missing_key = object()
59
60 def __init__(self, params):
61 timeout = params.get("timeout", params.get("TIMEOUT", 300))
62 if timeout is not None:
63 try:
64 timeout = int(timeout)
65 except (ValueError, TypeError):
66 timeout = 300
67 self.default_timeout = timeout
68
69 options = params.get("OPTIONS", {})
70 max_entries = params.get("max_entries", options.get("MAX_ENTRIES", 300))
71 try:
72 self._max_entries = int(max_entries)
73 except (ValueError, TypeError):
74 self._max_entries = 300
75
76 cull_frequency = params.get("cull_frequency", options.get("CULL_FREQUENCY", 3))
77 try:
78 self._cull_frequency = int(cull_frequency)
79 except (ValueError, TypeError):
80 self._cull_frequency = 3
81
82 self.key_prefix = params.get("KEY_PREFIX", "")
83 self.version = params.get("VERSION", 1)
84 self.key_func = get_key_func(params.get("KEY_FUNCTION"))
85
86 def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT):
87 """
88 Return the timeout value usable by this backend based upon the provided
89 timeout.
90 """
91 if timeout == DEFAULT_TIMEOUT:
92 timeout = self.default_timeout
93 elif timeout == 0:
94 # ticket 21147 - avoid time.time() related precision issues
95 timeout = -1
96 return None if timeout is None else time.time() + timeout
97
98 def make_key(self, key, version=None):
99 """
100 Construct the key used by all other methods. By default, use the
101 key_func to generate a key (which, by default, prepends the
102 `key_prefix' and 'version'). A different key function can be provided
103 at the time of cache construction; alternatively, you can subclass the
104 cache backend to provide custom key making behavior.
105 """
106 if version is None:
107 version = self.version
108
109 return self.key_func(key, self.key_prefix, version)
110
111 def validate_key(self, key):
112 """
113 Warn about keys that would not be portable to the memcached
114 backend. This encourages (but does not force) writing backend-portable
115 cache code.
116 """
117 for warning in memcache_key_warnings(key):
118 warnings.warn(warning, CacheKeyWarning)
119
120 def make_and_validate_key(self, key, version=None):
121 """Helper to make and validate keys."""
122 key = self.make_key(key, version=version)
123 self.validate_key(key)
124 return key
125
126 def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
127 """
128 Set a value in the cache if the key does not already exist. If
129 timeout is given, use that timeout for the key; otherwise use the
130 default cache timeout.
131
132 Return True if the value was stored, False otherwise.
133 """
134 raise NotImplementedError(
135 "subclasses of BaseCache must provide an add() method"
136 )
137
138 async def aadd(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
139 return await sync_to_async(self.add, thread_sensitive=True)(
140 key, value, timeout, version
141 )
142
143 def get(self, key, default=None, version=None):
144 """
145 Fetch a given key from the cache. If the key does not exist, return
146 default, which itself defaults to None.
147 """
148 raise NotImplementedError("subclasses of BaseCache must provide a get() method")
149
150 async def aget(self, key, default=None, version=None):
151 return await sync_to_async(self.get, thread_sensitive=True)(
152 key, default, version
153 )
154
155 def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
156 """
157 Set a value in the cache. If timeout is given, use that timeout for the
158 key; otherwise use the default cache timeout.
159 """
160 raise NotImplementedError("subclasses of BaseCache must provide a set() method")
161
162 async def aset(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
163 return await sync_to_async(self.set, thread_sensitive=True)(
164 key, value, timeout, version
165 )
166
167 def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
168 """
169 Update the key's expiry time using timeout. Return True if successful
170 or False if the key does not exist.
171 """
172 raise NotImplementedError(
173 "subclasses of BaseCache must provide a touch() method"
174 )
175
176 async def atouch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
177 return await sync_to_async(self.touch, thread_sensitive=True)(
178 key, timeout, version
179 )
180
181 def delete(self, key, version=None):
182 """
183 Delete a key from the cache and return whether it succeeded, failing
184 silently.
185 """
186 raise NotImplementedError(
187 "subclasses of BaseCache must provide a delete() method"
188 )
189
190 async def adelete(self, key, version=None):
191 return await sync_to_async(self.delete, thread_sensitive=True)(key, version)
192
193 def get_many(self, keys, version=None):
194 """
195 Fetch a bunch of keys from the cache. For certain backends (memcached,
196 pgsql) this can be *much* faster when fetching multiple values.
197
198 Return a dict mapping each key in keys to its value. If the given
199 key is missing, it will be missing from the response dict.
200 """
201 d = {}
202 for k in keys:
203 val = self.get(k, self._missing_key, version=version)
204 if val is not self._missing_key:
205 d[k] = val
206 return d
207
208 async def aget_many(self, keys, version=None):
209 """See get_many()."""
210 d = {}
211 for k in keys:
212 val = await self.aget(k, self._missing_key, version=version)
213 if val is not self._missing_key:
214 d[k] = val
215 return d
216
217 def get_or_set(self, key, default, timeout=DEFAULT_TIMEOUT, version=None):
218 """
219 Fetch a given key from the cache. If the key does not exist,
220 add the key and set it to the default value. The default value can
221 also be any callable. If timeout is given, use that timeout for the
222 key; otherwise use the default cache timeout.
223
224 Return the value of the key stored or retrieved.
225 """
226 val = self.get(key, self._missing_key, version=version)
227 if val is self._missing_key:
228 if callable(default):
229 default = default()
230 self.add(key, default, timeout=timeout, version=version)
231 # Fetch the value again to avoid a race condition if another caller
232 # added a value between the first get() and the add() above.
233 return self.get(key, default, version=version)
234 return val
235
236 async def aget_or_set(self, key, default, timeout=DEFAULT_TIMEOUT, version=None):
237 """See get_or_set()."""
238 val = await self.aget(key, self._missing_key, version=version)
239 if val is self._missing_key:
240 if callable(default):
241 default = default()
242 await self.aadd(key, default, timeout=timeout, version=version)
243 # Fetch the value again to avoid a race condition if another caller
244 # added a value between the first aget() and the aadd() above.
245 return await self.aget(key, default, version=version)
246 return val
247
248 def has_key(self, key, version=None):
249 """
250 Return True if the key is in the cache and has not expired.
251 """
252 return (
253 self.get(key, self._missing_key, version=version) is not self._missing_key
254 )
255
256 async def ahas_key(self, key, version=None):
257 return (
258 await self.aget(key, self._missing_key, version=version)
259 is not self._missing_key
260 )
261
262 def incr(self, key, delta=1, version=None):
263 """
264 Add delta to value in the cache. If the key does not exist, raise a
265 ValueError exception.
266 """
267 value = self.get(key, self._missing_key, version=version)
268 if value is self._missing_key:
269 raise ValueError("Key '%s' not found" % key)
270 new_value = value + delta
271 self.set(key, new_value, version=version)
272 return new_value
273
274 async def aincr(self, key, delta=1, version=None):
275 """See incr()."""
276 value = await self.aget(key, self._missing_key, version=version)
277 if value is self._missing_key:
278 raise ValueError("Key '%s' not found" % key)
279 new_value = value + delta
280 await self.aset(key, new_value, version=version)
281 return new_value
282
283 def decr(self, key, delta=1, version=None):
284 """
285 Subtract delta from value in the cache. If the key does not exist, raise
286 a ValueError exception.
287 """
288 return self.incr(key, -delta, version=version)
289
290 async def adecr(self, key, delta=1, version=None):
291 return await self.aincr(key, -delta, version=version)
292
293 def __contains__(self, key):
294 """
295 Return True if the key is in the cache and has not expired.
296 """
297 # This is a separate method, rather than just a copy of has_key(),
298 # so that it always has the same functionality as has_key(), even
299 # if a subclass overrides it.
300 return self.has_key(key)
301
302 def set_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
303 """
304 Set a bunch of values in the cache at once from a dict of key/value
305 pairs. For certain backends (memcached), this is much more efficient
306 than calling set() multiple times.
307
308 If timeout is given, use that timeout for the key; otherwise use the
309 default cache timeout.
310
311 On backends that support it, return a list of keys that failed
312 insertion, or an empty list if all keys were inserted successfully.
313 """
314 for key, value in data.items():
315 self.set(key, value, timeout=timeout, version=version)
316 return []
317
318 async def aset_many(self, data, timeout=DEFAULT_TIMEOUT, version=None):
319 for key, value in data.items():
320 await self.aset(key, value, timeout=timeout, version=version)
321 return []
322
323 def delete_many(self, keys, version=None):
324 """
325 Delete a bunch of values in the cache at once. For certain backends
326 (memcached), this is much more efficient than calling delete() multiple
327 times.
328 """
329 for key in keys:
330 self.delete(key, version=version)
331
332 async def adelete_many(self, keys, version=None):
333 for key in keys:
334 await self.adelete(key, version=version)
335
336 def clear(self):
337 """Remove *all* values from the cache at once."""
338 raise NotImplementedError(
339 "subclasses of BaseCache must provide a clear() method"
340 )
341
342 async def aclear(self):
343 return await sync_to_async(self.clear, thread_sensitive=True)()
344
345 def incr_version(self, key, delta=1, version=None):
346 """
347 Add delta to the cache version for the supplied key. Return the new
348 version.
349 """
350 if version is None:
351 version = self.version
352
353 value = self.get(key, self._missing_key, version=version)
354 if value is self._missing_key:
355 raise ValueError("Key '%s' not found" % key)
356
357 self.set(key, value, version=version + delta)
358 self.delete(key, version=version)
359 return version + delta
360
361 async def aincr_version(self, key, delta=1, version=None):
362 """See incr_version()."""
363 if version is None:
364 version = self.version
365
366 value = await self.aget(key, self._missing_key, version=version)
367 if value is self._missing_key:
368 raise ValueError("Key '%s' not found" % key)
369
370 await self.aset(key, value, version=version + delta)
371 await self.adelete(key, version=version)
372 return version + delta
373
374 def decr_version(self, key, delta=1, version=None):
375 """
376 Subtract delta from the cache version for the supplied key. Return the
377 new version.
378 """
379 return self.incr_version(key, -delta, version)
380
381 async def adecr_version(self, key, delta=1, version=None):
382 return await self.aincr_version(key, -delta, version)
383
384 def close(self, **kwargs):
385 """Close the cache connection"""
386 pass
387
388 async def aclose(self, **kwargs):
389 pass
390
391
392memcached_error_chars_re = _lazy_re_compile(r"[\x00-\x20\x7f]")
393
394
395def memcache_key_warnings(key):
396 if len(key) > MEMCACHE_MAX_KEY_LENGTH:
397 yield (
398 "Cache key will cause errors if used with memcached: %r "
399 "(longer than %s)" % (key, MEMCACHE_MAX_KEY_LENGTH)
400 )
401 if memcached_error_chars_re.search(key):
402 yield (
403 "Cache key contains characters that will cause errors if used with "
404 f"memcached: {key!r}"
405 )