@@ -59,19 +59,17 @@ def collection_for_write(self):
59
59
db = router .db_for_write (self .cache_model_class )
60
60
return connections [db ].get_collection (self ._collection_name )
61
61
62
- def get (self , key , default = None , version = None ):
63
- return self .get_many ([key ], version ).get (key , default )
64
-
65
62
def _filter_expired (self , expired = False ):
66
63
"""
67
- Create a filter to exclude expired data by default
68
- or include only expired data if `expired` is True.
64
+ Return MQL to exclude expired entries (needed because the MongoDB
65
+ daemon does not remove expired entries precisely when it expires).
66
+ If expired=True, return MQL to include only expired entries.
69
67
"""
70
- return (
71
- {"expires_at" : {"$lt" : datetime .utcnow ()}}
72
- if expired
73
- else { "expires_at" : { "$gte" : datetime . utcnow ()}}
74
- )
68
+ op = "$lt" if expired else "$gte"
69
+ return {"expires_at" : {op : datetime .utcnow ()}}
70
+
71
+ def get ( self , key , default = None , version = None ):
72
+ return self . get_many ([ key ], version ). get ( key , default )
75
73
76
74
def get_many (self , keys , version = None ):
77
75
if not keys :
@@ -93,7 +91,7 @@ def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
93
91
"$set" : {
94
92
"key" : key ,
95
93
"value" : self .serializer .dumps (value ),
96
- "expires_at" : self ._get_expiration_time (timeout ),
94
+ "expires_at" : self .get_backend_timeout (timeout ),
97
95
}
98
96
},
99
97
upsert = True ,
@@ -111,7 +109,7 @@ def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
111
109
"$set" : {
112
110
"key" : key ,
113
111
"value" : self .serializer .dumps (value ),
114
- "expires_at" : self ._get_expiration_time (timeout ),
112
+ "expires_at" : self .get_backend_timeout (timeout ),
115
113
}
116
114
},
117
115
upsert = True ,
@@ -124,10 +122,13 @@ def _cull(self, num):
124
122
if self ._cull_frequency == 0 :
125
123
self .clear ()
126
124
else :
125
+ # The fraction of entries that are culled when MAX_ENTRIES is
126
+ # reached is 1 / CULL_FREQUENCY. For example, in the default case
127
+ # of CULL_FREQUENCY=3, 2/3 of the entries are kept.
127
128
keep_num = num - num // self ._cull_frequency
128
129
try :
129
- # Find the first entry beyond the retention limit,
130
- # prioritizing earlier expiration dates .
130
+ # Find the first cache entry beyond the retention limit,
131
+ # culling entries that expire the soonest .
131
132
deleted_from = next (
132
133
self .collection_for_write .aggregate (
133
134
[
@@ -139,16 +140,19 @@ def _cull(self, num):
139
140
)
140
141
)
141
142
except StopIteration :
142
- # No entries found beyond the retention limit, nothing to delete.
143
+ # If no entries are found, there is nothing to delete. It may
144
+ # happen if the database removes expired entries between the
145
+ # query to get `num` and the query to get `deleted_from`.
143
146
pass
144
147
else :
145
- # Delete all entries with an earlier expiration date.
146
- # If multiple entries share the same expiration date,
147
- # delete those with a greater or equal key.
148
+ # Cull the cache.
148
149
self .collection_for_write .delete_many (
149
150
{
150
151
"$or" : [
152
+ # Delete keys that expire before `deleted_from`...
151
153
{"expires_at" : {"$lt" : deleted_from ["expires_at" ]}},
154
+ # and the entries that share an expiration with
155
+ # `deleted_from` but are alphabetically after it.
152
156
{
153
157
"$and" : [
154
158
{"expires_at" : deleted_from ["expires_at" ]},
@@ -162,7 +166,7 @@ def _cull(self, num):
162
166
def touch (self , key , timeout = DEFAULT_TIMEOUT , version = None ):
163
167
key = self .make_and_validate_key (key , version = version )
164
168
res = self .collection_for_write .update_one (
165
- {"key" : key }, {"$set" : {"expires_at" : self ._get_expiration_time (timeout )}}
169
+ {"key" : key }, {"$set" : {"expires_at" : self .get_backend_timeout (timeout )}}
166
170
)
167
171
return res .matched_count > 0
168
172
@@ -185,10 +189,10 @@ def incr(self, key, delta=1, version=None):
185
189
raise ValueError (f"Key '{ key } ' not found." ) from None
186
190
return updated ["value" ]
187
191
188
- def _get_expiration_time (self , timeout = None ):
192
+ def get_backend_timeout (self , timeout = DEFAULT_TIMEOUT ):
189
193
if timeout is None :
190
194
return datetime .max
191
- timestamp = self .get_backend_timeout (timeout )
195
+ timestamp = super () .get_backend_timeout (timeout )
192
196
return datetime .fromtimestamp (timestamp , tz = timezone .utc )
193
197
194
198
def delete (self , key , version = None ):
@@ -205,12 +209,10 @@ def _delete_many(self, keys, version=None):
205
209
206
210
def has_key (self , key , version = None ):
207
211
key = self .make_and_validate_key (key , version = version )
208
- return (
209
- self .collection_for_read .count_documents (
210
- {"key" : key , ** self ._filter_expired (expired = False )}
211
- )
212
- > 0
212
+ num = self .collection_for_read .count_documents (
213
+ {"key" : key , ** self ._filter_expired (expired = False )}
213
214
)
215
+ return num > 0
214
216
215
217
def clear (self ):
216
218
self .collection_for_write .delete_many ({})
0 commit comments