diff --git a/lms/djangoapps/courseware/tests/test_model_data.py b/lms/djangoapps/courseware/tests/test_model_data.py index dcd6fc7466..da06e85749 100644 --- a/lms/djangoapps/courseware/tests/test_model_data.py +++ b/lms/djangoapps/courseware/tests/test_model_data.py @@ -139,7 +139,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase): # to discover if something other than the DjangoXBlockUserStateClient # has written to the StudentModule (such as UserStateCache setting the score # on the StudentModule). - with self.assertNumQueries(2, using='default'): + with self.assertNumQueries(4, using='default'): with self.assertNumQueries(1, using='student_module_history'): self.kvs.set(user_state_key('a_field'), 'new_value') self.assertEquals(1, StudentModule.objects.all().count()) @@ -152,7 +152,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase): # to discover if something other than the DjangoXBlockUserStateClient # has written to the StudentModule (such as UserStateCache setting the score # on the StudentModule). - with self.assertNumQueries(2, using='default'): + with self.assertNumQueries(4, using='default'): with self.assertNumQueries(1, using='student_module_history'): self.kvs.set(user_state_key('not_a_field'), 'new_value') self.assertEquals(1, StudentModule.objects.all().count()) @@ -206,7 +206,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase): # We also need to read the database to discover if something other than the # DjangoXBlockUserStateClient has written to the StudentModule (such as # UserStateCache setting the score on the StudentModule). - with self.assertNumQueries(2, using="default"): + with self.assertNumQueries(4, using="default"): with self.assertNumQueries(1, using="student_module_history"): self.kvs.set_many(kv_dict) diff --git a/lms/djangoapps/courseware/user_state_client.py b/lms/djangoapps/courseware/user_state_client.py index ec5bbee7c4..1731b2e2e7 100644 --- a/lms/djangoapps/courseware/user_state_client.py +++ b/lms/djangoapps/courseware/user_state_client.py @@ -6,7 +6,7 @@ data in a Django ORM model. import itertools from operator import attrgetter from time import time - +import logging try: import simplejson as json except ImportError: @@ -14,10 +14,14 @@ except ImportError: import dogstats_wrapper as dog_stats_api from django.contrib.auth.models import User +from django.db import transaction +from django.db.utils import IntegrityError from xblock.fields import Scope from courseware.models import StudentModule, BaseStudentModuleHistory from edx_user_state_client.interface import XBlockUserStateClient, XBlockUserState +log = logging.getLogger(__name__) + class DjangoXBlockUserStateClient(XBlockUserStateClient): """ @@ -222,8 +226,19 @@ class DjangoXBlockUserStateClient(XBlockUserStateClient): current_state.update(state) num_fields_after = len(current_state) student_module.state = json.dumps(current_state) - # We just read this object, so we know that we can do an update - student_module.save(force_update=True) + try: + with transaction.atomic(): + # Updating the object - force_update guarantees no INSERT will occur. + student_module.save(force_update=True) + except IntegrityError: + # The UPDATE above failed. Log information - but ignore the error. + # See https://openedx.atlassian.net/browse/TNL-5365 + log.warning("set_many: IntegrityError for student {} - course_id {} - usage key {}".format( + user, repr(unicode(usage_key.course_key)), usage_key + )) + log.warning("set_many: All {} block keys: {}".format( + len(block_keys_to_state), block_keys_to_state.keys() + )) # The rest of this method exists only to submit DataDog events. # Remove it once we're no longer interested in the data.