Async Cache, and a helper for porting sync code

I’ve been working on adding async support to the caching framework. It is still a work in progress, but I think I’m at a point where I can start getting more eyes on it.

Caching

The actual work to write an async-capable cache backend is actually pretty straightforward, and there are good libraries to make the cache calls asynchronously. The hard part here is API design, how do we provide users with a clean way to access the cache in both sync and async contexts?.

I have implemented a backend for memcached in two ways: one based on the current memcached implementation using sync_to_async and another based on aiocache (which also supports redis and others). This currently works as a proof-of-concept (the code still needs some cleanup) and allows you to call the appropriate sync or async function in the cache framework from either context.

In [1]: from django.core.cache import cache
In [2]: cache.set('from_sync', 1)
In [3]: await cache.set.as_async('from_async', 2)

In [4]: await cache.get.as_async('from_sync')
Out[4]: 1

In [5]: cache.get('from_async')
Out[5]: 2

In [6]: cache.get.as_sync('from_sync')
Out[6]: 1

In [7]: cache.get.as_async('from_sync')
Out[7]: <coroutine object SyncToAsync.__call__ at 0x1048a0950>

API Design

The suggested API design here is to provide an extension to the critical functions so that users can use .as_sync and .as_async on them which will provide a function (or method) that behaves the same way as the existing one, but that is adapted to the appropriate calling context.

Helper decorator

This is all implemented via a helper decorator (currently called auto_async) which by default wraps the original function in sync_to_asyc or async_to_sync as needed and that will allow for custom sync or async implementations to be injected.

Using the helper to make other parts of django async-capable

The idea behind the helper is to allow us to quickly make other parts of django async-capable as needed. The process would be something like this:

  • Wrap code in auto_async or async_unsafe as needed. This leaves us with a naive implementation that runs the existing code via sync_to_async
  • Provide an appropriate async version of the code we want to replace. The decorator should make this customization easy (WIP)
  • As the implementation of these functions evolve and gets properly tested, we can change the default behaviour to sync or async as needed, and provide deprecation or usage warnings when the functions get created or executed

The current code can be seen in this pull request: https://github.com/andrewgodwin/django/pull/6/files

We discussed this at DjangoCon and agreed that the better approach is

cache.get
cache.async.get
cache.sync.get

There was some worry about async being a reserved word, but it appears to work fine in our syntax testing. @nicolaslara is going to try making an implementation of this and see how it goes.

Turns out async is a reserved word… on 3.7. Suggestions for an alternate name have been:

  • cache.a.get
  • cache.as_async.get

We’re going to try a for now and see how it sits.