MST-757 Move exam registration to async task (#27398)
Synchronously registering proctored exams while saving content to studio is causing a significant slow down. The function that registers the exams has been moved to an async task. In addition, a signal handler on_course_publish has also been moved to the async task, as it relies on exam registration being complete before being executed.
This commit is contained in:
@@ -52,6 +52,22 @@ SHOW_REVIEW_RULES_FLAG = CourseWaffleFlag( # lint-amnesty, pylint: disable=togg
|
||||
module_name=__name__,
|
||||
)
|
||||
|
||||
# Waffle flag to move the registration of special exams to an async celery task
|
||||
# .. toggle_name: contentstore.async_register_exams
|
||||
# .. toggle_implementation: CourseWaffleFlag
|
||||
# .. toggle_default: False
|
||||
# .. toggle_description: Toggles the asynchronous registration of special exams
|
||||
# .. toggle_use_cases: temporary, open_edx
|
||||
# .. toggle_creation_date: 2021-04-21
|
||||
# .. toggle_target_removal_date: 2021-05-07
|
||||
# .. toggle_warnings:
|
||||
# .. toggle_tickets: https://openedx.atlassian.net/browse/MST-757
|
||||
ENABLE_ASYNC_REGISTER_EXAMS = CourseWaffleFlag(
|
||||
waffle_namespace=waffle_flags(),
|
||||
flag_name='async_register_exams',
|
||||
module_name=__name__,
|
||||
)
|
||||
|
||||
# Waffle flag to redirect to the library authoring MFE.
|
||||
# .. toggle_name: contentstore.library_authoring_mfe
|
||||
# .. toggle_implementation: WaffleFlag
|
||||
|
||||
@@ -24,6 +24,7 @@ from openedx.core.lib.gating import api as gating_api
|
||||
from xmodule.modulestore.django import SignalHandler, modulestore
|
||||
|
||||
from .signals import GRADING_POLICY_CHANGED
|
||||
from ..config.waffle import ENABLE_ASYNC_REGISTER_EXAMS
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
@@ -51,18 +52,23 @@ def listen_for_course_publish(sender, course_key, **kwargs): # pylint: disable=
|
||||
registering proctored exams, building up credit requirements, and performing
|
||||
search indexing
|
||||
"""
|
||||
is_enabled = ENABLE_ASYNC_REGISTER_EXAMS.is_enabled(course_key)
|
||||
if is_enabled:
|
||||
from cms.djangoapps.contentstore.tasks import update_special_exams_and_publish
|
||||
course_key_str = str(course_key)
|
||||
update_special_exams_and_publish.delay(course_key_str)
|
||||
else:
|
||||
# first is to registered exams, the credit subsystem will assume that
|
||||
# all proctored exams have already been registered, so we have to do that first
|
||||
try:
|
||||
register_special_exams(course_key)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exception:
|
||||
log.exception(exception)
|
||||
|
||||
# first is to registered exams, the credit subsystem will assume that
|
||||
# all proctored exams have already been registered, so we have to do that first
|
||||
try:
|
||||
register_special_exams(course_key)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exception:
|
||||
log.exception(exception)
|
||||
|
||||
# then call into the credit subsystem (in /openedx/djangoapps/credit)
|
||||
# to perform any 'on_publish' workflow
|
||||
on_course_publish(course_key)
|
||||
# then call into the credit subsystem (in /openedx/djangoapps/credit)
|
||||
# to perform any 'on_publish' workflow
|
||||
on_course_publish(course_key)
|
||||
|
||||
# import here, because signal is registered at startup, but items in tasks are not yet able to be loaded
|
||||
from cms.djangoapps.contentstore.tasks import update_outline_from_modulestore_task, update_search_index
|
||||
|
||||
@@ -232,6 +232,31 @@ def update_library_index(library_id, triggered_time_isoformat):
|
||||
LOGGER.debug('Search indexing successful for library %s', library_id)
|
||||
|
||||
|
||||
@shared_task
|
||||
@set_code_owner_attribute
|
||||
def update_special_exams_and_publish(course_key_str):
|
||||
"""
|
||||
Registers special exams for a given course and calls publishing flow.
|
||||
|
||||
on_course_publish expects that the edx-proctoring subsystem has been refreshed
|
||||
before being executed, so both functions are called here synchronously.
|
||||
"""
|
||||
from cms.djangoapps.contentstore.proctoring import register_special_exams
|
||||
from openedx.core.djangoapps.credit.signals import on_course_publish
|
||||
|
||||
course_key = CourseKey.from_string(course_key_str)
|
||||
LOGGER.info('Attempting to register exams for course %s', course_key_str)
|
||||
try:
|
||||
register_special_exams(course_key)
|
||||
LOGGER.info('Successfully registered exams for course %s', course_key_str)
|
||||
# pylint: disable=broad-except
|
||||
except Exception as exception:
|
||||
LOGGER.exception(exception)
|
||||
|
||||
LOGGER.info('Publishing course %s', course_key_str)
|
||||
on_course_publish(course_key)
|
||||
|
||||
|
||||
class CourseExportTask(UserTask): # pylint: disable=abstract-method
|
||||
"""
|
||||
Base class for course and library export tasks.
|
||||
|
||||
@@ -9,9 +9,11 @@ from unittest.mock import patch
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from edx_proctoring.api import get_all_exams_for_course, get_review_policy_by_exam_id
|
||||
from edx_toggles.toggles.testutils import override_waffle_flag
|
||||
from pytz import UTC
|
||||
|
||||
from cms.djangoapps.contentstore.signals.handlers import listen_for_course_publish
|
||||
from cms.djangoapps.contentstore.config.waffle import ENABLE_ASYNC_REGISTER_EXAMS
|
||||
from common.djangoapps.student.tests.factories import UserFactory
|
||||
from common.lib.xmodule.xmodule.modulestore import ModuleStoreEnum
|
||||
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase
|
||||
@@ -100,7 +102,9 @@ class TestProctoredExams(ModuleStoreTestCase):
|
||||
is_onboarding_exam=is_onboarding_exam,
|
||||
)
|
||||
|
||||
listen_for_course_publish(self, self.course.id)
|
||||
with patch('cms.djangoapps.contentstore.tasks.update_special_exams_and_publish') as mock_task:
|
||||
listen_for_course_publish(self, self.course.id)
|
||||
mock_task.delay.assert_not_called()
|
||||
|
||||
self._verify_exam_data(sequence, True)
|
||||
|
||||
@@ -314,3 +318,45 @@ class TestProctoredExams(ModuleStoreTestCase):
|
||||
listen_for_course_publish(self, self.course.id)
|
||||
exams = get_all_exams_for_course(str(self.course.id))
|
||||
assert exams[0]['due_date'] is not None
|
||||
|
||||
@override_waffle_flag(ENABLE_ASYNC_REGISTER_EXAMS, active=True)
|
||||
def test_async_waffle_flag_publishes(self):
|
||||
chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
|
||||
sequence = ItemFactory.create(
|
||||
parent=chapter,
|
||||
category='sequential',
|
||||
display_name='Test Proctored Exam',
|
||||
graded=True,
|
||||
is_time_limited=True,
|
||||
default_time_limit_minutes=10,
|
||||
is_proctored_exam=True,
|
||||
hide_after_due=False,
|
||||
is_onboarding_exam=False,
|
||||
exam_review_rules="allow_use_of_paper",
|
||||
)
|
||||
|
||||
listen_for_course_publish(self, self.course.id)
|
||||
|
||||
exams = get_all_exams_for_course(str(self.course.id))
|
||||
self.assertEqual(len(exams), 1)
|
||||
self._verify_exam_data(sequence, True)
|
||||
|
||||
@override_waffle_flag(ENABLE_ASYNC_REGISTER_EXAMS, active=True)
|
||||
def test_async_waffle_flag_task(self):
|
||||
chapter = ItemFactory.create(parent=self.course, category='chapter', display_name='Test Section')
|
||||
ItemFactory.create(
|
||||
parent=chapter,
|
||||
category='sequential',
|
||||
display_name='Test Proctored Exam',
|
||||
graded=True,
|
||||
is_time_limited=True,
|
||||
default_time_limit_minutes=10,
|
||||
is_proctored_exam=True,
|
||||
hide_after_due=False,
|
||||
is_onboarding_exam=False,
|
||||
exam_review_rules="allow_use_of_paper",
|
||||
)
|
||||
|
||||
with patch('cms.djangoapps.contentstore.tasks.update_special_exams_and_publish') as mock_task:
|
||||
listen_for_course_publish(self, self.course.id)
|
||||
mock_task.delay.assert_called()
|
||||
|
||||
Reference in New Issue
Block a user