diff --git a/cms/djangoapps/contentstore/signals/handlers.py b/cms/djangoapps/contentstore/signals/handlers.py index 0fd23d8ebf..f32cf63fb2 100644 --- a/cms/djangoapps/contentstore/signals/handlers.py +++ b/cms/djangoapps/contentstore/signals/handlers.py @@ -1,8 +1,10 @@ """ receivers of course_published and library_updated events in order to trigger indexing task """ -import logging from datetime import datetime +from functools import wraps +import logging +from django.core.cache import cache from django.dispatch import receiver from pytz import UTC @@ -19,6 +21,20 @@ from xmodule.modulestore.django import SignalHandler, modulestore log = logging.getLogger(__name__) +GRADING_POLICY_COUNTDOWN_SECONDS = 3600 + + +def locked(expiry_seconds, key): + def task_decorator(func): + @wraps(func) + def wrapper(*args, **kwargs): + cache_key = '{}-{}'.format(func.__name__, kwargs[key]) + if cache.add(cache_key, "true", expiry_seconds): + log.info('Locking task in cache with key: %s for %s seconds', cache_key, expiry_seconds) + return func(*args, **kwargs) + return wrapper + return task_decorator + @receiver(SignalHandler.course_published) def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=unused-argument @@ -90,6 +106,7 @@ def handle_item_deleted(**kwargs): @receiver(GRADING_POLICY_CHANGED) +@locked(expiry_seconds=GRADING_POLICY_COUNTDOWN_SECONDS, key='course_key') def handle_grading_policy_changed(sender, **kwargs): # pylint: disable=unused-argument """ @@ -100,7 +117,7 @@ def handle_grading_policy_changed(sender, **kwargs): 'event_transaction_id': unicode(get_event_transaction_id()), 'event_transaction_type': unicode(get_event_transaction_type()), } - result = compute_all_grades_for_course.apply_async(kwargs=kwargs) + result = compute_all_grades_for_course.apply_async(kwargs=kwargs, countdown=GRADING_POLICY_COUNTDOWN_SECONDS) log.info("Grades: Created {task_name}[{task_id}] with arguments {kwargs}".format( task_name=compute_all_grades_for_course.name, task_id=result.task_id, diff --git a/cms/djangoapps/contentstore/tests/test_signals.py b/cms/djangoapps/contentstore/tests/test_signals.py new file mode 100644 index 0000000000..e2c6544ab8 --- /dev/null +++ b/cms/djangoapps/contentstore/tests/test_signals.py @@ -0,0 +1,39 @@ +import ddt +from mock import patch, Mock + +from cms.djangoapps.contentstore.signals.handlers import ( + GRADING_POLICY_COUNTDOWN_SECONDS, + handle_grading_policy_changed +) +from student.models import CourseEnrollment, anonymous_id_for_user +from student.tests.factories import UserFactory +from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase +from xmodule.modulestore.tests.factories import CourseFactory + + +@ddt.ddt +class LockedTest(ModuleStoreTestCase): + def setUp(self): + super(LockedTest, self).setUp() + self.course = CourseFactory.create( + org='edx', + name='course', + run='run', + ) + self.user = UserFactory.create() + CourseEnrollment.enroll(self.user, self.course.id) + + @patch('cms.djangoapps.contentstore.signals.handlers.cache.add') + @patch('cms.djangoapps.contentstore.signals.handlers.cache.delete') + @patch('cms.djangoapps.contentstore.signals.handlers.compute_all_grades_for_course.apply_async') + @ddt.data(True, False) + def test_locked(self, lock_available, compute_grades_async_mock, delete_mock, add_mock): + add_mock.return_value = lock_available + sender = Mock() + + handle_grading_policy_changed(sender, course_key=unicode(self.course.id)) + + cache_key = 'handle_grading_policy_changed-{}'.format(unicode(self.course.id)) + self.assertEqual(lock_available, compute_grades_async_mock.called) + if lock_available: + add_mock.assert_called_once_with(cache_key, "true", GRADING_POLICY_COUNTDOWN_SECONDS)