Skip to content

Commit

Permalink
feat: drop cache on course enrollments change
Browse files Browse the repository at this point in the history
  • Loading branch information
GlugovGrGlib committed Apr 23, 2024
1 parent d75fd6c commit 175eebe
Show file tree
Hide file tree
Showing 2 changed files with 43 additions and 5 deletions.
8 changes: 4 additions & 4 deletions lms/djangoapps/grades/rest_api/v1/gradebook_views.py
Original file line number Diff line number Diff line change
Expand Up @@ -670,7 +670,7 @@ def get(self, request, course_key): # lint-amnesty, pylint: disable=too-many-st
serializer = StudentGradebookEntrySerializer(entries, many=True)
return self.get_paginated_response(serializer.data, **users_counts)

def _get_user_count(self, query_args, cache_time=600, annotations=None):
def _get_user_count(self, course_key, query_args, cache_time=600, annotations=None):
"""
Return the user count for the given query arguments to CourseEnrollment.
Expand All @@ -681,7 +681,7 @@ def _get_user_count(self, query_args, cache_time=600, annotations=None):
queryset = queryset.annotate(**annotations)
queryset = queryset.filter(*query_args)

cache_key = 'usercount.%s' % queryset.query
cache_key = 'usercount.{course_key}.{queryset.query}'
user_count = cache.get(cache_key, None)
if user_count is None:
user_count = queryset.count()
Expand Down Expand Up @@ -710,15 +710,15 @@ def _get_users_counts(self, course_key, course_enrollment_filters, annotations=N
Q(course_id=course_key) & Q(is_active=True)
]

total_users_count = self._get_user_count(filter_args)
total_users_count = self._get_user_count(course_key, filter_args)

filter_args.extend(course_enrollment_filters or [])

# if course_enrollment_filters is empty, then the number of filtered users will equal the total number of users
filtered_users_count = (
total_users_count
if not course_enrollment_filters
else self._get_user_count(filter_args, annotations=annotations)
else self._get_user_count(course_key, filter_args, annotations=annotations)
)

return {
Expand Down
40 changes: 39 additions & 1 deletion lms/djangoapps/grades/signals/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@

from contextlib import contextmanager
from logging import getLogger
from urllib.parse import unquote

from django.conf import settings
from django.core.cache import cache
from django.dispatch import receiver
from opaque_keys.edx.keys import LearningContextKey
from openedx_events.learning.signals import EXAM_ATTEMPT_REJECTED, EXAM_ATTEMPT_VERIFIED
from submissions.models import score_reset, score_set
from xblock.scorable import ScorableXBlockMixin, Score

from common.djangoapps.student.models import user_by_anonymous_id
from common.djangoapps.student.signals import ENROLLMENT_TRACK_UPDATED
from common.djangoapps.student.signals import ENROLL_STATUS_CHANGE, ENROLLMENT_TRACK_UPDATED
from common.djangoapps.track.event_transaction_utils import get_event_transaction_id, get_event_transaction_type
from common.djangoapps.util.date_utils import to_timestamp
from lms.djangoapps.courseware.model_data import get_score, set_score
Expand Down Expand Up @@ -347,3 +350,38 @@ def exam_attempt_rejected_event_handler(sender, signal, **kwargs): # pylint: di
overrider=None,
comment=None,
)

@receiver(ENROLL_STATUS_CHANGE)
def invalidate_usercount_in_course_cache(sender, signal, **kwargs): # pylint: disable=unused-argument
"""
Invalidate the cache of get_user_count utility on CourseEnrollment status change.
"""
event_data = kwargs.get('exam_attempt')
course_key = event_data.course_id

cache_key_prefix = f"usercount.{course_key}"
cache_keys = get_cache_keys(cache_key_prefix)
cache.delete_many(cache_keys)


def get_cache_keys(cache_key_prefix):
"""
Get all cache keys for the given cache key prefix.
LocMemCache does not have a keys method, so we need to iterate over the cache
and manually filter out the keys that match the given prefix.
"""
cache_backend = settings.CACHES['default']['BACKEND']
if cache_backend == 'django_redis.cache.RedisCache':
yield cache.iter_keys(f"{cache_key_prefix}*")
elif cache_backend == 'django.core.cache.backends.locmem.LocMemCache':
for key in cache._cache.keys(): # pylint: disable=protected-access
try:
decoded_key = unquote(key.split(':', 2)[-1], encoding='utf-8')
except IndexError:
continue

if decoded_key.startswith(cache_key_prefix):
yield decoded_key
else:
log.error(f"Unsupported cache backend: {cache_backend}")
yield

0 comments on commit 175eebe

Please sign in to comment.