1+ import asyncio
12from threading import RLock
23from time import monotonic
34
@@ -19,16 +20,28 @@ def __init__(self):
1920 connection_cls = settings .REDIS_CONNECTION_CLASS
2021 if connection_cls is not None :
2122 self ._rd = utils .import_module_attr (connection_cls )()
23+ self ._ard = self ._rd
2224 else :
2325 try :
2426 import redis
2527 except ImportError :
2628 raise ImproperlyConfigured ("The Redis backend requires redis-py to be installed." ) from None
29+
2730 if isinstance (settings .REDIS_CONNECTION , str ):
2831 self ._rd = redis .from_url (settings .REDIS_CONNECTION )
2932 else :
3033 self ._rd = redis .Redis (** settings .REDIS_CONNECTION )
3134
35+ try :
36+ import redis .asyncio as aredis
37+
38+ if isinstance (settings .REDIS_CONNECTION , str ):
39+ self ._ard = aredis .from_url (settings .REDIS_CONNECTION )
40+ else :
41+ self ._ard = aredis .Redis (** settings .REDIS_CONNECTION )
42+ except ImportError :
43+ self ._ard = self ._rd
44+
3245 def add_prefix (self , key ):
3346 return f"{ self ._prefix } { key } "
3447
@@ -38,6 +51,15 @@ def get(self, key):
3851 return loads (value )
3952 return None
4053
54+ async def aget (self , key ):
55+ if hasattr (self ._ard , "aget" ):
56+ value = await self ._ard .aget (self .add_prefix (key ))
57+ else :
58+ value = await asyncio .to_thread (self ._rd .get , self .add_prefix (key ))
59+ if value :
60+ return loads (value )
61+ return None
62+
4163 def mget (self , keys ):
4264 if not keys :
4365 return
@@ -46,22 +68,49 @@ def mget(self, keys):
4668 if value :
4769 yield key , loads (value )
4870
71+ async def amget (self , keys ):
72+ if not keys :
73+ return {}
74+ prefixed_keys = [self .add_prefix (key ) for key in keys ]
75+ if hasattr (self ._ard , "amget" ):
76+ values = await self ._ard .amget (prefixed_keys )
77+ else :
78+ values = await asyncio .to_thread (self ._rd .mget , prefixed_keys )
79+ return {key : loads (value ) for key , value in zip (keys , values ) if value }
80+
4981 def set (self , key , value ):
5082 old_value = self .get (key )
5183 self ._rd .set (self .add_prefix (key ), dumps (value ))
5284 signals .config_updated .send (sender = config , key = key , old_value = old_value , new_value = value )
5385
86+ async def aset (self , key , value ):
87+ # We need the old value for the signal.
88+ # Signals are synchronous in Django, but we can't easily change that here.
89+ old_value = await self .aget (key )
90+ if hasattr (self ._ard , "aset" ):
91+ await self ._ard .aset (self .add_prefix (key ), dumps (value ))
92+ else :
93+ await asyncio .to_thread (self ._rd .set , self .add_prefix (key ), dumps (value ))
94+ signals .config_updated .send (sender = config , key = key , old_value = old_value , new_value = value )
95+
5496
5597class CachingRedisBackend (RedisBackend ):
5698 _sentinel = object ()
5799 _lock = RLock ()
100+ _async_lock = None # Lazy-initialized asyncio.Lock
58101
59102 def __init__ (self ):
60103 super ().__init__ ()
61104 self ._timeout = settings .REDIS_CACHE_TIMEOUT
62105 self ._cache = {}
63106 self ._sentinel = object ()
64107
108+ def _get_async_lock (self ):
109+ # Lazily create the asyncio lock to avoid issues with event loops
110+ if self ._async_lock is None :
111+ self ._async_lock = asyncio .Lock ()
112+ return self ._async_lock
113+
65114 def _has_expired (self , value ):
66115 return value [0 ] <= monotonic ()
67116
@@ -79,15 +128,70 @@ def get(self, key):
79128
80129 return value [1 ]
81130
131+ async def aget (self , key ):
132+ value = self ._cache .get (key , self ._sentinel )
133+
134+ if value is self ._sentinel or self ._has_expired (value ):
135+ async with self ._get_async_lock ():
136+ # Double-check after acquiring lock
137+ value = self ._cache .get (key , self ._sentinel )
138+ if value is self ._sentinel or self ._has_expired (value ):
139+ new_value = await super ().aget (key )
140+ self ._cache_value (key , new_value )
141+ return new_value
142+ return value [1 ]
143+
144+ return value [1 ]
145+
82146 def set (self , key , value ):
83147 with self ._lock :
84148 super ().set (key , value )
85149 self ._cache_value (key , value )
86150
151+ async def aset (self , key , value ):
152+ async with self ._get_async_lock ():
153+ await super ().aset (key , value )
154+ self ._cache_value (key , value )
155+
87156 def mget (self , keys ):
88157 if not keys :
89158 return
90159 for key in keys :
91160 value = self .get (key )
92161 if value is not None :
93162 yield key , value
163+
164+ async def amget (self , keys ):
165+ if not keys :
166+ return {}
167+
168+ results = {}
169+ missing_keys = []
170+
171+ # First, check the local cache for all keys
172+ for key in keys :
173+ value = self ._cache .get (key , self ._sentinel )
174+ if value is not self ._sentinel and not self ._has_expired (value ):
175+ results [key ] = value [1 ]
176+ else :
177+ missing_keys .append (key )
178+
179+ # Fetch missing keys from Redis
180+ if missing_keys :
181+ async with self ._get_async_lock ():
182+ # Re-check cache for keys that might have been fetched while waiting for lock
183+ still_missing = []
184+ for key in missing_keys :
185+ value = self ._cache .get (key , self ._sentinel )
186+ if value is not self ._sentinel and not self ._has_expired (value ):
187+ results [key ] = value [1 ]
188+ else :
189+ still_missing .append (key )
190+
191+ if still_missing :
192+ fetched = await super ().amget (still_missing )
193+ for key , value in fetched .items ():
194+ self ._cache_value (key , value )
195+ results [key ] = value
196+
197+ return results
0 commit comments