Chapter 16 · Caching, files, mail, signals and tasks
16.3 The cache API: caches and BaseCache
django, a CacheHandler, makes a backend for each alias of CACHES, and cache stands for the backend of "default". Every backend Django ships is a subclass of BaseCache, which reads the alias’s options, makes the key the backend uses, turns a timeout into a moment of expiry, and builds the rest of the API, asynchronous half included, on the few methods each backend writes.
Code that caches gives a key, a value, perhaps a timeout and a version, and expects the same calls to work whatever the store. So a backend writes the methods that touch its store (The cache backends), and BaseCache does what is the same for all.
caches, cache and the handler
CacheHandler is a BaseConnectionHandler, as the database’s handler is (Connections by alias: DATABASES and ConnectionHandler). CacheHandler copies the alias’s dictionary, pops "BACKEND" and "LOCATION", the location defaulting to the empty string, imports the class and calls it with the location and the rest of the dictionary. The location is a name, a directory, a table or servers, by backend. A class that will not import, and an alias CACHES lacks, are refused with InvalidCacheBackendError, the second as CacheHandler.
The handler leaves BaseConnectionHandler false, where the database’s handler sets it, so its asgiref Local is a context variable: each thread still makes its own backends, but code run through sync_to_async or async_to_sync sees its caller’s.
cache is a ConnectionProxy of the handler and DEFAULT_CACHE_ALIAS: each read of an attribute the proxy lacks, each attribute set or deleted on it, and in and ==, go to caches["default"], looked up again each time. The example’s settings, with a second alias:
# harbour/settings.py, in part
CACHES = {
"default": {"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "harbour"},
"berths": {
"BACKEND": "django.core.cache.backends.locmem.LocMemCache", "LOCATION": "berths",
"TIMEOUT": 60, "KEY_PREFIX": "office", "VERSION": 2,
},
}
cache is caches["default"] -> False
cache == caches["default"] -> True
caches["nosuch"] -> raises InvalidCacheBackendError: The connection 'nosuch' doesn't exist.
close_caches, a receiver of request_finished, closes the backends the current thread or context has made. BaseCache.close does nothing; of Django’s backends only PyMemcacheCache acts on closing.
The options: BaseCache.__init__
BaseCache reads the rest of the dictionary:
| option | default | kept as |
|---|---|---|
"TIMEOUT" |
default 300 | kept as Base |
"MAX_ENTRIES", in "OPTIONS" |
default 300 | kept as Base |
"CULL_FREQUENCY", in "OPTIONS" |
default 3 | kept as Base |
"KEY_PREFIX" |
default "" |
kept as Base |
"VERSION" |
default 1 | kept as Base |
"KEY_FUNCTION" |
default default_key_func |
kept as Base |
The first three go through int, a timeout of None excepted, and a value int refuses, such as "five minutes", silently gives way to the default. Only the backends that cull, in memory, in files and in a table, use the culling options.
Keys: make_key and validate_key
A backend works with the made key, made from the key the code gave. BaseCache passes the key, the prefix and a version, the call’s or else the cache’s, to the key function, and default_key_func joins the three with colons. In Django’s documentation, a prefix keeps apart the Django instances that share one cache, and a version retires old values without clearing the rest. get_key_func uses a callable "KEY_FUNCTION" as it is and imports a dotted path.
cache.make_key("sailings") -> ':1:sailings'
caches["berths"].make_key("sailings") -> 'office:2:sailings'
caches["berths"].make_key("sailings", version=3) -> 'office:3:sailings'
cache.set("berth list", [3, 7]) -> None
warns CacheKeyWarning: Cache key contains characters that will cause errors if used with memcached: ':1:berth list'
BaseCache issues a CacheKeyWarning for each message of memcache_key_warnings: a key longer than MEMCACHE_MAX_KEY_LENGTH, 250, or holding a control character or a space. It is given the made key, so the prefix counts towards the length. It only warns, and the value is stored: its docstring says it encourages, but does not force, code that would run on Memcached, whose backends raise InvalidCacheKey instead.
BaseCache does both, and is what a backend’s own methods call on the key they are given; the methods BaseCache builds on them make no key, and pass the caller’s down. The keys of {% cache %}, from make_template_fragment_key, are ordinary keys here (The libraries to load: static, i18n, l10n, tz and cache).
Timeouts: get_backend_timeout
The methods that take a timeout default it to DEFAULT_TIMEOUT, a sentinel meaning none given, which leaves None to mean never. BaseCache makes the sentinel the cache’s default, keeps None, and adds a number of seconds to time.time(), giving the moment of expiry. A timeout of 0 given in a call keeps nothing: it is made -1, so the expiry is already past, the comment citing ticket 21147 and the precision of time.time(). Memcached’s and Redis’s backends replace it.
The methods built on a backend’s own
BaseCache raises NotImplementedError from BaseCache.add, BaseCache.get, BaseCache.set, BaseCache.touch, BaseCache.delete and BaseCache.clear. Their docstrings fix their returns: add, whether it stored the value, which it does only for an absent key; touch, whether the key was there; delete, whether it succeeded. The rest, but for BaseCache.close, have defaults built on these, which a backend may replace.
BaseCache and BaseCache call get with the sentinel BaseCache as the default, so a stored None is told from an absent key. BaseCache calls has_key, its comment says, so that in follows any override of it. BaseCache and BaseCache loop over set and delete; set_many returns an empty list of failed keys, which the Memcached backends fill.
BaseCache gets the key; on a miss it calls the default if callable, adds the result and gets the key again. The comment’s reason for the second get: another caller may have added a value meanwhile, which add left in place. The computed value, as that get’s default, is returned even when nothing was kept:
cache.add("tide", "high", 0) -> True
cache.get("tide") -> None
cache.get_or_set("tide", "high", 0) -> 'high'
cache.get("tide") -> None
BaseCache.incr gets the value, raising ValueError if it is missing, and sets the sum; BaseCache.decr adds the negative. BaseCache gets the value at a version, the cache’s by default, sets it at the version plus the delta, deletes the old entry and returns the new version. The base’s error names the key given, LocMemCache.incr’s the made key:
cache.set("berths", ["north", "south"]) -> None
cache.incr_version("berths") -> 2
cache.get("berths") -> None
cache.get("berths", version=2) -> ['north', 'south']
cache.incr_version("nosuch") -> raises ValueError: Key 'nosuch' not found
cache.incr("nosuch") -> raises ValueError: Key ':1:nosuch' not found
The asynchronous twins
Each method of the API, but in and the helpers for keys and timeouts, has a twin with an a in front, such as BaseCache.aget, and every twin is BaseCache’s: no backend Django ships writes one, and its documentation says Django does not yet support asynchronous caching.
The twins of the methods a backend must write, and BaseCache.aclose, call the method through sync_to_async with thread_sensitive=True, on the thread kept for such work, under ASGI the request’s own (Sync and async in one chain). The twins of the defaults, but for BaseCache.adecr and BaseCache, which call aincr and aincr_version, run the backend’s replacement, if it has one, in one such call, and otherwise repeat the default’s steps through the other twins, each a crossing of its own. On LocMemCache, BaseCache crosses three times on a miss and BaseCache.aincr once, to LocMemCache.incr.