From ff78ef8f6101bf02e39ce36e2b8c411f64e50f75 Mon Sep 17 00:00:00 2001 From: pablohashescobar Date: Fri, 16 Feb 2024 14:00:38 +0530 Subject: [PATCH] dev: mini cache framework and caching for users and instance configuration --- apiserver/plane/app/views/config.py | 3 + apiserver/plane/app/views/user.py | 20 +++- apiserver/plane/license/api/views/instance.py | 9 ++ apiserver/plane/utils/cache.py | 93 +++++++++++++++++++ 4 files changed, 120 insertions(+), 5 deletions(-) create mode 100644 apiserver/plane/utils/cache.py diff --git a/apiserver/plane/app/views/config.py b/apiserver/plane/app/views/config.py index 29b4bbf8b..95ef540d6 100644 --- a/apiserver/plane/app/views/config.py +++ b/apiserver/plane/app/views/config.py @@ -12,6 +12,7 @@ from rest_framework.response import Response # Module imports from .base import BaseAPIView from plane.license.utils.instance_value import get_configuration_value +from ...utils.cache import cache_path_response class ConfigurationEndpoint(BaseAPIView): @@ -19,6 +20,7 @@ class ConfigurationEndpoint(BaseAPIView): AllowAny, ] + @cache_path_response(60 * 60 * 2) def get(self, request): # Get all the configuration ( @@ -136,6 +138,7 @@ class MobileConfigurationEndpoint(BaseAPIView): AllowAny, ] + @cache_path_response(60 * 60 * 2) def get(self, request): ( GOOGLE_CLIENT_ID, diff --git a/apiserver/plane/app/views/user.py b/apiserver/plane/app/views/user.py index 7764e3b97..12b78bce7 100644 --- a/apiserver/plane/app/views/user.py +++ b/apiserver/plane/app/views/user.py @@ -1,8 +1,9 @@ +# Django imports +from django.db.models import Q, F, Count, Case, When, IntegerField # Third party imports from rest_framework.response import Response from rest_framework import status - # Module imports from plane.app.serializers import ( UserSerializer, @@ -15,10 +16,7 @@ from plane.app.views.base import BaseViewSet, BaseAPIView from plane.db.models import User, IssueActivity, WorkspaceMember, ProjectMember from plane.license.models import Instance, InstanceAdmin from plane.utils.paginator import BasePaginator - - -from django.db.models import Q, F, Count, Case, When, IntegerField - +from ...utils.cache import cache_user_response, invalidate_user_cache class UserEndpoint(BaseViewSet): serializer_class = UserSerializer @@ -27,6 +25,7 @@ class UserEndpoint(BaseViewSet): def get_object(self): return self.request.user + @cache_user_response(60*15) def retrieve(self, request): serialized_data = UserMeSerializer(request.user).data return Response( @@ -34,10 +33,12 @@ class UserEndpoint(BaseViewSet): status=status.HTTP_200_OK, ) + @cache_user_response(60*15) def retrieve_user_settings(self, request): serialized_data = UserMeSettingsSerializer(request.user).data return Response(serialized_data, status=status.HTTP_200_OK) + @cache_user_response(60*15) def retrieve_instance_admin(self, request): instance = Instance.objects.first() is_admin = InstanceAdmin.objects.filter( @@ -47,6 +48,11 @@ class UserEndpoint(BaseViewSet): {"is_instance_admin": is_admin}, status=status.HTTP_200_OK ) + @invalidate_user_cache("/api/users/me/") + def partial_update(self, request, *args, **kwargs): + return super().partial_update(request, *args, **kwargs) + + @invalidate_user_cache("/api/users/me/") def deactivate(self, request): # Check all workspace user is active user = self.get_object() @@ -145,6 +151,8 @@ class UserEndpoint(BaseViewSet): class UpdateUserOnBoardedEndpoint(BaseAPIView): + + @invalidate_user_cache("/api/users/me") def patch(self, request): user = User.objects.get(pk=request.user.id, is_active=True) user.is_onboarded = request.data.get("is_onboarded", False) @@ -155,6 +163,8 @@ class UpdateUserOnBoardedEndpoint(BaseAPIView): class UpdateUserTourCompletedEndpoint(BaseAPIView): + + @invalidate_user_cache("/api/users/me") def patch(self, request): user = User.objects.get(pk=request.user.id, is_active=True) user.is_tour_completed = request.data.get("is_tour_completed", False) diff --git a/apiserver/plane/license/api/views/instance.py b/apiserver/plane/license/api/views/instance.py index 112c68bc8..8cb3d9163 100644 --- a/apiserver/plane/license/api/views/instance.py +++ b/apiserver/plane/license/api/views/instance.py @@ -32,6 +32,7 @@ from plane.license.api.permissions import ( ) from plane.db.models import User, WorkspaceMember, ProjectMember from plane.license.utils.encryption import encrypt_data +from plane.utils.cache import cache_path_response, invalidate_path_cache class InstanceEndpoint(BaseAPIView): @@ -44,6 +45,7 @@ class InstanceEndpoint(BaseAPIView): AllowAny(), ] + @cache_path_response(60 * 60 * 2) def get(self, request): instance = Instance.objects.first() # get the instance @@ -58,6 +60,7 @@ class InstanceEndpoint(BaseAPIView): data["is_activated"] = True return Response(data, status=status.HTTP_200_OK) + @invalidate_path_cache def patch(self, request): # Get the instance instance = Instance.objects.first() @@ -104,6 +107,7 @@ class InstanceAdminEndpoint(BaseAPIView): serializer = InstanceAdminSerializer(instance_admin) return Response(serializer.data, status=status.HTTP_201_CREATED) + @invalidate_path_cache("/api/instances/") def get(self, request): instance = Instance.objects.first() if instance is None: @@ -115,6 +119,7 @@ class InstanceAdminEndpoint(BaseAPIView): serializer = InstanceAdminSerializer(instance_admins, many=True) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_path_cache("/api/instances/") def delete(self, request, pk): instance = Instance.objects.first() instance_admin = InstanceAdmin.objects.filter( @@ -135,6 +140,8 @@ class InstanceConfigurationEndpoint(BaseAPIView): ) return Response(serializer.data, status=status.HTTP_200_OK) + @invalidate_path_cache("/api/configs/") + @invalidate_path_cache("/api/mobile-configs/") def patch(self, request): configurations = InstanceConfiguration.objects.filter( key__in=request.data.keys() @@ -170,6 +177,7 @@ class InstanceAdminSignInEndpoint(BaseAPIView): AllowAny, ] + @invalidate_path_cache("/api/instances/") def post(self, request): # Check instance first instance = Instance.objects.first() @@ -260,6 +268,7 @@ class SignUpScreenVisitedEndpoint(BaseAPIView): AllowAny, ] + @invalidate_path_cache("/api/instances/") def post(self, request): instance = Instance.objects.first() if instance is None: diff --git a/apiserver/plane/utils/cache.py b/apiserver/plane/utils/cache.py new file mode 100644 index 000000000..134a039c0 --- /dev/null +++ b/apiserver/plane/utils/cache.py @@ -0,0 +1,93 @@ +from django.core.cache import cache +from django.utils.encoding import force_bytes +import hashlib +from functools import wraps +from rest_framework.response import Response +from datetime import datetime, timedelta +from django.utils.http import http_date + + +def generate_cache_key(custom_path, auth_header=None): + if auth_header: + key_data = f'{custom_path}:{auth_header}' + else: + key_data = custom_path + return hashlib.md5(force_bytes(key_data)).hexdigest() + +def cache_user_response(timeout, path=None): + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Function to generate cache key + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + custom_path = path if path is not None else request.get_full_path() + key = generate_cache_key(custom_path, auth_header) + cached_result = cache.get(key) + if cached_result is not None: + return Response(cached_result['data'], status=cached_result['status']) + + response = view_func(instance, request, *args, **kwargs) + + if response.status_code == 200: + cache.set(key, {'data': response.data, 'status': response.status_code}, timeout) + response['Cache-Control'] = f'max-age={timeout}' + expires_time = datetime.utcnow() + timedelta(seconds=timeout) + response['Expires'] = http_date(expires_time.timestamp()) + + return response + return _wrapped_view + return decorator + +def invalidate_user_cache(path): + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Invalidate cache before executing the view function + custom_path = path if path is not None else request.get_full_path() + auth_header = request.META.get('HTTP_AUTHORIZATION', '') + key = generate_cache_key(custom_path, auth_header) + cache.delete(key) + + # Execute the view function + return view_func(instance, request, *args, **kwargs) + return _wrapped_view + return decorator + + +def cache_path_response(timeout, path=None): + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Function to generate cache key + custom_path = path if path is not None else request.get_full_path() + key = generate_cache_key(custom_path, None) + cached_result = cache.get(key) + if cached_result is not None: + return Response(cached_result['data'], status=cached_result['status']) + + response = view_func(instance, request, *args, **kwargs) + + if response.status_code == 200: + cache.set(key, {'data': response.data, 'status': response.status_code}, timeout) + response['Cache-Control'] = f'max-age={timeout}' + expires_time = datetime.utcnow() + timedelta(seconds=timeout) + response['Expires'] = http_date(expires_time.timestamp()) + + return response + return _wrapped_view + return decorator + +def invalidate_path_cache(path=None): + def decorator(view_func): + @wraps(view_func) + def _wrapped_view(instance, request, *args, **kwargs): + # Invalidate cache before executing the view function + custom_path = path if path is not None else request.get_full_path() + key = generate_cache_key(custom_path, None) + cache.delete(key) + + # Execute the view function + return view_func(instance, request, *args, **kwargs) + return _wrapped_view + return decorator +