per_request_cache

This commit is contained in:
Laura Klünder 2024-12-27 18:50:36 +01:00
parent 06135cedfd
commit 87b7f00740
6 changed files with 56 additions and 21 deletions

View file

@ -1,6 +1,7 @@
import re import re
from functools import wraps from functools import wraps
from c3nav.mapdata.utils.cache.local import per_request_cache
from c3nav.mapdata.utils.user import get_user_data_lazy from c3nav.mapdata.utils.user import get_user_data_lazy
@ -55,3 +56,15 @@ class UserDataMiddleware:
def __call__(self, request): def __call__(self, request):
request.user_data = get_user_data_lazy(request) request.user_data = get_user_data_lazy(request)
return self.get_response(request) return self.get_response(request)
class RequestCacheMiddleware:
"""
Resets the request_cache at the start of every request.
"""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
per_request_cache.clear()
return self.get_response(request)

View file

@ -14,6 +14,7 @@ from django.utils.translation import ngettext_lazy
from c3nav.mapdata.models import MapUpdate from c3nav.mapdata.models import MapUpdate
from c3nav.mapdata.models.base import SerializableMixin, TitledMixin from c3nav.mapdata.models.base import SerializableMixin, TitledMixin
from c3nav.mapdata.utils.cache.local import per_request_cache
class AccessRestriction(TitledMixin, models.Model): class AccessRestriction(TitledMixin, models.Model):
@ -38,20 +39,20 @@ class AccessRestriction(TitledMixin, models.Model):
@staticmethod @staticmethod
def get_all() -> set[int]: def get_all() -> set[int]:
cache_key = 'all_access_restrictions:%s' % MapUpdate.current_cache_key() cache_key = 'all_access_restrictions:%s' % MapUpdate.current_cache_key()
access_restriction_ids = cache.get(cache_key, None) access_restriction_ids = per_request_cache.get(cache_key, None)
if access_restriction_ids is None: if access_restriction_ids is None:
access_restriction_ids = set(AccessRestriction.objects.values_list('pk', flat=True)) access_restriction_ids = set(AccessRestriction.objects.values_list('pk', flat=True))
cache.set(cache_key, access_restriction_ids, 300) per_request_cache.set(cache_key, access_restriction_ids, 300)
return access_restriction_ids return access_restriction_ids
@staticmethod @staticmethod
def get_all_public() -> set[int]: def get_all_public() -> set[int]:
cache_key = 'public_access_restrictions:%s' % MapUpdate.current_cache_key() cache_key = 'public_access_restrictions:%s' % MapUpdate.current_cache_key()
access_restriction_ids = cache.get(cache_key, None) access_restriction_ids = per_request_cache.get(cache_key, None)
if access_restriction_ids is None: if access_restriction_ids is None:
access_restriction_ids = set(AccessRestriction.objects.filter(public=True) access_restriction_ids = set(AccessRestriction.objects.filter(public=True)
.values_list('pk', flat=True)) .values_list('pk', flat=True))
cache.set(cache_key, access_restriction_ids, 300) per_request_cache.set(cache_key, access_restriction_ids, 300)
return access_restriction_ids return access_restriction_ids
@ -321,14 +322,14 @@ class AccessPermission(models.Model):
return AccessRestriction.get_all() return AccessRestriction.get_all()
cache_key = cls.request_access_permission_key(request)+f':{can_grant}' cache_key = cls.request_access_permission_key(request)+f':{can_grant}'
access_restriction_ids = cache.get(cache_key, None) access_restriction_ids = per_request_cache.get(cache_key, None)
if access_restriction_ids is None: if access_restriction_ids is None:
permissions = cls.get_for_request_with_expire_date(request, can_grant=can_grant) permissions = cls.get_for_request_with_expire_date(request, can_grant=can_grant)
access_restriction_ids = set(permissions.keys()) access_restriction_ids = set(permissions.keys())
expire_date = min((e for e in permissions.values() if e), default=timezone.now() + timedelta(seconds=120)) expire_date = min((e for e in permissions.values() if e), default=timezone.now() + timedelta(seconds=120))
cache.set(cache_key, access_restriction_ids, min(300, (expire_date - timezone.now()).total_seconds())) per_request_cache.set(cache_key, access_restriction_ids, min(300, (expire_date - timezone.now()).total_seconds()))
return set(access_restriction_ids) | (set() if can_grant else AccessRestriction.get_all_public()) return set(access_restriction_ids) | (set() if can_grant else AccessRestriction.get_all_public())
@classmethod @classmethod

View file

@ -23,6 +23,7 @@ from c3nav.mapdata.fields import I18nField
from c3nav.mapdata.grid import grid from c3nav.mapdata.grid import grid
from c3nav.mapdata.models.access import AccessRestrictionMixin from c3nav.mapdata.models.access import AccessRestrictionMixin
from c3nav.mapdata.models.base import SerializableMixin, TitledMixin from c3nav.mapdata.models.base import SerializableMixin, TitledMixin
from c3nav.mapdata.utils.cache.local import per_request_cache
from c3nav.mapdata.utils.fields import LocationById from c3nav.mapdata.utils.fields import LocationById
from c3nav.mapdata.utils.models import get_submodels from c3nav.mapdata.utils.models import get_submodels
@ -620,10 +621,10 @@ class Position(CustomLocationProxyMixin, models.Model):
if not user.is_authenticated: if not user.is_authenticated:
return False return False
cache_key = 'user_has_positions:%d' % user.pk cache_key = 'user_has_positions:%d' % user.pk
result = cache.get(cache_key, None) result = per_request_cache.get(cache_key, None)
if result is None: if result is None:
result = cls.objects.filter(owner=user).exists() result = cls.objects.filter(owner=user).exists()
cache.set(cache_key, result, 600) per_request_cache.set(cache_key, result, 600)
return result return result
def serialize_position(self, request=None): def serialize_position(self, request=None):

View file

