diff --git a/src/c3nav/mapdata/middleware.py b/src/c3nav/mapdata/middleware.py index 9c0bf469..a827dfaa 100644 --- a/src/c3nav/mapdata/middleware.py +++ b/src/c3nav/mapdata/middleware.py @@ -1,6 +1,7 @@ import re from functools import wraps +from c3nav.mapdata.utils.cache.local import per_request_cache from c3nav.mapdata.utils.user import get_user_data_lazy @@ -55,3 +56,15 @@ class UserDataMiddleware: def __call__(self, request): request.user_data = get_user_data_lazy(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) diff --git a/src/c3nav/mapdata/models/access.py b/src/c3nav/mapdata/models/access.py index 3bdec1e9..be9ddb5d 100644 --- a/src/c3nav/mapdata/models/access.py +++ b/src/c3nav/mapdata/models/access.py @@ -14,6 +14,7 @@ from django.utils.translation import ngettext_lazy from c3nav.mapdata.models import MapUpdate from c3nav.mapdata.models.base import SerializableMixin, TitledMixin +from c3nav.mapdata.utils.cache.local import per_request_cache class AccessRestriction(TitledMixin, models.Model): @@ -38,20 +39,20 @@ class AccessRestriction(TitledMixin, models.Model): @staticmethod def get_all() -> set[int]: 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: 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 @staticmethod def get_all_public() -> set[int]: 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: access_restriction_ids = set(AccessRestriction.objects.filter(public=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 @@ -321,14 +322,14 @@ class AccessPermission(models.Model): return AccessRestriction.get_all() 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: permissions = cls.get_for_request_with_expire_date(request, can_grant=can_grant) access_restriction_ids = set(permissions.keys()) 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()) @classmethod diff --git a/src/c3nav/mapdata/models/locations.py b/src/c3nav/mapdata/models/locations.py index 465e5f25..d109c082 100644 --- a/src/c3nav/mapdata/models/locations.py +++ b/src/c3nav/mapdata/models/locations.py @@ -23,6 +23,7 @@ from c3nav.mapdata.fields import I18nField from c3nav.mapdata.grid import grid from c3nav.mapdata.models.access import AccessRestrictionMixin 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.models import get_submodels @@ -620,10 +621,10 @@ class Position(CustomLocationProxyMixin, models.Model): if not user.is_authenticated: return False 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: result = cls.objects.filter(owner=user).exists() - cache.set(cache_key, result, 600) + per_request_cache.set(cache_key, result, 600) return result def serialize_position(self, request=None): diff --git a/src/c3nav/mapdata/models/update.py b/src/c3nav/mapdata/models/update.py index a847b014..4212a9b2 100644 --- a/src/c3nav/mapdata/models/update.py +++ b/src/c3nav/mapdata/models/update.py @@ -16,6 +16,7 @@ from shapely.ops import unary_union from c3nav.mapdata.tasks import process_map_updates from c3nav.mapdata.utils.cache.changes import GeometryChangeTracker +from c3nav.mapdata.utils.cache.local import per_request_cache class MapUpdate(models.Model): @@ -48,47 +49,47 @@ class MapUpdate(models.Model): @classmethod def last_update(cls, force=False): 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: return last_update try: with cls.lock(): 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: 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 @classmethod def last_processed_update(cls, force=False, lock=True): 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: return last_processed_update try: with (cls.lock() if lock else nullcontext()): 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: 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 @classmethod def last_processed_geometry_update(cls, force=False): 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: return last_processed_geometry_update try: with cls.lock(): last_processed_geometry_update = cls.objects.filter(processed=True, 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: 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 @property @@ -239,7 +240,8 @@ class MapUpdate(models.Model): LevelRenderData.rebuild(geometry_update_cache_key) 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: logger.info('No geometries affected.') @@ -282,7 +284,7 @@ class MapUpdate(models.Model): if new: 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: transaction.on_commit( diff --git a/src/c3nav/mapdata/utils/cache/local.py b/src/c3nav/mapdata/utils/cache/local.py index 98be8fbf..9c1a6271 100644 --- a/src/c3nav/mapdata/utils/cache/local.py +++ b/src/c3nav/mapdata/utils/cache/local.py @@ -1,8 +1,7 @@ from collections import OrderedDict from django.core.cache import cache - -from c3nav.mapdata.models import MapUpdate +from django.conf import settings class NoneFromCache: @@ -42,6 +41,9 @@ class LocalCacheProxy: self._items.pop(next(iter(self._items.keys()))) 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() if self._mapupdate != mapupdate: self._items = OrderedDict() @@ -52,3 +54,18 @@ class LocalCacheProxy: cache.set(key, value, expire) self._items[key] = value 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) \ No newline at end of file diff --git a/src/c3nav/settings.py b/src/c3nav/settings.py index c458ba39..4724f18e 100644 --- a/src/c3nav/settings.py +++ b/src/c3nav/settings.py @@ -390,6 +390,7 @@ MIDDLEWARE = [ 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware', + 'c3nav.mapdata.middleware.RequestCacheMiddleware', 'c3nav.mapdata.middleware.UserDataMiddleware', 'c3nav.site.middleware.MobileclientMiddleware', 'c3nav.control.middleware.UserPermissionsMiddleware',