Skip to content

Commit 4209a8c

Browse files
etjCopilot
andauthored
[Fixes #14097] Label thesaurus is not reloaded on thesaurus changes (#14098)
* [Fixes #14097] Label thesaurus is not reloaded on thesaurus changes * Fix thread safety: guard set(), clear(), force_check() with _lock Agent-Logs-Url: https://github.com/GeoNode/geonode/sessions/26a81ebe-aef6-4850-b356-885f6bdce0dc Co-authored-by: etj <717359+etj@users.noreply.github.com> * Add test for stale cache invalidation on thesaurus update Agent-Logs-Url: https://github.com/GeoNode/geonode/sessions/cbf02dc8-5fb7-402a-9ab5-9f58f1a5378e Co-authored-by: etj <717359+etj@users.noreply.github.com> * Apply black formatting to test_i18n.py Agent-Logs-Url: https://github.com/GeoNode/geonode/sessions/73015399-e501-43f4-8387-7807b3257567 Co-authored-by: etj <717359+etj@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: etj <717359+etj@users.noreply.github.com>
1 parent e7a2e66 commit 4209a8c

2 files changed

Lines changed: 58 additions & 17 deletions

File tree

geonode/base/i18n.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,8 @@ def get_entry(self, lang, data_key):
115115
.first()
116116
)
117117
if cached_entry and cached_entry.date != thesaurus_date:
118-
logger.info(f"Cache for {lang}:{data_key} needs to be recreated")
118+
logger.info(f"Cache for {lang}:{data_key} dirty, clearing all caches")
119+
self.lang_cache.clear()
119120
return thesaurus_date, None
120121
if not cached_entry:
121122
logger.info(f"Cache for {lang}:{data_key} needs to be created")
@@ -126,31 +127,35 @@ def get_entry(self, lang, data_key):
126127

127128
def set(self, lang: str, data_key: str, data, request_date: str):
128129
# TODO: check if lang is allowed
129-
cached_entry: I18nCacheEntry = self.lang_cache.setdefault(lang, I18nCacheEntry())
130-
130+
# Perform DB query outside the lock to avoid holding it during I/O
131131
latest_date = (
132132
Thesaurus.objects.filter(identifier=I18N_THESAURUS_IDENTIFIER).values_list("date", flat=True).first()
133133
)
134134

135-
if request_date == latest_date:
136-
# no changes after processing, set the info right away
137-
logger.debug(f"Caching lang:{lang} key:{data_key} date:{request_date}")
138-
cached_entry.date = latest_date
139-
cached_entry.caches[data_key] = data
140-
return True
141-
else:
142-
logger.warning(
143-
f"Cache will not be updated for lang:{lang} key:{data_key} reqdate:{request_date} latest:{latest_date}"
144-
)
145-
return False
135+
with self._lock:
136+
cached_entry: I18nCacheEntry = self.lang_cache.setdefault(lang, I18nCacheEntry())
137+
138+
if request_date == latest_date:
139+
# no changes after processing, set the info right away
140+
logger.debug(f"Caching lang:{lang} key:{data_key} date:{request_date}")
141+
cached_entry.date = latest_date
142+
cached_entry.caches[data_key] = data
143+
return True
144+
else:
145+
logger.warning(
146+
f"Cache will not be updated for lang:{lang} key:{data_key} reqdate:{request_date} latest:{latest_date}"
147+
)
148+
return False
146149

147150
def clear(self):
148151
logger.info("Clearing i18n cache")
149-
self.lang_cache.clear()
152+
with self._lock:
153+
self.lang_cache.clear()
150154

151155
def force_check(self):
152156
"""For testing: forces a check against the DB on the next get_entry call."""
153-
self._last_check = 0
157+
with self._lock:
158+
self._last_check = 0
154159

155160

156161
class LabelResolver:

geonode/metadata/tests/test_i18n.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
from geonode.metadata.handlers.sparse import SparseHandler, SparseFieldRegistry
2424
from geonode.metadata.manager import MetadataManager
2525

26-
from geonode.base.i18n import I18N_THESAURUS_IDENTIFIER, i18nCache
26+
from geonode.base.i18n import I18N_THESAURUS_IDENTIFIER, i18nCache, labelResolver
2727
from geonode.base.models import (
2828
ThesaurusKeyword,
2929
ThesaurusKeywordLabel,
@@ -147,3 +147,39 @@ def test_schema_i18n_title_defined(self):
147147
self._add_label("field1__ovr", "en", "f1_ovr_en")
148148
schema = self.mm.build_schema(lang="en")
149149
self.assertEqual("f1_ovr_en", schema["properties"]["field1"]["title"])
150+
151+
def test_stale_cache_invalidated_on_thesaurus_update(self):
152+
"""
153+
Ensure that all language caches (en and it) are invalidated when the Thesaurus date
154+
changes, so that stale values are never served after a thesaurus update.
155+
"""
156+
# Populate labels for two languages and warm up the cache
157+
self._add_label("key1", "en", "key1_en_v1")
158+
self._add_label("key1", "it", "key1_it_v1")
159+
160+
labels_en = labelResolver.get_labels("en")
161+
labels_it = labelResolver.get_labels("it")
162+
163+
self.assertEqual("key1_en_v1", labels_en.get("key1"))
164+
self.assertEqual("key1_it_v1", labels_it.get("key1"))
165+
166+
# Update label values in the DB to simulate a thesaurus edit
167+
ThesaurusKeywordLabel.objects.filter(keyword__thesaurus_id=self.tid, keyword__about="key1", lang="en").update(
168+
label="key1_en_v2"
169+
)
170+
ThesaurusKeywordLabel.objects.filter(keyword__thesaurus_id=self.tid, keyword__about="key1", lang="it").update(
171+
label="key1_it_v2"
172+
)
173+
174+
# Simulate a thesaurus date bump (what happens when the thesaurus is updated)
175+
Thesaurus.objects.filter(id=self.tid).update(date="2024-01-01")
176+
177+
# Force cache freshness check to bypass CHECK_INTERVAL
178+
i18nCache.force_check()
179+
180+
# Both language caches must be rebuilt with the new values
181+
labels_en = labelResolver.get_labels("en")
182+
labels_it = labelResolver.get_labels("it")
183+
184+
self.assertEqual("key1_en_v2", labels_en.get("key1"))
185+
self.assertEqual("key1_it_v2", labels_it.get("key1"))

0 commit comments

Comments
 (0)