Commit 88b91874 by Calen Pennington

Decrease the number of inserts and updates needed by DjangoKeyValueStore

parent 2d51f643
...@@ -266,7 +266,7 @@ class FieldDataCache(object): ...@@ -266,7 +266,7 @@ class FieldDataCache(object):
def find_or_create(self, key): def find_or_create(self, key):
''' '''
Find a model data object in this cache, or create it if it doesn't Find a model data object in this cache, or create a new one if it doesn't
exist exist
''' '''
field_object = self.find(key) field_object = self.find(key)
...@@ -275,28 +275,26 @@ class FieldDataCache(object): ...@@ -275,28 +275,26 @@ class FieldDataCache(object):
return field_object return field_object
if key.scope == Scope.user_state: if key.scope == Scope.user_state:
field_object, __ = StudentModule.objects.get_or_create( field_object = StudentModule(
course_id=self.course_id, course_id=self.course_id,
student_id=key.user_id, student_id=key.user_id,
module_state_key=key.block_scope_id, module_state_key=key.block_scope_id,
defaults={ state=json.dumps({}),
'state': json.dumps({}), module_type=key.block_scope_id.block_type,
'module_type': key.block_scope_id.block_type,
},
) )
elif key.scope == Scope.user_state_summary: elif key.scope == Scope.user_state_summary:
field_object, __ = XModuleUserStateSummaryField.objects.get_or_create( field_object = XModuleUserStateSummaryField(
field_name=key.field_name, field_name=key.field_name,
usage_id=key.block_scope_id usage_id=key.block_scope_id
) )
elif key.scope == Scope.preferences: elif key.scope == Scope.preferences:
field_object, __ = XModuleStudentPrefsField.objects.get_or_create( field_object = XModuleStudentPrefsField(
field_name=key.field_name, field_name=key.field_name,
module_type=BlockTypeKeyV1(key.block_family, key.block_scope_id), module_type=BlockTypeKeyV1(key.block_family, key.block_scope_id),
student_id=key.user_id, student_id=key.user_id,
) )
elif key.scope == Scope.user_info: elif key.scope == Scope.user_info:
field_object, __ = XModuleStudentInfoField.objects.get_or_create( field_object = XModuleStudentInfoField(
field_name=key.field_name, field_name=key.field_name,
student_id=key.user_id, student_id=key.user_id,
) )
...@@ -362,39 +360,39 @@ class DjangoKeyValueStore(KeyValueStore): ...@@ -362,39 +360,39 @@ class DjangoKeyValueStore(KeyValueStore):
""" """
saved_fields = [] saved_fields = []
# field_objects maps a field_object to a list of associated fields # field_objects maps id(field_object) to a the object and a list of associated fields.
field_objects = dict() # We use id() because FieldDataCache might return django models with no primary key
for field in kv_dict: # set, but will return the same django model each time the same key is passed in.
# Check field for validity dirty_field_objects = defaultdict(lambda: (None, []))
if field.scope not in self._allowed_scopes: for key in kv_dict:
raise InvalidScopeError(field) # Check key for validity
if key.scope not in self._allowed_scopes:
# If the field is valid and isn't already in the dictionary, add it. raise InvalidScopeError(key)
field_object = self._field_data_cache.find_or_create(field)
if field_object not in field_objects.keys(): field_object = self._field_data_cache.find_or_create(key)
field_objects[field_object] = [] # Update the list dirtied field_objects
# Update the list of associated fields _, dirty_names = dirty_field_objects.setdefault(id(field_object), (field_object, []))
field_objects[field_object].append(field) dirty_names.append(key.field_name)
# Special case when scope is for the user state, because this scope saves fields in a single row # Special case when scope is for the user state, because this scope saves fields in a single row
if field.scope == Scope.user_state: if key.scope == Scope.user_state:
state = json.loads(field_object.state) state = json.loads(field_object.state)
state[field.field_name] = kv_dict[field] state[key.field_name] = kv_dict[key]
field_object.state = json.dumps(state) field_object.state = json.dumps(state)
else: else:
# The remaining scopes save fields on different rows, so # The remaining scopes save fields on different rows, so
# we don't have to worry about conflicts # we don't have to worry about conflicts
field_object.value = json.dumps(kv_dict[field]) field_object.value = json.dumps(kv_dict[key])
for field_object in field_objects: for field_object, names in dirty_field_objects.values():
try: try:
# Save the field object that we made above # Save the field object that we made above
field_object.save() field_object.save(force_update=field_object.pk is not None)
# If save is successful on this scope, add the saved fields to # If save is successful on this scope, add the saved fields to
# the list of successful saves # the list of successful saves
saved_fields.extend([field.field_name for field in field_objects[field_object]]) saved_fields.extend(names)
except DatabaseError: except DatabaseError:
log.exception('Error saving fields %r', field_objects[field_object]) log.exception('Error saving fields %r', names)
raise KeyValueMultiSaveError(saved_fields) raise KeyValueMultiSaveError(saved_fields)
def delete(self, key): def delete(self, key):
...@@ -409,7 +407,7 @@ class DjangoKeyValueStore(KeyValueStore): ...@@ -409,7 +407,7 @@ class DjangoKeyValueStore(KeyValueStore):
state = json.loads(field_object.state) state = json.loads(field_object.state)
del state[key.field_name] del state[key.field_name]
field_object.state = json.dumps(state) field_object.state = json.dumps(state)
field_object.save() field_object.save(force_update=field_object.pk is not None)
else: else:
field_object.delete() field_object.delete()
......
...@@ -109,7 +109,11 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase): ...@@ -109,7 +109,11 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
# There should be only one query to load a single descriptor with a single user_state field # There should be only one query to load a single descriptor with a single user_state field
with self.assertNumQueries(1): with self.assertNumQueries(1):
self.field_data_cache = FieldDataCache([mock_descriptor([mock_field(Scope.user_state, 'a_field')])], course_id, self.user) self.field_data_cache = FieldDataCache(
[mock_descriptor([mock_field(Scope.user_state, 'a_field')])],
course_id,
self.user
)
self.kvs = DjangoKeyValueStore(self.field_data_cache) self.kvs = DjangoKeyValueStore(self.field_data_cache)
...@@ -129,7 +133,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase): ...@@ -129,7 +133,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
"Test that setting an existing user_state field changes the value" "Test that setting an existing user_state field changes the value"
# We are updating a problem, so we write to courseware_studentmodulehistory # We are updating a problem, so we write to courseware_studentmodulehistory
# as well as courseware_studentmodule # as well as courseware_studentmodule
with self.assertNumQueries(3): with self.assertNumQueries(2):
self.kvs.set(user_state_key('a_field'), 'new_value') self.kvs.set(user_state_key('a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count()) self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'b_field': 'b_value', 'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state)) self.assertEquals({'b_field': 'b_value', 'a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
...@@ -138,7 +142,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase): ...@@ -138,7 +142,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
"Test that setting a new user_state field changes the value" "Test that setting a new user_state field changes the value"
# We are updating a problem, so we write to courseware_studentmodulehistory # We are updating a problem, so we write to courseware_studentmodulehistory
# as well as courseware_studentmodule # as well as courseware_studentmodule
with self.assertNumQueries(3): with self.assertNumQueries(2):
self.kvs.set(user_state_key('not_a_field'), 'new_value') self.kvs.set(user_state_key('not_a_field'), 'new_value')
self.assertEquals(1, StudentModule.objects.all().count()) self.assertEquals(1, StudentModule.objects.all().count())
self.assertEquals({'b_field': 'b_value', 'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state)) self.assertEquals({'b_field': 'b_value', 'a_field': 'a_value', 'not_a_field': 'new_value'}, json.loads(StudentModule.objects.all()[0].state))
...@@ -147,7 +151,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase): ...@@ -147,7 +151,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
"Test that deleting an existing field removes it from the StudentModule" "Test that deleting an existing field removes it from the StudentModule"
# We are updating a problem, so we write to courseware_studentmodulehistory # We are updating a problem, so we write to courseware_studentmodulehistory
# as well as courseware_studentmodule # as well as courseware_studentmodule
with self.assertNumQueries(3): with self.assertNumQueries(2):
self.kvs.delete(user_state_key('a_field')) self.kvs.delete(user_state_key('a_field'))
self.assertEquals(1, StudentModule.objects.all().count()) self.assertEquals(1, StudentModule.objects.all().count())
self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field')) self.assertRaises(KeyError, self.kvs.get, user_state_key('not_a_field'))
...@@ -184,7 +188,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase): ...@@ -184,7 +188,7 @@ class TestStudentModuleStorage(OtherUserFailureTestMixin, TestCase):
# Scope.user_state is stored in a single row in the database, so we only # Scope.user_state is stored in a single row in the database, so we only
# need to send a single update to that table. # need to send a single update to that table.
# We also are updating a problem, so we write to courseware student module history # We also are updating a problem, so we write to courseware student module history
with self.assertNumQueries(3): with self.assertNumQueries(2):
self.kvs.set_many(kv_dict) self.kvs.set_many(kv_dict)
for key in kv_dict: for key in kv_dict:
...@@ -228,7 +232,7 @@ class TestMissingStudentModule(TestCase): ...@@ -228,7 +232,7 @@ class TestMissingStudentModule(TestCase):
# We are updating a problem, so we write to courseware_studentmodulehistory # We are updating a problem, so we write to courseware_studentmodulehistory
# as well as courseware_studentmodule # as well as courseware_studentmodule
with self.assertNumQueries(6): with self.assertNumQueries(2):
self.kvs.set(user_state_key('a_field'), 'a_value') self.kvs.set(user_state_key('a_field'), 'a_value')
self.assertEquals(1, len(self.field_data_cache.cache)) self.assertEquals(1, len(self.field_data_cache.cache))
...@@ -281,7 +285,7 @@ class StorageTestBase(object): ...@@ -281,7 +285,7 @@ class StorageTestBase(object):
self.kvs = DjangoKeyValueStore(self.field_data_cache) self.kvs = DjangoKeyValueStore(self.field_data_cache)
def test_set_and_get_existing_field(self): def test_set_and_get_existing_field(self):
with self.assertNumQueries(2): with self.assertNumQueries(1):
self.kvs.set(self.key_factory('existing_field'), 'test_value') self.kvs.set(self.key_factory('existing_field'), 'test_value')
with self.assertNumQueries(0): with self.assertNumQueries(0):
self.assertEquals('test_value', self.kvs.get(self.key_factory('existing_field'))) self.assertEquals('test_value', self.kvs.get(self.key_factory('existing_field')))
...@@ -298,14 +302,14 @@ class StorageTestBase(object): ...@@ -298,14 +302,14 @@ class StorageTestBase(object):
def test_set_existing_field(self): def test_set_existing_field(self):
"Test that setting an existing field changes the value" "Test that setting an existing field changes the value"
with self.assertNumQueries(2): with self.assertNumQueries(1):
self.kvs.set(self.key_factory('existing_field'), 'new_value') self.kvs.set(self.key_factory('existing_field'), 'new_value')
self.assertEquals(1, self.storage_class.objects.all().count()) self.assertEquals(1, self.storage_class.objects.all().count())
self.assertEquals('new_value', json.loads(self.storage_class.objects.all()[0].value)) self.assertEquals('new_value', json.loads(self.storage_class.objects.all()[0].value))
def test_set_missing_field(self): def test_set_missing_field(self):
"Test that setting a new field changes the value" "Test that setting a new field changes the value"
with self.assertNumQueries(4): with self.assertNumQueries(1):
self.kvs.set(self.key_factory('missing_field'), 'new_value') self.kvs.set(self.key_factory('missing_field'), 'new_value')
self.assertEquals(2, self.storage_class.objects.all().count()) self.assertEquals(2, self.storage_class.objects.all().count())
self.assertEquals('old_value', json.loads(self.storage_class.objects.get(field_name='existing_field').value)) self.assertEquals('old_value', json.loads(self.storage_class.objects.get(field_name='existing_field').value))
...@@ -347,7 +351,7 @@ class StorageTestBase(object): ...@@ -347,7 +351,7 @@ class StorageTestBase(object):
# Each field is a separate row in the database, hence # Each field is a separate row in the database, hence
# a separate query # a separate query
with self.assertNumQueries(len(kv_dict)*3): with self.assertNumQueries(len(kv_dict)):
self.kvs.set_many(kv_dict) self.kvs.set_many(kv_dict)
for key in kv_dict: for key in kv_dict:
self.assertEquals(self.kvs.get(key), kv_dict[key]) self.assertEquals(self.kvs.get(key), kv_dict[key])
...@@ -355,8 +359,8 @@ class StorageTestBase(object): ...@@ -355,8 +359,8 @@ class StorageTestBase(object):
def test_set_many_failure(self): def test_set_many_failure(self):
"""Test that setting many regular fields with a DB error """ """Test that setting many regular fields with a DB error """
kv_dict = self.construct_kv_dict() kv_dict = self.construct_kv_dict()
with self.assertNumQueries(6): for key in kv_dict:
for key in kv_dict: with self.assertNumQueries(1):
self.kvs.set(key, 'test value') self.kvs.set(key, 'test value')
with patch('django.db.models.Model.save', side_effect=[None, DatabaseError]): with patch('django.db.models.Model.save', side_effect=[None, DatabaseError]):
...@@ -365,7 +369,6 @@ class StorageTestBase(object): ...@@ -365,7 +369,6 @@ class StorageTestBase(object):
exception = exception_context.exception exception = exception_context.exception
self.assertEquals(len(exception.saved_field_names), 1) self.assertEquals(len(exception.saved_field_names), 1)
self.assertEquals(exception.saved_field_names[0], 'existing_field')
class TestUserStateSummaryStorage(StorageTestBase, TestCase): class TestUserStateSummaryStorage(StorageTestBase, TestCase):
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment