Usage¶
Setup¶
Install the package (with hiredis for the faster C parser, if you like):
pip install action0-django-acache # or: uv add action0-django-acache
pip install "action0-django-acache[hiredis]"
Then point a cache at the backend. Everything Django’s
Redis backend
accepts works the same way: one or several servers in LOCATION, TIMEOUT,
KEY_PREFIX, VERSION, and the redis-py connection options in OPTIONS.
CACHES = {
"default": {
"BACKEND": "action0.django_acache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"TIMEOUT": 600,
"OPTIONS": {
"socket_timeout": 2,
},
},
}
Async methods¶
Use the cache’s async methods in async views, middleware, consumers or tasks, exactly as with any Django cache:
from django.core.cache import cache
from django.http import JsonResponse
async def weather(request, city):
report = await cache.aget(f"weather:{city}")
if report is None:
report = await fetch_weather(city)
await cache.aset(f"weather:{city}", report, timeout=300)
return JsonResponse(report)
Django’s own backend implements these by running its sync methods in a worker
thread (asgiref’s sync_to_async), which costs a thread hop per call and
limits concurrency to the thread pool. Here they are coroutines that talk to
the server through redis.asyncio:
Method |
Implementation |
|---|---|
|
native, one command each ( |
|
native, one |
|
native, one pipeline: |
|
native, one |
|
native, |
|
native, |
|
Django’s generic versions, built on the native ones above |
|
does nothing — see Pools |
The sync methods are Django’s, untouched: they use the sync redis-py client
as before, and write the same keys with the same serializer. Sync and async
code therefore share one cache — a value stored with cache.set() is found by
await cache.aget() and vice versa. Django’s own cache users (the cache
middleware, cache_page, the cache session backend) call the sync methods and
keep working unchanged.
Two small differences to the sync methods: aclear() returns None (as
Django’s base class declares) rather than the server’s reply, and
adelete_many() of an empty generator does nothing instead of sending an
empty DEL.
ASYNC_OPTIONS¶
OPTIONS apply to the async connection pools, too. A few redis-py options,
though, name classes that come in a sync and an async variant, and the sync
one does not work on the async side. ASYNC_OPTIONS, next to OPTIONS,
overrides keys for the async pools only:
from redis.asyncio.retry import Retry as AsyncRetry
from redis.backoff import ExponentialBackoff
from redis.retry import Retry
CACHES = {
"default": {
"BACKEND": "action0.django_acache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"pool_class": "redis.BlockingConnectionPool",
"max_connections": 50,
"retry": Retry(ExponentialBackoff(), 3),
},
"ASYNC_OPTIONS": {
"pool_class": "redis.asyncio.BlockingConnectionPool",
"retry": AsyncRetry(ExponentialBackoff(), 3),
},
},
}
Option |
Sync side ( |
Async side |
|---|---|---|
|
default |
default |
|
default redis-py’s sync parser |
default redis-py’s async parser, never the sync one |
|
as given |
as in |
|
as given |
always the same as the sync side; not allowed in |
everything else |
as given |
as in |
As in OPTIONS, pool_class and parser_class may be dotted import paths.
A sync class or object that would reach the async pools, whether inherited from
OPTIONS or given in ASYNC_OPTIONS, raises ImproperlyConfigured as soon as
the cache is first used — by a sync method, too. Some of them would otherwise
only fail on the first async call; a sync Retry would even be accepted
silently and simply never retry. The message names the fix:
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
Pools¶
A redis.asyncio connection belongs to the event loop it was opened
on. And under ASGI, Django usually creates a new cache instance for every
request (its cache handler keeps instances per async context). Connection
pools kept on the cache instance would mean a new connection to the server for
every request.
So the async pools are kept in one process-wide registry instead, per event loop and per configuration:
A pool is created on a loop’s first async cache call, for one server URL, pool class and set of options. Its connections are opened on demand.
All cache instances with equal settings share it — whichever request created it.
max_connectionstherefore limits the connections of the whole loop, not of one request.Every loop gets pools of its own: each
asyncio.run(), each thread running a loop, eachasync_to_sync()call made outside an event loop.A loop’s pools are closed when the loop shuts down.
asyncio.run()andasyncio.Runner(which ASGI servers like uvicorn use), asgiref’sasync_to_syncandunittest.IsolatedAsyncioTestCaseall shut down a loop’s async generators before closing it, and that is the moment the registry uses.Django calls every cache’s
close()after each request. That does not touch the async pools, and neither doesaclose(): other requests are still using them.
To close the running loop’s pools earlier, e.g. at the end of a test that
checks for leaked connections, call
aclose_pools(). A later cache call simply
creates new pools.
from action0.django_acache import aclose_pools
await aclose_pools()
A loop that is closed without shutting down its async generators (by calling
loop.close() yourself) cannot have its pools closed: redis-py reports the
open connections with a ResourceWarning, and the registry drops them the next
time a new loop asks for a pool.
Testing with fakeredis¶
fakeredis can stand in for the server
in your test settings. Give the sync and the async side the connection class
for their side, and the same FakeServer, so that both see the same data:
import fakeredis
CACHES = {
"default": {
"BACKEND": "action0.django_acache.RedisCache",
"LOCATION": "redis://localhost:6379/0",
"OPTIONS": {
"connection_class": fakeredis.FakeRedisConnection,
"server": fakeredis.FakeServer(),
},
"ASYNC_OPTIONS": {
"connection_class": fakeredis.FakeAsyncRedisConnection,
},
},
}
This library’s own tests run exactly like that, and in CI additionally against real Redis and Valkey servers.