Make Robust Grades Robust Again
As part of the Robust Grades rollout, we expect to see some DatabaseErrors in various methods that write to the database. Since the success of this write operation is not needed for the end-user, we just log and swallow the error. In the future, we'll also enqueue an async task to finish the write operation that failed.
This commit is contained in:
@@ -229,18 +229,18 @@ class TestFieldOverrideMongoPerformance(FieldOverridePerformanceTestCase):
|
||||
# # of sql queries to default,
|
||||
# # of mongo queries,
|
||||
# )
|
||||
('no_overrides', 1, True, False): (20, 6),
|
||||
('no_overrides', 2, True, False): (20, 6),
|
||||
('no_overrides', 3, True, False): (20, 6),
|
||||
('ccx', 1, True, False): (20, 6),
|
||||
('ccx', 2, True, False): (20, 6),
|
||||
('ccx', 3, True, False): (20, 6),
|
||||
('no_overrides', 1, False, False): (20, 6),
|
||||
('no_overrides', 2, False, False): (20, 6),
|
||||
('no_overrides', 3, False, False): (20, 6),
|
||||
('ccx', 1, False, False): (20, 6),
|
||||
('ccx', 2, False, False): (20, 6),
|
||||
('ccx', 3, False, False): (20, 6),
|
||||
('no_overrides', 1, True, False): (22, 6),
|
||||
('no_overrides', 2, True, False): (22, 6),
|
||||
('no_overrides', 3, True, False): (22, 6),
|
||||
('ccx', 1, True, False): (22, 6),
|
||||
('ccx', 2, True, False): (22, 6),
|
||||
('ccx', 3, True, False): (22, 6),
|
||||
('no_overrides', 1, False, False): (22, 6),
|
||||
('no_overrides', 2, False, False): (22, 6),
|
||||
('no_overrides', 3, False, False): (22, 6),
|
||||
('ccx', 1, False, False): (22, 6),
|
||||
('ccx', 2, False, False): (22, 6),
|
||||
('ccx', 3, False, False): (22, 6),
|
||||
}
|
||||
|
||||
|
||||
@@ -252,19 +252,19 @@ class TestFieldOverrideSplitPerformance(FieldOverridePerformanceTestCase):
|
||||
__test__ = True
|
||||
|
||||
TEST_DATA = {
|
||||
('no_overrides', 1, True, False): (20, 3),
|
||||
('no_overrides', 2, True, False): (20, 3),
|
||||
('no_overrides', 3, True, False): (20, 3),
|
||||
('ccx', 1, True, False): (20, 3),
|
||||
('ccx', 2, True, False): (20, 3),
|
||||
('ccx', 3, True, False): (20, 3),
|
||||
('ccx', 1, True, True): (21, 3),
|
||||
('ccx', 2, True, True): (21, 3),
|
||||
('ccx', 3, True, True): (21, 3),
|
||||
('no_overrides', 1, False, False): (20, 3),
|
||||
('no_overrides', 2, False, False): (20, 3),
|
||||
('no_overrides', 3, False, False): (20, 3),
|
||||
('ccx', 1, False, False): (20, 3),
|
||||
('ccx', 2, False, False): (20, 3),
|
||||
('ccx', 3, False, False): (20, 3),
|
||||
('no_overrides', 1, True, False): (22, 3),
|
||||
('no_overrides', 2, True, False): (22, 3),
|
||||
('no_overrides', 3, True, False): (22, 3),
|
||||
('ccx', 1, True, False): (22, 3),
|
||||
('ccx', 2, True, False): (22, 3),
|
||||
('ccx', 3, True, False): (22, 3),
|
||||
('ccx', 1, True, True): (23, 3),
|
||||
('ccx', 2, True, True): (23, 3),
|
||||
('ccx', 3, True, True): (23, 3),
|
||||
('no_overrides', 1, False, False): (22, 3),
|
||||
('no_overrides', 2, False, False): (22, 3),
|
||||
('no_overrides', 3, False, False): (22, 3),
|
||||
('ccx', 1, False, False): (22, 3),
|
||||
('ccx', 2, False, False): (22, 3),
|
||||
('ccx', 3, False, False): (22, 3),
|
||||
}
|
||||
|
||||
@@ -1345,17 +1345,17 @@ class ProgressPageTests(ModuleStoreTestCase):
|
||||
"""Test that query counts remain the same for self-paced and instructor-paced courses."""
|
||||
SelfPacedConfiguration(enabled=self_paced_enabled).save()
|
||||
self.setup_course(self_paced=self_paced)
|
||||
with self.assertNumQueries(37), check_mongo_calls(4):
|
||||
with self.assertNumQueries(39), check_mongo_calls(4):
|
||||
self._get_progress_page()
|
||||
|
||||
def test_progress_queries(self):
|
||||
self.setup_course()
|
||||
with self.assertNumQueries(37), check_mongo_calls(4):
|
||||
with self.assertNumQueries(39), check_mongo_calls(4):
|
||||
self._get_progress_page()
|
||||
|
||||
# subsequent accesses to the progress page require fewer queries.
|
||||
for _ in range(2):
|
||||
with self.assertNumQueries(20), check_mongo_calls(4):
|
||||
with self.assertNumQueries(22), check_mongo_calls(4):
|
||||
self._get_progress_page()
|
||||
|
||||
@patch(
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
SubsectionGrade Class
|
||||
"""
|
||||
from collections import OrderedDict
|
||||
from contextlib import contextmanager
|
||||
from django.db import transaction
|
||||
from django.db.utils import DatabaseError
|
||||
from lazy import lazy
|
||||
from logging import getLogger
|
||||
from courseware.model_data import ScoresClient
|
||||
@@ -10,6 +13,7 @@ from lms.djangoapps.grades.models import BlockRecord, PersistentSubsectionGrade
|
||||
from lms.djangoapps.grades.config.models import PersistentGradesEnabledFlag
|
||||
from student.models import anonymous_id_for_user, User
|
||||
from submissions import api as submissions_api
|
||||
from traceback import format_exc
|
||||
from xmodule import block_metadata_utils, graders
|
||||
from xmodule.graders import Score
|
||||
|
||||
@@ -17,6 +21,20 @@ from xmodule.graders import Score
|
||||
log = getLogger(__name__)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def persistence_safe_fallback():
|
||||
"""
|
||||
A context manager intended to be used to wrap robust grades database-specific code that can be ignored on failure.
|
||||
"""
|
||||
try:
|
||||
with transaction.atomic():
|
||||
yield
|
||||
except DatabaseError:
|
||||
# Error deliberately logged, then swallowed. It's assumed that the wrapped code is not guaranteed to succeed.
|
||||
# TODO: enqueue a celery task to finish the task asynchronously, see TNL-5471
|
||||
log.warning("Persistent Grades: Persistence Error, falling back.\n{}".format(format_exc()))
|
||||
|
||||
|
||||
class SubsectionGrade(object):
|
||||
"""
|
||||
Class for Subsection Grades.
|
||||
@@ -254,8 +272,9 @@ class SubsectionGradeFactory(object):
|
||||
if read_only:
|
||||
self._unsaved_subsection_grades.append(subsection_grade)
|
||||
else:
|
||||
grade_model = subsection_grade.create_model(self.student)
|
||||
self._update_saved_subsection_grade(subsection.location, grade_model)
|
||||
with persistence_safe_fallback():
|
||||
grade_model = subsection_grade.create_model(self.student)
|
||||
self._update_saved_subsection_grade(subsection.location, grade_model)
|
||||
return subsection_grade
|
||||
|
||||
def bulk_create_unsaved(self):
|
||||
@@ -264,8 +283,9 @@ class SubsectionGradeFactory(object):
|
||||
"""
|
||||
self._log_event(log.warning, u"bulk_create_unsaved")
|
||||
|
||||
SubsectionGrade.bulk_create_models(self.student, self._unsaved_subsection_grades, self.course.id)
|
||||
self._unsaved_subsection_grades = []
|
||||
with persistence_safe_fallback():
|
||||
SubsectionGrade.bulk_create_models(self.student, self._unsaved_subsection_grades, self.course.id)
|
||||
self._unsaved_subsection_grades = []
|
||||
|
||||
def update(self, subsection, block_structure=None):
|
||||
"""
|
||||
|
||||
@@ -4,6 +4,7 @@ Test saved subsection grade functionality.
|
||||
|
||||
import ddt
|
||||
from django.conf import settings
|
||||
from django.db.utils import DatabaseError
|
||||
from mock import patch
|
||||
|
||||
from capa.tests.response_xml_factory import MultipleChoiceResponseXMLFactory
|
||||
@@ -117,7 +118,7 @@ class SubsectionGradeFactoryTest(GradeTestBase):
|
||||
'lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory._get_saved_grade',
|
||||
wraps=self.subsection_grade_factory._get_saved_grade # pylint: disable=protected-access
|
||||
) as mock_get_saved_grade:
|
||||
with self.assertNumQueries(12):
|
||||
with self.assertNumQueries(14):
|
||||
grade_a = self.subsection_grade_factory.create(self.sequence)
|
||||
self.assertTrue(mock_get_saved_grade.called)
|
||||
self.assertTrue(mock_create_grade.called)
|
||||
@@ -133,6 +134,30 @@ class SubsectionGradeFactoryTest(GradeTestBase):
|
||||
self.assertEqual(grade_a.url_name, grade_b.url_name)
|
||||
self.assertEqual(grade_a.all_total, grade_b.all_total)
|
||||
|
||||
@ddt.data(
|
||||
(
|
||||
'lms.djangoapps.grades.new.subsection_grade.SubsectionGrade.create_model',
|
||||
lambda self: self.subsection_grade_factory.create(self.sequence)
|
||||
),
|
||||
(
|
||||
'lms.djangoapps.grades.new.subsection_grade.SubsectionGrade.bulk_create_models',
|
||||
lambda self: self.subsection_grade_factory.bulk_create_unsaved()
|
||||
),
|
||||
)
|
||||
@ddt.unpack
|
||||
def test_fallback_handling(self, underlying_method, method_to_test):
|
||||
"""
|
||||
Tests that the persistent grades fallback handler functions as expected.
|
||||
"""
|
||||
with patch('lms.djangoapps.grades.new.subsection_grade.log') as log_mock:
|
||||
with patch(underlying_method) as underlying:
|
||||
underlying.side_effect = DatabaseError("I'm afraid I can't do that")
|
||||
method_to_test(self)
|
||||
# By making it this far, we implicitly assert "the factory method swallowed the exception correctly"
|
||||
self.assertTrue(
|
||||
log_mock.warning.call_args_list[0].startswith("Persistent Grades: Persistence Error, falling back.")
|
||||
)
|
||||
|
||||
@patch.dict(settings.FEATURES, {'PERSISTENT_GRADES_ENABLED_FOR_ALL_TESTS': False})
|
||||
@ddt.data(
|
||||
(True, True),
|
||||
|
||||
@@ -1670,7 +1670,7 @@ class TestCertificateGeneration(InstructorTaskModuleTestCase):
|
||||
'failed': 3,
|
||||
'skipped': 2
|
||||
}
|
||||
with self.assertNumQueries(175):
|
||||
with self.assertNumQueries(191):
|
||||
self.assertCertificatesGenerated(task_input, expected_results)
|
||||
|
||||
expected_results = {
|
||||
|
||||
Reference in New Issue
Block a user