API reference¶
Everything public is importable from the package root:
from action0.django_acache import (
PoolRegistry,
RedisCache,
RedisCacheClient,
aclose_pools,
registry,
)
Backend¶
The cache backend: Django’s Redis cache, with native async methods.
- class action0.django_acache.backend.RedisCache(server, params)[source]¶
Django’s
RedisCache, with async methods that talk to the server throughredis.asyncioinstead of running the sync ones in a worker thread.Configured like Django’s backend, plus an optional
ASYNC_OPTIONSsetting that overridesOPTIONSkeys for the async pools only:CACHES = { "default": { "BACKEND": "action0.django_acache.RedisCache", "LOCATION": "redis://127.0.0.1:6379", }, }
The sync methods are Django’s own. The async ones mirror them one by one, including
aincr()staying a singleINCRon the server; the remaining ones (adecr,aget_or_set,aincr_version, …) are Django’s generic implementations, built on these.- Parameters:
- get_backend_timeout(timeout=<object object>)[source]¶
Django’s timeout conversion, typed as what it returns for Redis: whole seconds.
- async aadd(key, value, timeout=<object object>, version=None)[source]¶
The async counterpart of
add().
- async aset(key, value, timeout=<object object>, version=None)[source]¶
The async counterpart of
set().
- async aset_many(data, timeout=<object object>, version=None)[source]¶
The async counterpart of
set_many().
- async aclose(**kwargs)[source]¶
Do nothing, just like
close()— but without a detour through a worker thread.Django closes its caches after every request, and the async pools are shared by all requests on the event loop, so they are closed at loop shutdown instead (or explicitly, by
aclose_pools()).
Client¶
The cache client: Django’s, plus async twins of its methods.
- class action0.django_acache.client.RedisCacheClient(servers, async_options=None, **options)[source]¶
Django’s Redis cache client, with an async method for each of its sync ones.
The async methods are line-by-line translations of Django’s sync ones onto
redis.asyncio, with the same serializer and the same server selection (writes go to the first server, reads to a random other one), so the two sides read each other’s values. Their pools come from the sharedregistry.- Parameters:
- async aget_client(key=None, *, write=False)[source]¶
The async counterpart of
get_client(): the running loop’s client for a server.
Pools¶
Async connection pools, shared per event loop.
A redis.asyncio connection belongs to the event loop it was opened on, so every loop
needs pools of its own. And under ASGI, Django usually creates a new cache backend instance for
every request: the cache handler keeps its instances in a context-local, each request runs in
a task of its own, and a task only sees the instances its parent context had already created.
Pools kept on the backend instance would mean a new pool, and a new connection to the server,
for every request.
The pools therefore live in one process-wide PoolRegistry, per running event loop
and per configuration: all cache instances with equal settings share them, whichever request
created them. Every loop’s pools are closed when the loop shuts down. asyncio.run(),
asyncio.Runner, ASGI servers and asgiref’s async_to_sync all call
shutdown_asyncgens() before they close a loop, and the registry leaves
an async generator on every loop it serves whose finally clause closes that loop’s pools.
- class action0.django_acache.pools.PoolRegistry[source]¶
The async clients and their pools, per event loop and configuration.
Thread-safe: every thread may run a loop of its own. The lock only guards the mapping of loops, and is never held across an
await.- async client(pool_class, url, options)[source]¶
The running loop’s client for this configuration.
The pool is created on first use, and closed when the loop shuts down; its connections are opened on demand.
- pools(loop=None)[source]¶
The open pools of a loop, in the order they were created.
- Parameters:
loop (
AbstractEventLoop|None, default:None) – the loop; the running one by default- Return type:
- action0.django_acache.pools.registry = <action0.django_acache.pools.PoolRegistry object>¶
the registry that all cache backends of the process share
- async action0.django_acache.pools.aclose_pools()[source]¶
Close the running event loop’s async connection pools now.
They are closed at loop shutdown anyway; this is for when that is too late, e.g. in a test that checks for leaked connections before its loop ends. The pools are shared by all cache backends, so don’t call it while other tasks on the loop still use the cache. Using a cache afterwards simply creates new pools.
- Return type:
Options¶
The configuration of the async connection pools.
Django’s RedisCacheClient turns the cache’s
OPTIONS into keyword arguments for its connection pools. Nearly all of them (db,
password, socket_timeout, …) mean the same to redis.asyncio, so the async
pools start out with the very same ones. A few name redis-py classes that come in a sync and an
async variant, though: pool_class, parser_class, connection_class, retry. The
cache setting ASYNC_OPTIONS overrides keys for the async pools only, and a sync class or
object that would still reach them is an error (see validation).
- action0.django_acache.options.SHARED_ONLY = frozenset({'serializer'})¶
OPTIONSkeys that must stay the same for the sync and the async side: values written by one side have to be readable by the other
- action0.django_acache.options.async_pool_config(pool_options, overrides)[source]¶
Derive the async pool class and pool options from the sync ones.
pool_classandparser_classdefault to theirredis.asynciovariants — never to the sync ones configured inOPTIONS— and, like inOPTIONS, both may be given as dotted import paths.>>> from redis.connection import DefaultParser as SyncParser >>> pool_class, options = async_pool_config( ... {"db": 1, "parser_class": SyncParser}, ... {"pool_class": "redis.asyncio.BlockingConnectionPool"}, ... ) >>> pool_class.__name__, options["db"], options["parser_class"] is DefaultParser ('BlockingConnectionPool', 1, True)
- Parameters:
- Return type:
- Returns:
the pool class and the keyword arguments for its
from_url()- Raises:
ImproperlyConfigured – if
overridescontains a key that must be shared, or if a sync redis-py class or object would reach the async pools
Checks that the async pools get async redis-py classes and objects.
The async pools inherit OPTIONS, so an option naming a sync redis-py class or object reaches
them unless ASYNC_OPTIONS overrides it. Some of those would only fail on first use; a sync
redis.retry.Retry even fails silently: the async connection accepts it, and it never
retries — its call_with_retry() returns the connect coroutine instead of awaiting it. So
these options are checked when the cache client is created, and a wrong one is an error that
names the fix.
- action0.django_acache.validation.RULES: dict[str, Rule] = {'connection_class': (<function _subclass_of.<locals>.<lambda>>, 'a redis.asyncio connection class'), 'pool_class': (<function _subclass_of.<locals>.<lambda>>, 'a redis.asyncio connection pool class'), 'retry': (<function <lambda>>, 'a redis.asyncio.retry.Retry')}¶
the options that come in a sync and an async variant, and what the async pools need
- action0.django_acache.validation.check_async_options(options, overrides)[source]¶
Raise if an option of the async pools is a sync redis-py class or object.
>>> from redis.backoff import NoBackoff >>> from redis.retry import Retry >>> check_async_options({"retry": Retry(NoBackoff(), 3)}, {}) Traceback (most recent call last): ... django.core.exceptions.ImproperlyConfigured: OPTIONS['retry'] is a redis.retry.Retry, which the async side cannot use: set ASYNC_OPTIONS['retry'] to a redis.asyncio.retry.Retry