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
This commit is contained in:
Jake Howard 2022-09-29 22:59:23 +01:00
parent d1523a886b
commit 7c008c2149
Signed by: jake
GPG key ID: 57AFB45680EDD477
2 changed files with 64 additions and 2 deletions

View file

@ -25,6 +25,7 @@ from wagtail.snippets.models import register_snippet
from wagtailmetadata.models import MetadataMixin from wagtailmetadata.models import MetadataMixin
from website.contrib.unsplash.widgets import UnsplashPhotoChooser from website.contrib.unsplash.widgets import UnsplashPhotoChooser
from website.utils.cache import cached_model_property
from .serializers import PaginationSerializer from .serializers import PaginationSerializer
from .streamfield import add_heading_anchors, get_blocks, get_content_html from .streamfield import add_heading_anchors, get_blocks, get_content_html
@ -127,11 +128,11 @@ class BaseContentPage(BasePage, MetadataMixin):
def _body_html(self) -> str: def _body_html(self) -> str:
return str(self.body) return str(self.body)
@cached_property @cached_model_property
def content_html(self) -> str: def content_html(self) -> str:
return get_content_html(self._body_html) return get_content_html(self._body_html)
@cached_property @cached_model_property
def plain_text(self) -> str: def plain_text(self) -> str:
return extract_text(self.content_html) return extract_text(self.content_html)

61
website/utils/cache.py Normal file
View file

@ -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)