From 7c008c2149f0f2cc067810ff88d6d6fa7c63ef2c Mon Sep 17 00:00:00 2001 From: Jake Howard Date: Thu, 29 Sep 2022 22:59:23 +0100 Subject: [PATCH] Add the ability to cache model attributes in redis This not only means they persist longer than the instance, but can also be shared between processes. This is especially useful for list pages, as rendering content for summaries etc is quite expensive --- website/common/models.py | 5 ++-- website/utils/cache.py | 61 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 2 deletions(-) create mode 100644 website/utils/cache.py diff --git a/website/common/models.py b/website/common/models.py index 0040360..9ee1e25 100644 --- a/website/common/models.py +++ b/website/common/models.py @@ -25,6 +25,7 @@ from wagtail.snippets.models import register_snippet from wagtailmetadata.models import MetadataMixin from website.contrib.unsplash.widgets import UnsplashPhotoChooser +from website.utils.cache import cached_model_property from .serializers import PaginationSerializer from .streamfield import add_heading_anchors, get_blocks, get_content_html @@ -127,11 +128,11 @@ class BaseContentPage(BasePage, MetadataMixin): def _body_html(self) -> str: return str(self.body) - @cached_property + @cached_model_property def content_html(self) -> str: return get_content_html(self._body_html) - @cached_property + @cached_model_property def plain_text(self) -> str: return extract_text(self.content_html) diff --git a/website/utils/cache.py b/website/utils/cache.py new file mode 100644 index 0000000..bb79dd1 --- /dev/null +++ b/website/utils/cache.py @@ -0,0 +1,61 @@ +import inspect +from functools import wraps +from typing import Callable, Type, TypeVar + +from django.core.cache import cache +from django.db.models import Model +from django.db.models.signals import post_save +from django.dispatch import receiver +from django.utils.functional import cached_property + +T = TypeVar("T") + + +def get_cache_key(instance: Model, method: Callable) -> str: + return f"page_{method.__name__}_{instance.pk}" + + +def get_cached_model_properties(model: Type[Model]) -> list[str]: + return [ + name + for name, _ in inspect.getmembers( + model, predicate=lambda p: hasattr(p, "__cached__") + ) + ] + + +def cached_model_property(f: Callable[[Model], T]) -> T: + @cached_property + @wraps(f) + def wrapped(self: Model) -> T: + cache_key = get_cache_key(self, f) + value = cache.get(cache_key) + + if value is None: + value = f(self) + # Cache for 1 week + cache.set(cache_key, value, 604800) + + return value + + wrapped.__cached__ = True + return wrapped + + +@receiver(post_save) +def clear_cached_model_properties( + sender: Type, instance: Model, **kwargs: dict +) -> None: + cached_model_properties = get_cached_model_properties(instance.__class__) + + if cached_model_properties: + cache.delete_many( + [ + get_cache_key(instance, getattr(instance.__class__, name).real_func) + for name in cached_model_properties + ] + ) + + # Prime caches again + for name in cached_model_properties: + getattr(instance, name)