From a4046904e9754b666376c92895d4f5875e69addc Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sun, 14 Apr 2024 18:34:20 +1000 Subject: [PATCH 1/2] Refactor to only cache lb scores, not full page --- leaderboards/models.py | 27 +++++++++++++++++++++------ leaderboards/views.py | 31 ++++++++++++------------------- 2 files changed, 33 insertions(+), 25 deletions(-) diff --git a/leaderboards/models.py b/leaderboards/models.py index a7875c8..53cf0d5 100644 --- a/leaderboards/models.py +++ b/leaderboards/models.py @@ -91,6 +91,17 @@ def get_pp_record(self) -> typing.Union[Score, None]: return scores.aggregate(Max("sorting_pp"))["sorting_pp__max"] + def get_top_scores(self, limit=5): + scores = ( + Score.objects.non_restricted() + .distinct() + .filter(membership__leaderboard_id=self.id) + .select_related("user_stats", "user_stats__user", "beatmap") + .get_score_set(score_set=self.score_set) + ) + + return scores[:limit] + def get_top_membership(self): if self.access_type == LeaderboardAccessType.GLOBAL: memberships = Membership.global_memberships @@ -165,9 +176,11 @@ def update_membership(self, user_id): ) elif self.score_set == ScoreSet.NEVER_CHOKE: membership.pp = calculate_pp_total( - score.nochoke_performance_total - if score.result & ScoreResult.CHOKE - else score.performance_total + ( + score.nochoke_performance_total + if score.result & ScoreResult.CHOKE + else score.performance_total + ) for score in scores ) elif self.score_set == ScoreSet.ALWAYS_FULL_COMBO: @@ -321,9 +334,11 @@ def recalculate(self): ) elif self.leaderboard.score_set == ScoreSet.NEVER_CHOKE: self.pp = calculate_pp_total( - score.nochoke_performance_total - if score.result & ScoreResult.CHOKE - else score.performance_total + ( + score.nochoke_performance_total + if score.result & ScoreResult.CHOKE + else score.performance_total + ) for score in self.scores.order_by("-performance_total").all() ) elif self.leaderboard.score_set == ScoreSet.ALWAYS_FULL_COMBO: diff --git a/leaderboards/views.py b/leaderboards/views.py index 4ca1374..83f497d 100644 --- a/leaderboards/views.py +++ b/leaderboards/views.py @@ -1,8 +1,8 @@ from collections import OrderedDict +from django.core.cache import cache from django.core.exceptions import ValidationError from django.core.validators import URLValidator -from django.views.decorators.cache import cache_page from rest_framework import permissions, status from rest_framework.exceptions import NotFound, ParseError, PermissionDenied from rest_framework.response import Response @@ -265,7 +265,7 @@ class LeaderboardScoreList(APIView): permission_classes = (permissions.IsAuthenticatedOrReadOnly,) - def _get(self, request, leaderboard_type, gamemode, leaderboard_id): + def get(self, request, leaderboard_type, gamemode, leaderboard_id): osu_user_id = ( request.user.osu_user_id if request.user.is_authenticated else None ) @@ -284,24 +284,17 @@ def _get(self, request, leaderboard_type, gamemode, leaderboard_id): except Leaderboard.DoesNotExist: raise NotFound("Leaderboard not found.") - scores = ( - Score.objects.non_restricted() - .distinct() - .filter(membership__leaderboard_id=leaderboard_id) - .select_related("user_stats", "user_stats__user", "beatmap") - .order_by("-performance_total", "date") - .get_score_set(score_set=leaderboard.score_set) - ) - serialiser = LeaderboardScoreSerialiser(scores[:limit], many=True) - return Response(serialiser.data) - - # TODO: get rid of this cache by optimising - def get(self, request, leaderboard_type, gamemode, leaderboard_id): - if leaderboard_type == "global": - cached_page = cache_page(60 * 60)(self._get) - return cached_page(request, leaderboard_type, gamemode, leaderboard_id) + if leaderboard.access_type == LeaderboardAccessType.GLOBAL: + scores = cache.get_or_set( + f"leaderboards::global_leaderboard_top_5_scores::{leaderboard.id}", + lambda: leaderboard.get_top_scores(limit=limit), + 900, + ) else: - return self._get(request, leaderboard_type, gamemode, leaderboard_id) + scores = leaderboard.get_top_scores(limit=5) + + serialiser = LeaderboardScoreSerialiser(scores, many=True) + return Response(serialiser.data) class LeaderboardMemberList(APIView): From d8fba112728cf35d2eaef8f9d9099beee1ad2005 Mon Sep 17 00:00:00 2001 From: Samuel Cattini-Schultz Date: Sun, 14 Apr 2024 18:40:56 +1000 Subject: [PATCH 2/2] Add scheduled task to refresh lb score cache --- leaderboards/tasks.py | 24 +++++++++++++++++++++--- osuchan/settings.py | 4 ++++ 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/leaderboards/tasks.py b/leaderboards/tasks.py index 2b88a8a..7f777f8 100644 --- a/leaderboards/tasks.py +++ b/leaderboards/tasks.py @@ -2,6 +2,7 @@ from celery import shared_task from django.conf import settings +from django.core.cache import cache from django.db import transaction from common.discord_webhook_sender import DiscordWebhookSender @@ -51,9 +52,11 @@ def update_memberships(user_id, gamemode=Gamemode.STANDARD): ) elif leaderboard.score_set == ScoreSet.NEVER_CHOKE: membership.pp = calculate_pp_total( - score.nochoke_performance_total - if score.result & ScoreResult.CHOKE - else score.performance_total + ( + score.nochoke_performance_total + if score.result & ScoreResult.CHOKE + else score.performance_total + ) for score in scores ) elif leaderboard.score_set == ScoreSet.ALWAYS_FULL_COMBO: @@ -111,6 +114,21 @@ def send_notification( return memberships +@shared_task +def update_global_leaderboard_top_5_score_cache(): + for gamemode in Gamemode: + leaderboards = Leaderboard.objects.filter( + access_type=LeaderboardAccessType.GLOBAL, gamemode=gamemode + ) + for leaderboard in leaderboards: + scores = leaderboard.get_top_scores(limit=5) + cache.set( + f"leaderboards::global_leaderboard_top_5_scores::{leaderboard.id}", + scores, + 900, + ) + + @shared_task def send_leaderboard_top_score_notification(leaderboard_id: int, score_id: int): # passing score_id instead of querying for top score in case it changes before the job is picked up diff --git a/osuchan/settings.py b/osuchan/settings.py index 042357a..34460c6 100644 --- a/osuchan/settings.py +++ b/osuchan/settings.py @@ -200,6 +200,10 @@ class EnvSettings(BaseSettings): "task": "profiles.tasks.dispatch_update_all_global_leaderboard_top_members", "schedule": crontab(minute=0, hour=0), # midnight UTC }, + "update-global-leaderboard-top-5-score-cache-every-10-minutes": { + "task": "leaderboards.tasks.update_global_leaderboard_top_5_score_cache", + "schedule": crontab(minute="*/10"), + }, }