@ -16,6 +16,7 @@ from shapely.ops import unary_union
from c3nav.mapdata.tasks import process_map_updates from c3nav.mapdata.tasks import process_map_updates
from c3nav.mapdata.utils.cache.changes import GeometryChangeTracker from c3nav.mapdata.utils.cache.changes import GeometryChangeTracker
from c3nav.mapdata.utils.cache.local import per_request_cache
class MapUpdate(models.Model): class MapUpdate(models.Model):
@ -48,47 +49,47 @@ class MapUpdate(models.Model):
@classmethod @classmethod
def last_update(cls, force=False): def last_update(cls, force=False):
if not force: if not force:
last_update = cache.get('mapdata:last_update', None) last_update = per_request_cache.get('mapdata:last_update', None)
if last_update is not None: if last_update is not None:
return last_update return last_update
try: try:
with cls.lock(): with cls.lock():
last_update = cls.objects.latest().to_tuple last_update = cls.objects.latest().to_tuple
cache.set('mapdata:last_update', last_update, None) per_request_cache.set('mapdata:last_update', last_update, None)
except cls.DoesNotExist: except cls.DoesNotExist:
last_update = (0, 0) last_update = (0, 0)
cache.set('mapdata:last_update', last_update, None) per_request_cache.set('mapdata:last_update', last_update, None)
return last_update return last_update
@classmethod @classmethod
def last_processed_update(cls, force=False, lock=True): def last_processed_update(cls, force=False, lock=True):
if not force: if not force:
last_processed_update = cache.get('mapdata:last_processed_update', None) last_processed_update = per_request_cache.get('mapdata:last_processed_update', None)
if last_processed_update is not None: if last_processed_update is not None:
return last_processed_update return last_processed_update
try: try:
with (cls.lock() if lock else nullcontext()): with (cls.lock() if lock else nullcontext()):
last_processed_update = cls.objects.filter(processed=True).latest().to_tuple last_processed_update = cls.objects.filter(processed=True).latest().to_tuple
cache.set('mapdata:last_processed_update', last_processed_update, None) per_request_cache.set('mapdata:last_processed_update', last_processed_update, None)
except cls.DoesNotExist: except cls.DoesNotExist:
last_processed_update = (0, 0) last_processed_update = (0, 0)
cache.set('mapdata:last_processed_update', last_processed_update, None) per_request_cache.set('mapdata:last_processed_update', last_processed_update, None)
return last_processed_update return last_processed_update
@classmethod @classmethod
def last_processed_geometry_update(cls, force=False): def last_processed_geometry_update(cls, force=False):
if not force: if not force:
last_processed_geometry_update = cache.get('mapdata:last_processed_geometry_update', None) last_processed_geometry_update = per_request_cache.get('mapdata:last_processed_geometry_update', None)
if last_processed_geometry_update is not None: if last_processed_geometry_update is not None:
return last_processed_geometry_update return last_processed_geometry_update
try: try:
with cls.lock(): with cls.lock():
last_processed_geometry_update = cls.objects.filter(processed=True, last_processed_geometry_update = cls.objects.filter(processed=True,
geometries_changed=True).latest().to_tuple geometries_changed=True).latest().to_tuple
cache.set('mapdata:last_processed_geometry_update', last_processed_geometry_update, None) per_request_cache.set('mapdata:last_processed_geometry_update', last_processed_geometry_update, None)
except cls.DoesNotExist: except cls.DoesNotExist:
last_processed_geometry_update = (0, 0) last_processed_geometry_update = (0, 0)
cache.set('mapdata:last_processed_geometry_update', last_processed_geometry_update, None) per_request_cache.set('mapdata:last_processed_geometry_update', last_processed_geometry_update, None)
return last_processed_geometry_update return last_processed_geometry_update
@property @property
@ -239,7 +240,8 @@ class MapUpdate(models.Model):
LevelRenderData.rebuild(geometry_update_cache_key) LevelRenderData.rebuild(geometry_update_cache_key)
transaction.on_commit( transaction.on_commit(
lambda: cache.set('mapdata:last_processed_geometry_update', last_geometry_update.to_tuple, None) lambda: per_request_cache.set('mapdata:last_processed_geometry_update',
last_geometry_update.to_tuple, None)
) )
else: else:
logger.info('No geometries affected.') logger.info('No geometries affected.')
@ -282,7 +284,7 @@ class MapUpdate(models.Model):
if new: if new:
transaction.on_commit( transaction.on_commit(
lambda: cache.set('mapdata:last_update', self.to_tuple, None) lambda: per_request_cache.set('mapdata:last_update', self.to_tuple, None)
) )
if settings.HAS_CELERY and settings.AUTO_PROCESS_UPDATES: if settings.HAS_CELERY and settings.AUTO_PROCESS_UPDATES:
transaction.on_commit( transaction.on_commit(

View file

@ -1,8 +1,7 @@
from collections import OrderedDict from collections import OrderedDict
from django.core.cache import cache from django.core.cache import cache
from django.conf import settings
from c3nav.mapdata.models import MapUpdate
class NoneFromCache: class NoneFromCache:
@ -42,6 +41,9 @@ class LocalCacheProxy:
self._items.pop(next(iter(self._items.keys()))) self._items.pop(next(iter(self._items.keys())))
def _check_mapupdate(self): def _check_mapupdate(self):
# todo: would be nice to not need this… why do we need this?
from c3nav.mapdata.models import MapUpdate
mapupdate = MapUpdate.current_cache_key() mapupdate = MapUpdate.current_cache_key()
if self._mapupdate != mapupdate: if self._mapupdate != mapupdate:
self._items = OrderedDict() self._items = OrderedDict()
@ -52,3 +54,18 @@ class LocalCacheProxy:
cache.set(key, value, expire) cache.set(key, value, expire)
self._items[key] = value self._items[key] = value
self._prune() self._prune()
def clear(self):
self._items.clear()
class RequestLocalCacheProxy(LocalCacheProxy):
""" this is a subclass without prune, to be cleared after every request """
def _prune(self):
pass
def _check_mapupdate(self):
pass
per_request_cache = RequestLocalCacheProxy(maxsize=settings.CACHE_SIZE_LOCATIONS)

View file

@ -390,6 +390,7 @@ MIDDLEWARE = [
'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
'c3nav.mapdata.middleware.RequestCacheMiddleware',
'c3nav.mapdata.middleware.UserDataMiddleware', 'c3nav.mapdata.middleware.UserDataMiddleware',
'c3nav.site.middleware.MobileclientMiddleware', 'c3nav.site.middleware.MobileclientMiddleware',
'c3nav.control.middleware.UserPermissionsMiddleware', 'c3nav.control.middleware.UserPermissionsMiddleware',