#36455: Cache backends delegate async methods to BaseCache implementations 
instead
of using the backend's native support for incr(), get_many(), etc..
-------------------------------------+-------------------------------------
     Reporter:  LaughInJar           |                     Type:  Bug
       Status:  new                  |                Component:  Core
                                     |  (Cache system)
      Version:  5.2                  |                 Severity:  Normal
     Keywords:  aincr adecr          |             Triage Stage:
                                     |  Unreviewed
    Has patch:  0                    |      Needs documentation:  0
  Needs tests:  0                    |  Patch needs improvement:  0
Easy pickings:  0                    |                    UI/UX:  0
-------------------------------------+-------------------------------------
 Both the Redis and Memcached backends do not override the async methods of
 the `BaseCache` base class. However the base class does not simply wrap
 all the sync methods using `sync_to_async` but instead recreate the
 functionality of methods like `incr()` or `get_many()` by using `aget()`
 and `aset()`.

 For example, the `aincr()` method in the BaseCache looks like this:

 {{{#!python
 async def aincr(self, key, delta=1, version=None):
     """See incr()."""
     value = await self.aget(key, self._missing_key, version=version)
     if value is self._missing_key:
         raise ValueError("Key '%s' not found" % key)
     new_value = value + delta
     await self.aset(key, new_value, version=version)
     return new_value
 }}}

 However, both Redis and Memcached have native support for incrementing a
 key the respective Django backends override the `BaseCache` 's sync
 methods to use these native functions. But since they do not override the
 async methods, they fall back to the implementation of `BaseCache` which
 uses `aget` and `aset`.

 This is not only a performance drawback since this requires additional
 calls, it also means that the calls to `aincr` are no longer atomic. The
 docs say that atomicity is not guaranteed but it it will be atomic when
 the backend supports it. Redis and Memcache support it.

 This is relevant when e.g. trying to implement a throttle for API calls.

 In order to reproduce, recreate a new virtualenv and install Django.

 {{{#!python
 import asyncio
 from django.core.cache.backends.redis import RedisCache

 cache = RedisCache("redis://localhost:6379/4", {})

 # uncomment this to 'fix' the issue
 # from asgiref.sync import sync_to_async
 # cache.aincr = sync_to_async(cache.incr)

 key = "counter"
 cache.add(key, 0, timeout=1)

 async def task() -> int:
     return await cache.aincr(key)

 async def run():
     tasks = [task() for _ in range(10)]
     return await asyncio.gather(*tasks)

 counters = sorted(asyncio.run(run()))

 print(counters)
 # outputs [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
 # the expected output should be
 # [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
 }}}

 Although I've focused on `aincr()` I can see from the code that other
 methods like `ahas_key`, `aset_many`, `aget_many`, `adelete_many`, and
 `aget_or_set` are similarily emulated using `aget` and `aset`. Redis and
 Memcache have native support for these functions which is utilized when
 calling the sync methods. The expectation would be that the async methods
 would work in the same manner as the sync methods.
-- 
Ticket URL: <https://code.djangoproject.com/ticket/36455>
Django <https://code.djangoproject.com/>
The Web framework for perfectionists with deadlines.

-- 
You received this message because you are subscribed to the Google Groups 
"Django updates" group.
To unsubscribe from this group and stop receiving emails from it, send an email 
to django-updates+unsubscr...@googlegroups.com.
To view this discussion visit 
https://groups.google.com/d/msgid/django-updates/010701975ed92c0f-a60ac1dd-18d1-4446-9485-af0f2aefcc8a-000000%40eu-central-1.amazonses.com.

Reply via email to