Commit 1de87f91 by Nimisha Asthagiri Committed by GitHub

Merge pull request #14213 from edx/beryl/mongocrazy

Pre-load Definitions in Split Modulestore when accessing all blocks
parents 0d222b3e 99e6d539
...@@ -432,9 +432,11 @@ class SplitBulkWriteMixin(BulkOperationsMixin): ...@@ -432,9 +432,11 @@ class SplitBulkWriteMixin(BulkOperationsMixin):
if len(ids): if len(ids):
# Query the db for the definitions. # Query the db for the definitions.
defs_from_db = self.db_connection.get_definitions(list(ids), course_key) defs_from_db = list(self.db_connection.get_definitions(list(ids), course_key))
defs_dict = {d.get('_id'): d for d in defs_from_db}
# Add the retrieved definitions to the cache. # Add the retrieved definitions to the cache.
bulk_write_record.definitions.update({d.get('_id'): d for d in defs_from_db}) bulk_write_record.definitions_in_db.update(defs_dict.iterkeys())
bulk_write_record.definitions.update(defs_dict)
definitions.extend(defs_from_db) definitions.extend(defs_from_db)
return definitions return definitions
...@@ -772,11 +774,16 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase): ...@@ -772,11 +774,16 @@ class SplitMongoModuleStore(SplitBulkWriteMixin, ModuleStoreWriteBase):
Load the definitions into each block if lazy is in kwargs and is False; Load the definitions into each block if lazy is in kwargs and is False;
otherwise, do not load the definitions - they'll be loaded later when needed. otherwise, do not load the definitions - they'll be loaded later when needed.
""" """
lazy = kwargs.pop('lazy', True)
should_cache_items = not lazy
runtime = self._get_cache(course_entry.structure['_id']) runtime = self._get_cache(course_entry.structure['_id'])
if runtime is None: if runtime is None:
lazy = kwargs.pop('lazy', True)
runtime = self.create_runtime(course_entry, lazy) runtime = self.create_runtime(course_entry, lazy)
self._add_cache(course_entry.structure['_id'], runtime) self._add_cache(course_entry.structure['_id'], runtime)
should_cache_items = True
if should_cache_items:
self.cache_items(runtime, block_keys, course_entry.course_key, depth, lazy) self.cache_items(runtime, block_keys, course_entry.course_key, depth, lazy)
return [runtime.load_item(block_key, course_entry, **kwargs) for block_key in block_keys] return [runtime.load_item(block_key, course_entry, **kwargs) for block_key in block_keys]
......
...@@ -13,7 +13,8 @@ from xmodule.modulestore.xml_exporter import export_course_to_xml ...@@ -13,7 +13,8 @@ from xmodule.modulestore.xml_exporter import export_course_to_xml
from xmodule.modulestore.tests.factories import check_mongo_calls from xmodule.modulestore.tests.factories import check_mongo_calls
from xmodule.modulestore.tests.utils import ( from xmodule.modulestore.tests.utils import (
MixedModulestoreBuilder, VersioningModulestoreBuilder, MixedModulestoreBuilder, VersioningModulestoreBuilder,
MongoModulestoreBuilder, TEST_DATA_DIR MongoModulestoreBuilder, TEST_DATA_DIR,
MemoryCache,
) )
MIXED_OLD_MONGO_MODULESTORE_BUILDER = MixedModulestoreBuilder([('draft', MongoModulestoreBuilder())]) MIXED_OLD_MONGO_MODULESTORE_BUILDER = MixedModulestoreBuilder([('draft', MongoModulestoreBuilder())])
...@@ -87,6 +88,46 @@ class CountMongoCallsCourseTraversal(TestCase): ...@@ -87,6 +88,46 @@ class CountMongoCallsCourseTraversal(TestCase):
to the leaf nodes. to the leaf nodes.
""" """
def _traverse_blocks_in_course(self, course, access_all_block_fields):
"""
Traverses all the blocks in the given course.
If access_all_block_fields is True, also reads all the
xblock fields in each block in the course.
"""
all_blocks = []
stack = [course]
while stack:
curr_block = stack.pop()
all_blocks.append(curr_block)
if curr_block.has_children:
for block in reversed(curr_block.get_children()):
stack.append(block)
if access_all_block_fields:
# Read the fields on each block in order to ensure each block and its definition is loaded.
for xblock in all_blocks:
for __, field in xblock.fields.iteritems():
if field.is_set_on(xblock):
__ = field.read_from(xblock)
def _import_course(self, content_store, modulestore):
"""
Imports a course for testing.
Returns the course key.
"""
course_key = modulestore.make_course_key('a', 'course', 'course')
import_course_from_xml(
modulestore,
'test_user',
TEST_DATA_DIR,
source_dirs=['manual-testing-complete'],
static_content_store=content_store,
target_id=course_key,
create_if_not_present=True,
raise_on_failure=True,
)
return course_key
# Suppose you want to traverse a course - maybe accessing the fields of each XBlock in the course, # Suppose you want to traverse a course - maybe accessing the fields of each XBlock in the course,
# maybe not. What parameters should one use for get_course() in order to minimize the number of # maybe not. What parameters should one use for get_course() in order to minimize the number of
# mongo calls? The tests below both ensure that code changes don't increase the number of mongo calls # mongo calls? The tests below both ensure that code changes don't increase the number of mongo calls
...@@ -109,52 +150,43 @@ class CountMongoCallsCourseTraversal(TestCase): ...@@ -109,52 +150,43 @@ class CountMongoCallsCourseTraversal(TestCase):
# (if you'll eventually access all the fields and load all the definitions anyway). # (if you'll eventually access all the fields and load all the definitions anyway).
(MIXED_SPLIT_MODULESTORE_BUILDER, None, False, True, 4), (MIXED_SPLIT_MODULESTORE_BUILDER, None, False, True, 4),
(MIXED_SPLIT_MODULESTORE_BUILDER, None, True, True, 38), (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, True, 38),
(MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, True, 131), (MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, True, 38),
(MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, True, 38), (MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, True, 38),
(MIXED_SPLIT_MODULESTORE_BUILDER, None, False, False, 4), (MIXED_SPLIT_MODULESTORE_BUILDER, None, False, False, 4),
(MIXED_SPLIT_MODULESTORE_BUILDER, None, True, False, 4), (MIXED_SPLIT_MODULESTORE_BUILDER, None, True, False, 4),
# TODO: The call count below seems like a bug - should be 4? (MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, False, 4),
# Seems to be related to using self.lazy in CachingDescriptorSystem.get_module_data().
(MIXED_SPLIT_MODULESTORE_BUILDER, 0, False, False, 131),
(MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, False, 4) (MIXED_SPLIT_MODULESTORE_BUILDER, 0, True, False, 4)
) )
@ddt.unpack @ddt.unpack
def test_number_mongo_calls(self, store, depth, lazy, access_all_block_fields, num_mongo_calls): def test_number_mongo_calls(self, store_builder, depth, lazy, access_all_block_fields, num_mongo_calls):
with store.build() as (source_content, source_store): request_cache = MemoryCache()
with store_builder.build(request_cache=request_cache) as (content_store, modulestore):
source_course_key = source_store.make_course_key('a', 'course', 'course') course_key = self._import_course(content_store, modulestore)
# First, import a course.
import_course_from_xml(
source_store,
'test_user',
TEST_DATA_DIR,
source_dirs=['manual-testing-complete'],
static_content_store=source_content,
target_id=source_course_key,
create_if_not_present=True,
raise_on_failure=True,
)
# Course traversal modeled after the traversal done here: # Course traversal modeled after the traversal done here:
# lms/djangoapps/mobile_api/video_outlines/serializers.py:BlockOutline # lms/djangoapps/mobile_api/video_outlines/serializers.py:BlockOutline
# Starting at the root course block, do a breadth-first traversal using # Starting at the root course block, do a breadth-first traversal using
# get_children() to retrieve each block's children. # get_children() to retrieve each block's children.
with check_mongo_calls(num_mongo_calls): with check_mongo_calls(num_mongo_calls):
with source_store.bulk_operations(source_course_key): with modulestore.bulk_operations(course_key):
start_block = source_store.get_course(source_course_key, depth=depth, lazy=lazy) start_block = modulestore.get_course(course_key, depth=depth, lazy=lazy)
all_blocks = [] self._traverse_blocks_in_course(start_block, access_all_block_fields)
stack = [start_block]
while stack: @ddt.data(
curr_block = stack.pop() (MIXED_OLD_MONGO_MODULESTORE_BUILDER, 176),
all_blocks.append(curr_block) (MIXED_SPLIT_MODULESTORE_BUILDER, 5),
if curr_block.has_children: )
for block in reversed(curr_block.get_children()): @ddt.unpack
stack.append(block) def test_lazy_when_course_previously_cached(self, store_builder, num_mongo_calls):
request_cache = MemoryCache()
if access_all_block_fields: with store_builder.build(request_cache=request_cache) as (content_store, modulestore):
# Read the fields on each block in order to ensure each block and its definition is loaded. course_key = self._import_course(content_store, modulestore)
for xblock in all_blocks:
for __, field in xblock.fields.iteritems(): with check_mongo_calls(num_mongo_calls):
if field.is_set_on(xblock): with modulestore.bulk_operations(course_key):
__ = field.read_from(xblock) # assume the course was retrieved earlier
course = modulestore.get_course(course_key, depth=0, lazy=True)
# and then subsequently retrieved with the lazy and depth=None values
course = modulestore.get_item(course.location, depth=None, lazy=False)
self._traverse_blocks_in_course(course, access_all_block_fields=True)
"""
Tests for bulk operations in Split Modulestore.
"""
# pylint: disable=protected-access
import copy import copy
import ddt import ddt
import unittest import unittest
...@@ -444,6 +448,17 @@ class TestBulkWriteMixinFindMethods(TestBulkWriteMixin): ...@@ -444,6 +448,17 @@ class TestBulkWriteMixinFindMethods(TestBulkWriteMixin):
else: else:
self.assertNotIn(db_definition(_id), results) self.assertNotIn(db_definition(_id), results)
def test_get_definitions_doesnt_update_db(self):
test_ids = [1, 2]
db_definition = lambda _id: {'db': 'definition', '_id': _id}
db_definitions = [db_definition(_id) for _id in test_ids]
self.conn.get_definitions.return_value = db_definitions
self.bulk._begin_bulk_operation(self.course_key)
self.bulk.get_definitions(self.course_key, test_ids)
self.bulk._end_bulk_operation(self.course_key)
self.assertFalse(self.conn.insert_definition.called)
def test_no_bulk_find_structures_derived_from(self): def test_no_bulk_find_structures_derived_from(self):
ids = [Mock(name='id')] ids = [Mock(name='id')]
self.conn.find_structures_derived_from.return_value = [MagicMock(name='result')] self.conn.find_structures_derived_from.return_value = [MagicMock(name='result')]
......
...@@ -194,7 +194,7 @@ class MemoryCache(object): ...@@ -194,7 +194,7 @@ class MemoryCache(object):
the modulestore, and stores the data in a dictionary in memory. the modulestore, and stores the data in a dictionary in memory.
""" """
def __init__(self): def __init__(self):
self._data = {} self.data = {}
def get(self, key, default=None): def get(self, key, default=None):
""" """
...@@ -204,7 +204,7 @@ class MemoryCache(object): ...@@ -204,7 +204,7 @@ class MemoryCache(object):
key: The key to update. key: The key to update.
default: The value to return if the key hasn't been set previously. default: The value to return if the key hasn't been set previously.
""" """
return self._data.get(key, default) return self.data.get(key, default)
def set(self, key, value): def set(self, key, value):
""" """
...@@ -214,7 +214,7 @@ class MemoryCache(object): ...@@ -214,7 +214,7 @@ class MemoryCache(object):
key: The key to update. key: The key to update.
value: The value change the key to. value: The value change the key to.
""" """
self._data[key] = value self.data[key] = value
class MongoContentstoreBuilder(object): class MongoContentstoreBuilder(object):
...@@ -255,19 +255,19 @@ class StoreBuilderBase(object): ...@@ -255,19 +255,19 @@ class StoreBuilderBase(object):
""" """
contentstore = kwargs.pop('contentstore', None) contentstore = kwargs.pop('contentstore', None)
if not contentstore: if not contentstore:
with self.build_without_contentstore() as (contentstore, modulestore): with self.build_without_contentstore(**kwargs) as (contentstore, modulestore):
yield contentstore, modulestore yield contentstore, modulestore
else: else:
with self.build_with_contentstore(contentstore) as modulestore: with self.build_with_contentstore(contentstore, **kwargs) as modulestore:
yield modulestore yield modulestore
@contextmanager @contextmanager
def build_without_contentstore(self): def build_without_contentstore(self, **kwargs):
""" """
Build both the contentstore and the modulestore. Build both the contentstore and the modulestore.
""" """
with MongoContentstoreBuilder().build() as contentstore: with MongoContentstoreBuilder().build() as contentstore:
with self.build_with_contentstore(contentstore) as modulestore: with self.build_with_contentstore(contentstore, **kwargs) as modulestore:
yield contentstore, modulestore yield contentstore, modulestore
...@@ -276,7 +276,7 @@ class MongoModulestoreBuilder(StoreBuilderBase): ...@@ -276,7 +276,7 @@ class MongoModulestoreBuilder(StoreBuilderBase):
A builder class for a DraftModuleStore. A builder class for a DraftModuleStore.
""" """
@contextmanager @contextmanager
def build_with_contentstore(self, contentstore): def build_with_contentstore(self, contentstore, **kwargs):
""" """
A contextmanager that returns an isolated mongo modulestore, and then deletes A contextmanager that returns an isolated mongo modulestore, and then deletes
all of its data at the end of the context. all of its data at the end of the context.
...@@ -324,7 +324,7 @@ class VersioningModulestoreBuilder(StoreBuilderBase): ...@@ -324,7 +324,7 @@ class VersioningModulestoreBuilder(StoreBuilderBase):
A builder class for a VersioningModuleStore. A builder class for a VersioningModuleStore.
""" """
@contextmanager @contextmanager
def build_with_contentstore(self, contentstore): def build_with_contentstore(self, contentstore, **kwargs):
""" """
A contextmanager that returns an isolated versioning modulestore, and then deletes A contextmanager that returns an isolated versioning modulestore, and then deletes
all of its data at the end of the context. all of its data at the end of the context.
...@@ -347,6 +347,7 @@ class VersioningModulestoreBuilder(StoreBuilderBase): ...@@ -347,6 +347,7 @@ class VersioningModulestoreBuilder(StoreBuilderBase):
fs_root, fs_root,
render_template=repr, render_template=repr,
xblock_mixins=XBLOCK_MIXINS, xblock_mixins=XBLOCK_MIXINS,
**kwargs
) )
modulestore.ensure_indexes() modulestore.ensure_indexes()
...@@ -369,7 +370,7 @@ class XmlModulestoreBuilder(StoreBuilderBase): ...@@ -369,7 +370,7 @@ class XmlModulestoreBuilder(StoreBuilderBase):
""" """
# pylint: disable=unused-argument # pylint: disable=unused-argument
@contextmanager @contextmanager
def build_with_contentstore(self, contentstore=None, course_ids=None): def build_with_contentstore(self, contentstore=None, course_ids=None, **kwargs):
""" """
A contextmanager that returns an isolated xml modulestore A contextmanager that returns an isolated xml modulestore
...@@ -403,7 +404,7 @@ class MixedModulestoreBuilder(StoreBuilderBase): ...@@ -403,7 +404,7 @@ class MixedModulestoreBuilder(StoreBuilderBase):
self.mixed_modulestore = None self.mixed_modulestore = None
@contextmanager @contextmanager
def build_with_contentstore(self, contentstore): def build_with_contentstore(self, contentstore, **kwargs):
""" """
A contextmanager that returns a mixed modulestore built on top of modulestores A contextmanager that returns a mixed modulestore built on top of modulestores
generated by other builder classes. generated by other builder classes.
...@@ -414,7 +415,7 @@ class MixedModulestoreBuilder(StoreBuilderBase): ...@@ -414,7 +415,7 @@ class MixedModulestoreBuilder(StoreBuilderBase):
""" """
names, generators = zip(*self.store_builders) names, generators = zip(*self.store_builders)
with nested(*(gen.build_with_contentstore(contentstore) for gen in generators)) as modulestores: with nested(*(gen.build_with_contentstore(contentstore, **kwargs) for gen in generators)) as modulestores:
# Make the modulestore creation function just return the already-created modulestores # Make the modulestore creation function just return the already-created modulestores
store_iterator = iter(modulestores) store_iterator = iter(modulestores)
next_modulestore = lambda *args, **kwargs: store_iterator.next() next_modulestore = lambda *args, **kwargs: store_iterator.next()
......
...@@ -2,12 +2,14 @@ ...@@ -2,12 +2,14 @@
Tests for Blocks api.py Tests for Blocks api.py
""" """
import ddt
from django.test.client import RequestFactory from django.test.client import RequestFactory
from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache
from student.tests.factories import UserFactory from student.tests.factories import UserFactory
from xmodule.modulestore import ModuleStoreEnum from xmodule.modulestore import ModuleStoreEnum
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase
from xmodule.modulestore.tests.factories import SampleCourseFactory from xmodule.modulestore.tests.factories import SampleCourseFactory, check_mongo_calls
from ..api import get_blocks from ..api import get_blocks
...@@ -19,7 +21,8 @@ class TestGetBlocks(SharedModuleStoreTestCase): ...@@ -19,7 +21,8 @@ class TestGetBlocks(SharedModuleStoreTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
super(TestGetBlocks, cls).setUpClass() super(TestGetBlocks, cls).setUpClass()
cls.course = SampleCourseFactory.create() with cls.store.default_store(ModuleStoreEnum.Type.split):
cls.course = SampleCourseFactory.create()
# hide the html block # hide the html block
cls.html_block = cls.store.get_item(cls.course.id.make_usage_key('html', 'html_x1a_1')) cls.html_block = cls.store.get_item(cls.course.id.make_usage_key('html', 'html_x1a_1'))
...@@ -95,3 +98,47 @@ class TestGetBlocks(SharedModuleStoreTestCase): ...@@ -95,3 +98,47 @@ class TestGetBlocks(SharedModuleStoreTestCase):
self.assertEquals(len(blocks['blocks']), 3) self.assertEquals(len(blocks['blocks']), 3)
for block in blocks['blocks'].itervalues(): for block in blocks['blocks'].itervalues():
self.assertEqual(block['type'], 'problem') self.assertEqual(block['type'], 'problem')
@ddt.ddt
class TestGetBlocksQueryCounts(SharedModuleStoreTestCase):
"""
Tests query counts for the get_blocks function.
"""
def setUp(self):
super(TestGetBlocksQueryCounts, self).setUp()
self.user = UserFactory.create()
self.request = RequestFactory().get("/dummy")
self.request.user = self.user
def _create_course(self, store_type):
"""
Creates the sample course in the given store type.
"""
with self.store.default_store(store_type):
return SampleCourseFactory.create()
def _get_blocks(self, course, expected_mongo_queries):
"""
Verifies the number of expected queries when calling
get_blocks on the given course.
"""
with check_mongo_calls(expected_mongo_queries):
with self.assertNumQueries(2):
get_blocks(self.request, course.location, self.user)
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_query_counts_cached(self, store_type):
course = self._create_course(store_type)
self._get_blocks(course, expected_mongo_queries=0)
@ddt.data(
(ModuleStoreEnum.Type.mongo, 5),
(ModuleStoreEnum.Type.split, 3),
)
@ddt.unpack
def test_query_counts_uncached(self, store_type, expected_mongo_queries):
course = self._create_course(store_type)
clear_course_from_cache(course.id)
self._get_blocks(course, expected_mongo_queries)
...@@ -191,6 +191,48 @@ class TestJumpTo(ModuleStoreTestCase): ...@@ -191,6 +191,48 @@ class TestJumpTo(ModuleStoreTestCase):
@attr(shard=2) @attr(shard=2)
@ddt.ddt @ddt.ddt
class IndexQueryTestCase(ModuleStoreTestCase):
"""
Tests for query count.
"""
CREATE_USER = False
NUM_PROBLEMS = 20
@ddt.data(
(ModuleStoreEnum.Type.mongo, 8),
(ModuleStoreEnum.Type.split, 4),
)
@ddt.unpack
def test_index_query_counts(self, store_type, expected_query_count):
with self.store.default_store(store_type):
course = CourseFactory.create()
with self.store.bulk_operations(course.id):
chapter = ItemFactory.create(category='chapter', parent_location=course.location)
section = ItemFactory.create(category='sequential', parent_location=chapter.location)
vertical = ItemFactory.create(category='vertical', parent_location=section.location)
for _ in range(self.NUM_PROBLEMS):
ItemFactory.create(category='problem', parent_location=vertical.location)
password = 'test'
self.user = UserFactory(password=password)
self.client.login(username=self.user.username, password=password)
CourseEnrollment.enroll(self.user, course.id)
with check_mongo_calls(expected_query_count):
url = reverse(
'courseware_section',
kwargs={
'course_id': unicode(course.id),
'chapter': unicode(chapter.location.name),
'section': unicode(section.location.name),
}
)
response = self.client.get(url)
self.assertEqual(response.status_code, 200)
@attr(shard=2)
@ddt.ddt
class ViewsTestCase(ModuleStoreTestCase): class ViewsTestCase(ModuleStoreTestCase):
""" """
Tests for views.py methods. Tests for views.py methods.
......
...@@ -348,7 +348,7 @@ class CoursewareIndex(View): ...@@ -348,7 +348,7 @@ class CoursewareIndex(View):
sets up the runtime, which binds the request user to the section. sets up the runtime, which binds the request user to the section.
""" """
# Pre-fetch all descendant data # Pre-fetch all descendant data
self.section = modulestore().get_item(self.section.location, depth=None) self.section = modulestore().get_item(self.section.location, depth=None, lazy=False)
self.field_data_cache.add_descriptor_descendents(self.section, depth=None) self.field_data_cache.add_descriptor_descendents(self.section, depth=None)
# Bind section to user # Bind section to user
......
...@@ -158,43 +158,45 @@ def _update_subsection_grades( ...@@ -158,43 +158,45 @@ def _update_subsection_grades(
that those subsection grades were updated. that those subsection grades were updated.
""" """
student = User.objects.get(id=user_id) student = User.objects.get(id=user_id)
course_structure = get_course_blocks(student, modulestore().make_course_usage_key(course_key)) store = modulestore()
subsections_to_update = course_structure.get_transformer_block_field( with store.bulk_operations(course_key):
scored_block_usage_key, course_structure = get_course_blocks(student, store.make_course_usage_key(course_key))
GradesTransformer, subsections_to_update = course_structure.get_transformer_block_field(
'subsections', scored_block_usage_key,
set(), GradesTransformer,
) 'subsections',
set(),
course = modulestore().get_course(course_key, depth=0)
subsection_grade_factory = SubsectionGradeFactory(student, course, course_structure)
try:
for subsection_usage_key in subsections_to_update:
if subsection_usage_key in course_structure:
subsection_grade = subsection_grade_factory.update(
course_structure[subsection_usage_key],
only_if_higher,
)
SUBSECTION_SCORE_CHANGED.send(
sender=recalculate_subsection_grade,
course=course,
course_structure=course_structure,
user=student,
subsection_grade=subsection_grade,
)
except DatabaseError as exc:
raise _retry_recalculate_subsection_grade(
user_id,
course_id,
usage_id,
only_if_higher,
expected_modified_time,
score_deleted,
exc,
) )
course = store.get_course(course_key, depth=0)
subsection_grade_factory = SubsectionGradeFactory(student, course, course_structure)
try:
for subsection_usage_key in subsections_to_update:
if subsection_usage_key in course_structure:
subsection_grade = subsection_grade_factory.update(
course_structure[subsection_usage_key],
only_if_higher,
)
SUBSECTION_SCORE_CHANGED.send(
sender=recalculate_subsection_grade,
course=course,
course_structure=course_structure,
user=student,
subsection_grade=subsection_grade,
)
except DatabaseError as exc:
raise _retry_recalculate_subsection_grade(
user_id,
course_id,
usage_id,
only_if_higher,
expected_modified_time,
score_deleted,
exc,
)
def _retry_recalculate_subsection_grade( def _retry_recalculate_subsection_grade(
user_id, user_id,
......
...@@ -121,16 +121,17 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -121,16 +121,17 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self.assertTrue(mock_subsection_signal.called) self.assertTrue(mock_subsection_signal.called)
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 1), (ModuleStoreEnum.Type.mongo, 1, 23),
(ModuleStoreEnum.Type.split, 0), (ModuleStoreEnum.Type.split, 3, 22),
) )
@ddt.unpack @ddt.unpack
def test_subsection_grade_updated(self, default_store, added_queries): def test_subsection_grade_updated(self, default_store, num_mongo_calls, num_sql_calls):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
with check_mongo_calls(2) and self.assertNumQueries(22 + added_queries): with check_mongo_calls(num_mongo_calls):
self._apply_recalculate_subsection_grade() with self.assertNumQueries(num_sql_calls):
self._apply_recalculate_subsection_grade()
@patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send') @patch('lms.djangoapps.grades.signals.signals.SUBSECTION_SCORE_CHANGED.send')
def test_other_inaccessible_subsection(self, mock_subsection_signal): def test_other_inaccessible_subsection(self, mock_subsection_signal):
...@@ -166,27 +167,38 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -166,27 +167,38 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
self._apply_recalculate_subsection_grade() self._apply_recalculate_subsection_grade()
self.assertEquals(mock_block_structure_create.call_count, 1) self.assertEquals(mock_block_structure_create.call_count, 1)
# TODO (TNL-6225) Fix the number of SQL queries so they
# don't grow linearly with the number of sequentials.
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 1), (ModuleStoreEnum.Type.mongo, 1, 46),
(ModuleStoreEnum.Type.split, 0), (ModuleStoreEnum.Type.split, 3, 45),
) )
@ddt.unpack @ddt.unpack
def test_query_count_does_not_change_with_more_problems(self, default_store, added_queries): def test_query_count_does_not_change_with_more_content(self, default_store, num_mongo_calls, num_sql_calls):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) self.assertTrue(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem2')
ItemFactory.create(parent=self.sequential, category='problem', display_name='problem3') num_problems = 10
with check_mongo_calls(2) and self.assertNumQueries(22 + added_queries): for _ in range(num_problems):
self._apply_recalculate_subsection_grade() ItemFactory.create(parent=self.sequential, category='problem')
num_sequentials = 10
for _ in range(num_sequentials):
ItemFactory.create(parent=self.chapter, category='sequential')
with check_mongo_calls(num_mongo_calls):
with self.assertNumQueries(num_sql_calls):
self._apply_recalculate_subsection_grade()
@ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split) @ddt.data(ModuleStoreEnum.Type.mongo, ModuleStoreEnum.Type.split)
def test_subsection_grades_not_enabled_on_course(self, default_store): def test_subsection_grades_not_enabled_on_course(self, default_store):
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course(enable_subsection_grades=False) self.set_up_course(enable_subsection_grades=False)
self.assertFalse(PersistentGradesEnabledFlag.feature_enabled(self.course.id)) self.assertFalse(PersistentGradesEnabledFlag.feature_enabled(self.course.id))
with check_mongo_calls(2) and self.assertNumQueries(0): with check_mongo_calls(0):
self._apply_recalculate_subsection_grade() with self.assertNumQueries(0):
self._apply_recalculate_subsection_grade()
@skip("Pending completion of TNL-5089") @skip("Pending completion of TNL-5089")
@ddt.data( @ddt.data(
...@@ -200,8 +212,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase): ...@@ -200,8 +212,9 @@ class RecalculateSubsectionGradeTest(ModuleStoreTestCase):
PersistentGradesEnabledFlag.objects.create(enabled=feature_flag) PersistentGradesEnabledFlag.objects.create(enabled=feature_flag)
with self.store.default_store(default_store): with self.store.default_store(default_store):
self.set_up_course() self.set_up_course()
with check_mongo_calls(0) and self.assertNumQueries(3 if feature_flag else 2): with check_mongo_calls(0):
self._apply_recalculate_subsection_grade() with self.assertNumQueries(3 if feature_flag else 2):
self._apply_recalculate_subsection_grade()
@patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry') @patch('lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2.retry')
@patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update') @patch('lms.djangoapps.grades.new.subsection_grade.SubsectionGradeFactory.update')
......
...@@ -17,7 +17,7 @@ from xmodule.modulestore.tests.factories import check_mongo_calls ...@@ -17,7 +17,7 @@ from xmodule.modulestore.tests.factories import check_mongo_calls
from lms.djangoapps.course_blocks.api import get_course_blocks from lms.djangoapps.course_blocks.api import get_course_blocks
from lms.djangoapps.course_blocks.transformers.tests.helpers import CourseStructureTestCase from lms.djangoapps.course_blocks.transformers.tests.helpers import CourseStructureTestCase
from openedx.core.djangoapps.content.block_structure.api import get_cache from openedx.core.djangoapps.content.block_structure.api import clear_course_from_cache
from ..transformer import GradesTransformer from ..transformer import GradesTransformer
...@@ -390,6 +390,7 @@ class GradesTransformerTestCase(CourseStructureTestCase): ...@@ -390,6 +390,7 @@ class GradesTransformerTestCase(CourseStructureTestCase):
) )
@ddt.ddt
class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModuleStoreTestCase): class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModuleStoreTestCase):
""" """
Test mongo usage in GradesTransformer. Test mongo usage in GradesTransformer.
...@@ -403,7 +404,12 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul ...@@ -403,7 +404,12 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul
self.student = UserFactory.create(is_staff=False, username=u'test_student', password=password) self.student = UserFactory.create(is_staff=False, username=u'test_student', password=password)
self.client.login(username=self.student.username, password=password) self.client.login(username=self.student.username, password=password)
def test_modulestore_performance(self): @ddt.data(
(ModuleStoreEnum.Type.split, 3),
(ModuleStoreEnum.Type.mongo, 2),
)
@ddt.unpack
def test_modulestore_performance(self, store_type, expected_mongo_queries):
""" """
Test that a constant number of mongo calls are made regardless of how Test that a constant number of mongo calls are made regardless of how
many grade-related blocks are in the course. many grade-related blocks are in the course.
...@@ -437,7 +443,8 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul ...@@ -437,7 +443,8 @@ class MultiProblemModulestoreAccessTestCase(CourseStructureTestCase, SharedModul
</problem>'''.format(number=problem_number), </problem>'''.format(number=problem_number),
} }
) )
blocks = self.build_course(course) with self.store.default_store(store_type):
get_cache().clear() blocks = self.build_course(course)
with check_mongo_calls(2): clear_course_from_cache(blocks[u'course'].id)
with check_mongo_calls(expected_mongo_queries):
get_course_blocks(self.student, blocks[u'course'].location, self.transformers) get_course_blocks(self.student, blocks[u'course'].location, self.transformers)
...@@ -103,11 +103,11 @@ class XBlockCacheTaskTests(BookmarksTestsBase): ...@@ -103,11 +103,11 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
} }
@ddt.data( @ddt.data(
(ModuleStoreEnum.Type.mongo, 2, 2, 3), (ModuleStoreEnum.Type.mongo, 2, 2, 4),
(ModuleStoreEnum.Type.mongo, 4, 2, 3), (ModuleStoreEnum.Type.mongo, 4, 2, 4),
(ModuleStoreEnum.Type.mongo, 2, 3, 4), (ModuleStoreEnum.Type.mongo, 2, 3, 5),
(ModuleStoreEnum.Type.mongo, 4, 3, 4), (ModuleStoreEnum.Type.mongo, 4, 3, 5),
(ModuleStoreEnum.Type.mongo, 2, 4, 5), (ModuleStoreEnum.Type.mongo, 2, 4, 6),
# (ModuleStoreEnum.Type.mongo, 4, 4, 6), Too slow. # (ModuleStoreEnum.Type.mongo, 4, 4, 6), Too slow.
(ModuleStoreEnum.Type.split, 2, 2, 3), (ModuleStoreEnum.Type.split, 2, 2, 3),
(ModuleStoreEnum.Type.split, 4, 2, 3), (ModuleStoreEnum.Type.split, 4, 2, 3),
...@@ -119,6 +119,9 @@ class XBlockCacheTaskTests(BookmarksTestsBase): ...@@ -119,6 +119,9 @@ class XBlockCacheTaskTests(BookmarksTestsBase):
course = self.create_course_with_blocks(children_per_block, depth, store_type) course = self.create_course_with_blocks(children_per_block, depth, store_type)
# clear cache to get consistent query counts
self.clear_caches()
with check_mongo_calls(expected_mongo_calls): with check_mongo_calls(expected_mongo_calls):
blocks_data = _calculate_course_xblocks_data(course.id) blocks_data = _calculate_course_xblocks_data(course.id)
self.assertGreater(len(blocks_data), children_per_block ** depth) self.assertGreater(len(blocks_data), children_per_block ** depth)
......
...@@ -53,7 +53,7 @@ class BlockStructureFactory(object): ...@@ -53,7 +53,7 @@ class BlockStructureFactory(object):
block_structure._add_relation(xblock.location, child.location) # pylint: disable=protected-access block_structure._add_relation(xblock.location, child.location) # pylint: disable=protected-access
build_block_structure(child) build_block_structure(child)
root_xblock = modulestore.get_item(root_block_usage_key, depth=None) root_xblock = modulestore.get_item(root_block_usage_key, depth=None, lazy=False)
build_block_structure(root_xblock) build_block_structure(root_xblock)
return block_structure return block_structure
......
...@@ -55,7 +55,7 @@ class MockModulestore(object): ...@@ -55,7 +55,7 @@ class MockModulestore(object):
""" """
self.blocks = blocks self.blocks = blocks
def get_item(self, block_key, depth=None): # pylint: disable=unused-argument def get_item(self, block_key, depth=None, lazy=False): # pylint: disable=unused-argument
""" """
Returns the mock XBlock (MockXBlock) associated with the Returns the mock XBlock (MockXBlock) associated with the
given block_key. given block_key.
......
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