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:
alangsto
2021-04-26 12:04:04 -04:00
committed by GitHub
parent 9892f358fc
commit 50d57a64bb
4 changed files with 105 additions and 12 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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.

View File

@@ -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()