Skip to content

Commit f859a29

Browse files
committed
edits
1 parent 8747645 commit f859a29

File tree

4 files changed

+57
-61
lines changed

4 files changed

+57
-61
lines changed

django_mongodb_backend/cache.py

+28-26
Original file line numberDiff line numberDiff line change
@@ -59,19 +59,17 @@ def collection_for_write(self):
5959
db = router.db_for_write(self.cache_model_class)
6060
return connections[db].get_collection(self._collection_name)
6161

62-
def get(self, key, default=None, version=None):
63-
return self.get_many([key], version).get(key, default)
64-
6562
def _filter_expired(self, expired=False):
6663
"""
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.
6967
"""
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)
7573

7674
def get_many(self, keys, version=None):
7775
if not keys:
@@ -93,7 +91,7 @@ def set(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
9391
"$set": {
9492
"key": key,
9593
"value": self.serializer.dumps(value),
96-
"expires_at": self._get_expiration_time(timeout),
94+
"expires_at": self.get_backend_timeout(timeout),
9795
}
9896
},
9997
upsert=True,
@@ -111,7 +109,7 @@ def add(self, key, value, timeout=DEFAULT_TIMEOUT, version=None):
111109
"$set": {
112110
"key": key,
113111
"value": self.serializer.dumps(value),
114-
"expires_at": self._get_expiration_time(timeout),
112+
"expires_at": self.get_backend_timeout(timeout),
115113
}
116114
},
117115
upsert=True,
@@ -124,10 +122,13 @@ def _cull(self, num):
124122
if self._cull_frequency == 0:
125123
self.clear()
126124
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.
127128
keep_num = num - num // self._cull_frequency
128129
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.
131132
deleted_from = next(
132133
self.collection_for_write.aggregate(
133134
[
@@ -139,16 +140,19 @@ def _cull(self, num):
139140
)
140141
)
141142
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`.
143146
pass
144147
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.
148149
self.collection_for_write.delete_many(
149150
{
150151
"$or": [
152+
# Delete keys that expire before `deleted_from`...
151153
{"expires_at": {"$lt": deleted_from["expires_at"]}},
154+
# and the entries that share an expiration with
155+
# `deleted_from` but are alphabetically after it.
152156
{
153157
"$and": [
154158
{"expires_at": deleted_from["expires_at"]},
@@ -162,7 +166,7 @@ def _cull(self, num):
162166
def touch(self, key, timeout=DEFAULT_TIMEOUT, version=None):
163167
key = self.make_and_validate_key(key, version=version)
164168
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)}}
166170
)
167171
return res.matched_count > 0
168172

@@ -185,10 +189,10 @@ def incr(self, key, delta=1, version=None):
185189
raise ValueError(f"Key '{key}' not found.") from None
186190
return updated["value"]
187191

188-
def _get_expiration_time(self, timeout=None):
192+
def get_backend_timeout(self, timeout=DEFAULT_TIMEOUT):
189193
if timeout is None:
190194
return datetime.max
191-
timestamp = self.get_backend_timeout(timeout)
195+
timestamp = super().get_backend_timeout(timeout)
192196
return datetime.fromtimestamp(timestamp, tz=timezone.utc)
193197

194198
def delete(self, key, version=None):
@@ -205,12 +209,10 @@ def _delete_many(self, keys, version=None):
205209

206210
def has_key(self, key, version=None):
207211
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)}
213214
)
215+
return num > 0
214216

215217
def clear(self):
216218
self.collection_for_write.delete_many({})

django_mongodb_backend/management/commands/createcachecollection.py

-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212

1313
class Command(BaseCommand):
1414
help = "Creates the collections needed to use the MongoDB cache backend."
15-
1615
requires_system_checks = []
1716

1817
def add_arguments(self, parser):

docs/source/topics/known-issues.rst

+2-1
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,5 @@ Caching
102102
:djadmin:`createcachetable`.
103103

104104
Secondly, you must use the :class:`django_mongodb_backend.cache.MongoDBCache`
105-
backend rather than Django's built-in database cache backend.
105+
backend rather than Django's built-in database cache backend,
106+
``django.core.cache.backends.db.DatabaseCache``).

tests/cache_/tests.py

+27-33
Original file line numberDiff line numberDiff line change
@@ -92,12 +92,31 @@ def caches_setting_for_tests(base=None, exclude=None, **params):
9292
return setting
9393

9494

95-
class BaseCacheTests:
95+
@override_settings(
96+
CACHES=caches_setting_for_tests(
97+
BACKEND="django_mongodb_backend.cache.MongoDBCache",
98+
# Spaces are used in the name to ensure quoting/escaping works.
99+
LOCATION="test cache collection",
100+
),
101+
)
102+
@modify_settings(
103+
INSTALLED_APPS={"prepend": "django_mongodb_backend"},
104+
)
105+
class CacheTests(TestCase):
96106
factory = RequestFactory()
97107
incr_decr_type_error_msg = "Cannot apply %s() to a value of non-numeric type."
98108

99-
def tearDown(self):
100-
cache.clear()
109+
def setUp(self):
110+
# The super calls needs to happen first for the settings override.
111+
super().setUp()
112+
self.create_cache_collection()
113+
self.addCleanup(self.drop_collection)
114+
115+
def create_cache_collection(self):
116+
management.call_command("createcachecollection", verbosity=0)
117+
118+
def drop_collection(self):
119+
cache.collection_for_write.drop()
101120

102121
def test_simple(self):
103122
# Simple cache set/get works
@@ -930,34 +949,10 @@ def test_collection_has_indexes(self):
930949
)
931950
)
932951

933-
934-
@override_settings(
935-
CACHES=caches_setting_for_tests(
936-
BACKEND="django_mongodb_backend.cache.MongoDBCache",
937-
# Spaces are used in the table name to ensure quoting/escaping is working
938-
LOCATION="test cache table",
939-
),
940-
)
941-
@modify_settings(
942-
INSTALLED_APPS={"prepend": "django_mongodb_backend"},
943-
)
944-
class DBCacheTests(BaseCacheTests, TestCase):
945-
def setUp(self):
946-
# The super calls needs to happen first for the settings override.
947-
super().setUp()
948-
self.create_cache_collection()
949-
self.addCleanup(self.drop_collection)
950-
951-
def drop_collection(self):
952-
cache.collection_for_write.drop()
953-
954-
def create_cache_collection(self):
955-
management.call_command("createcachecollection", verbosity=0)
956-
957-
958-
@override_settings(USE_TZ=True)
959-
class DBCacheWithTimeZoneTests(DBCacheTests):
960-
pass
952+
def test_serializer_dumps(self):
953+
self.assertEqual(cache.serializer.dumps(123), 123)
954+
self.assertIsInstance(cache.serializer.dumps(True), bytes)
955+
self.assertIsInstance(cache.serializer.dumps("abc"), bytes)
961956

962957

963958
class DBCacheRouter:
@@ -1001,6 +996,5 @@ def test_createcachetable_observes_database_router(self):
1001996
# cache table should be created on 'other'
1002997
# Queries:
1003998
# 1: Create indexes
1004-
num = 1
1005-
with self.assertNumQueries(num, using="other"):
999+
with self.assertNumQueries(1, using="other"):
10061000
management.call_command("createcachecollection", database="other", verbosity=0)

0 commit comments

Comments
 (0)