forked from ungleich-public/dynamicweb
171 lines
6.8 KiB
Python
171 lines
6.8 KiB
Python
"""
|
|
This module manages placeholder caching. We use a cache-versioning strategy
|
|
in which each (placeholder x lang x site_id) manages its own version. The
|
|
actual cache includes additional keys appropriate for the placeholders
|
|
get_vary_cache_on().
|
|
|
|
Invalidation of a placeholder's cache simply increments the version number for
|
|
the (placeholder x lang) pair, which renders any cache entries for that
|
|
placeholder under that version inaccessible. Those cache entries will simply
|
|
expire and will be purged according to the policy of the cache backend in-use.
|
|
|
|
The cache entries themselves may include additional sub-keys, according to the
|
|
list of VARY header-names as returned by placeholder.get_vary_cache_on() and
|
|
the current HTTPRequest object.
|
|
|
|
The vary-on header-names are also stored with the version. This enables us to
|
|
check for cache hits without re-computing placeholder.get_vary_cache_on().
|
|
"""
|
|
import hashlib
|
|
import time
|
|
|
|
from django.utils.timezone import now
|
|
|
|
from cms.utils.conf import get_cms_setting
|
|
from cms.utils.helpers import get_header_name, get_timezone_name
|
|
|
|
|
|
def _get_placeholder_cache_version_key(placeholder, lang, site_id):
|
|
"""
|
|
Returns the version key for the given «placeholder», «lang» and «site_id».
|
|
|
|
Invalidating this (via clear_placeholder_cache by replacing the stored
|
|
value with a new value) will effectively make all "sub-caches" relating to
|
|
this (placeholder x lang) inaccessible. Sub-caches include caches per TZ
|
|
and per VARY header.
|
|
"""
|
|
prefix = get_cms_setting("CACHE_PREFIX")
|
|
key = f"{prefix}|placeholder_cache_version|id:{placeholder.pk}|lang:{str(lang)}|site:{site_id}"
|
|
# django itself adds "version" add the end of cache-keys, e.g. "<key>:1".
|
|
# -> If `cache.set()` is for example called with `version=""`, it still adds
|
|
# `:` at the end. So if we check the length for `> 250`, a length of 249
|
|
# or even 250 ends up in an InvalidCacheKey-exception.
|
|
# In order to avoid these errors, we hash the keys at a lower length to also
|
|
# have a little buffer.
|
|
if len(key) > 200:
|
|
key = "{prefix}|{hash}".format(
|
|
prefix=prefix,
|
|
hash=hashlib.sha1(key.encode("utf-8")).hexdigest(),
|
|
)
|
|
return key
|
|
|
|
|
|
def _get_placeholder_cache_version(placeholder, lang, site_id):
|
|
"""
|
|
Gets the (placeholder x lang)'s current version and vary-on header-names
|
|
list, if present, otherwise resets to («timestamp», []).
|
|
"""
|
|
from django.core.cache import cache
|
|
|
|
key = _get_placeholder_cache_version_key(placeholder, lang, site_id)
|
|
cached = cache.get(key)
|
|
if cached:
|
|
version, vary_on_list = cached
|
|
else:
|
|
version = int(time.time() * 1000000)
|
|
vary_on_list = []
|
|
_set_placeholder_cache_version(placeholder, lang, site_id, version, vary_on_list)
|
|
return version, vary_on_list
|
|
|
|
|
|
def _set_placeholder_cache_version(placeholder, lang, site_id, version, vary_on_list=None, duration=None):
|
|
"""
|
|
Sets the (placeholder x lang)'s version and vary-on header-names list.
|
|
"""
|
|
from django.core.cache import cache
|
|
|
|
key = _get_placeholder_cache_version_key(placeholder, lang, site_id)
|
|
|
|
if not version or version < 1:
|
|
version = int(time.time() * 1000000)
|
|
|
|
if vary_on_list is None:
|
|
vary_on_list = []
|
|
|
|
cache.set(key, (version, vary_on_list), duration)
|
|
|
|
|
|
def _get_placeholder_cache_key(placeholder, lang, site_id, request, soft=False):
|
|
"""
|
|
Returns the fully-addressed cache key for the given placeholder and
|
|
the request.
|
|
|
|
The kwarg «soft» should be set to True if getting the cache key to then
|
|
read from the cache. If instead the key retrieval is to support a cache
|
|
write, let «soft» be False.
|
|
"""
|
|
prefix = get_cms_setting("CACHE_PREFIX")
|
|
version, vary_on_list = _get_placeholder_cache_version(placeholder, lang, site_id)
|
|
tz = get_timezone_name()
|
|
main_key = f"{prefix}|render_placeholder|id:{placeholder.pk}|lang:{lang}|site:{site_id}|tz:{tz}|v:{version}"
|
|
|
|
if not soft:
|
|
# We are about to write to the cache, so we want to get the latest
|
|
# vary_cache_on headers and the correct cache expiration, ignoring any
|
|
# we already have. If the placeholder has already been rendered, this
|
|
# will be very efficient (zero-additional queries) due to the caching
|
|
# of all its plugins during the rendering process anyway.
|
|
vary_on_list = placeholder.get_vary_cache_on(request)
|
|
duration = placeholder.get_cache_expiration(request, now())
|
|
# Update the main placeholder cache version
|
|
_set_placeholder_cache_version(placeholder, lang, site_id, version, vary_on_list, duration)
|
|
|
|
sub_key_list = []
|
|
for key in vary_on_list:
|
|
value = request.META.get(get_header_name(key)) or "_"
|
|
sub_key_list.append(key + ":" + value)
|
|
|
|
cache_key = main_key
|
|
if sub_key_list:
|
|
cache_key += "|" + "|".join(sub_key_list)
|
|
|
|
# django itself adds "version" add the end of cache-keys, e.g. "<key>:1".
|
|
# -> If `cache.set()` is for example called with `version=""`, it still adds
|
|
# `:` at the end. So if we check the length for `> 250`, a length of 249
|
|
# or even 250 ends up in an InvalidCacheKey-exception.
|
|
# In order to avoid these errors, we hash the keys at a lower length to also
|
|
# have a little buffer.
|
|
if len(cache_key) > 200:
|
|
cache_key = "{prefix}|{hash}".format(
|
|
prefix=prefix,
|
|
hash=hashlib.sha1(cache_key.encode("utf-8")).hexdigest(),
|
|
)
|
|
|
|
return cache_key
|
|
|
|
|
|
def set_placeholder_cache(placeholder, lang, site_id, content, request):
|
|
"""
|
|
Sets the (correct) placeholder cache with the rendered placeholder.
|
|
"""
|
|
from django.core.cache import cache
|
|
|
|
key = _get_placeholder_cache_key(placeholder, lang, site_id, request)
|
|
|
|
duration = min(get_cms_setting("CACHE_DURATIONS")["content"], placeholder.get_cache_expiration(request, now()))
|
|
cache.set(key, content, duration)
|
|
# "touch" the cache-version, so that it stays as fresh as this content.
|
|
version, vary_on_list = _get_placeholder_cache_version(placeholder, lang, site_id)
|
|
_set_placeholder_cache_version(placeholder, lang, site_id, version, vary_on_list, duration=duration)
|
|
|
|
|
|
def get_placeholder_cache(placeholder, lang, site_id, request):
|
|
"""
|
|
Returns the placeholder from cache respecting the placeholder's
|
|
VARY headers.
|
|
"""
|
|
from django.core.cache import cache
|
|
|
|
key = _get_placeholder_cache_key(placeholder, lang, site_id, request, soft=True)
|
|
content = cache.get(key)
|
|
return content
|
|
|
|
|
|
def clear_placeholder_cache(placeholder, lang, site_id):
|
|
"""
|
|
Invalidates all existing cache entries for (placeholder x lang x site_id).
|
|
We don't need to re-store the vary_on_list, because the cache is now
|
|
effectively empty.
|
|
"""
|
|
version = int(time.time() * 1000000)
|
|
_set_placeholder_cache_version(placeholder, lang, site_id, version, [])
|