Skip to content

Commit d7825b2

Browse files
committed
refactor: convert global modulestore to request-cached
Having a global modulestore reference is a holdover from the early days, when the XML modulestore would be extremely expensive to initialize. That is no longer the case, and we should eliminate the global here to reduce the chances of multi-threading bugs that might mutate the global. Unfortunately, a lot of code currently assumes that calls to the modulestore() function are essentially free, instead of the 1-2 ms it is without caching. There are cases where this may be called thousands of times deep in loops somewhere while doing course content traversal, so it's risky to eliminate the caching behavior altogether. The thought here is that we'll tie to the RequestCache, which should at least not be shared across users/threads. Since new instances of the modulestore will not have cached entries for a particular course, it means that some query count tests have to be adjusted upward.
1 parent 8201e57 commit d7825b2

4 files changed

Lines changed: 27 additions & 30 deletions

File tree

lms/djangoapps/course_api/blocks/transformers/tests/test_milestones.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ def setUp(self):
4444
CourseEnrollmentFactory.create(user=self.user, course_id=self.course.id, is_active=True)
4545

4646
self.transformers = BlockStructureTransformers([self.TRANSFORMER_CLASS_TO_TEST(False)])
47+
self.clear_caches()
4748

4849
def setup_gated_section(self, gated_block, gating_block):
4950
"""
@@ -184,7 +185,7 @@ def test_gated(self, gated_block_ref, gating_block_ref, expected_blocks_before_c
184185
self.user,
185186
)
186187

187-
with self.assertNumQueries(4):
188+
with self.assertNumQueries(5):
188189
self.get_blocks_and_check_against_expected(self.user, self.ALL_BLOCKS_EXCEPT_SPECIAL)
189190

190191
def test_staff_access(self):

lms/djangoapps/grades/tests/test_course_grade_factory.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -289,7 +289,7 @@ def test_grading_exception(self, mock_course_grade):
289289
else mock_course_grade.return_value
290290
for student in self.students
291291
]
292-
with self.assertNumQueries(20):
292+
with self.assertNumQueries(21):
293293
all_course_grades, all_errors = self._course_grades_and_errors_for(self.course, self.students)
294294
assert {student: str(all_errors[student]) for student in all_errors} == {
295295
student3: 'Error for student3.',

openedx/core/djangoapps/user_api/accounts/tests/test_utils.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from openedx.core.djangolib.testing.utils import skip_unless_lms
1212
from common.djangoapps.student.models import CourseEnrollment
1313
from common.djangoapps.student.tests.factories import UserFactory
14-
from xmodule.modulestore.tests.django_utils import SharedModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
14+
from xmodule.modulestore.tests.django_utils import ModuleStoreTestCase # lint-amnesty, pylint: disable=wrong-import-order
1515
from xmodule.modulestore.tests.factories import CourseFactory, BlockFactory # lint-amnesty, pylint: disable=wrong-import-order
1616

1717
from ..utils import format_social_link, validate_social_link
@@ -63,7 +63,7 @@ def test_social_link_input(self, platform_name, link_input, formatted_link_expec
6363

6464

6565
@ddt.ddt
66-
class CompletionUtilsTestCase(SharedModuleStoreTestCase, CompletionWaffleTestMixin, TestCase):
66+
class CompletionUtilsTestCase(ModuleStoreTestCase, CompletionWaffleTestMixin, TestCase):
6767
"""
6868
Test completion utility functions
6969
"""

xmodule/modulestore/django.py

Lines changed: 22 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
import django.dispatch # lint-amnesty, pylint: disable=wrong-import-position
2525
import django.utils # lint-amnesty, pylint: disable=wrong-import-position
2626
from django.utils.translation import get_language, to_locale # lint-amnesty, pylint: disable=wrong-import-position
27-
from edx_django_utils.cache import DEFAULT_REQUEST_CACHE # lint-amnesty, pylint: disable=wrong-import-position
27+
from edx_django_utils.cache import RequestCache # lint-amnesty, pylint: disable=wrong-import-position
2828

29+
from openedx.core.lib.cache_utils import request_cached # pylint: disable=wrong-import-position
2930
from xmodule.contentstore.django import contentstore # lint-amnesty, pylint: disable=wrong-import-position
3031
from xmodule.modulestore.draft_and_published import BranchSettingMixin # lint-amnesty, pylint: disable=wrong-import-position
3132
from xmodule.modulestore.mixed import MixedModuleStore # lint-amnesty, pylint: disable=wrong-import-position
@@ -55,6 +56,8 @@
5556
log = logging.getLogger(__name__)
5657
ASSET_IGNORE_REGEX = getattr(settings, "ASSET_IGNORE_REGEX", r"(^\._.*$)|(^\.DS_Store$)|(^.*~$)")
5758

59+
MODULESTORE_REQUEST_CACHE_NAMESPACE = "modulestore"
60+
5861

5962
class SwitchedSignal(django.dispatch.Signal):
6063
"""
@@ -275,7 +278,7 @@ def create_modulestore_instance(
275278
if key in _options and isinstance(_options[key], str):
276279
_options[key] = load_function(_options[key])
277280

278-
request_cache = DEFAULT_REQUEST_CACHE
281+
request_cache = RequestCache(MODULESTORE_REQUEST_CACHE_NAMESPACE)
279282

280283
try:
281284
metadata_inheritance_cache = caches['mongo_metadata_inheritance']
@@ -324,33 +327,27 @@ def fetch_disabled_xblock_types():
324327
)
325328

326329

327-
# A singleton instance of the Mixed Modulestore
328-
_MIXED_MODULESTORE = None
329-
330-
330+
@request_cached(MODULESTORE_REQUEST_CACHE_NAMESPACE)
331331
def modulestore():
332332
"""
333333
Returns the Mixed modulestore
334334
"""
335-
global _MIXED_MODULESTORE # pylint: disable=global-statement
336-
if _MIXED_MODULESTORE is None:
337-
_MIXED_MODULESTORE = create_modulestore_instance(
338-
settings.MODULESTORE['default']['ENGINE'],
339-
contentstore(),
340-
settings.MODULESTORE['default'].get('DOC_STORE_CONFIG', {}),
341-
settings.MODULESTORE['default'].get('OPTIONS', {})
342-
)
343-
344-
if settings.FEATURES.get('CUSTOM_COURSES_EDX'):
345-
# TODO: This import prevents a circular import issue, but is
346-
# symptomatic of a lib having a dependency on code in lms. This
347-
# should be updated to have a setting that enumerates modulestore
348-
# wrappers and then uses that setting to wrap the modulestore in
349-
# appropriate wrappers depending on enabled features.
350-
from lms.djangoapps.ccx.modulestore import CCXModulestoreWrapper
351-
_MIXED_MODULESTORE = CCXModulestoreWrapper(_MIXED_MODULESTORE)
335+
mixed_modulestore = create_modulestore_instance(
336+
settings.MODULESTORE['default']['ENGINE'],
337+
contentstore(),
338+
settings.MODULESTORE['default'].get('DOC_STORE_CONFIG', {}),
339+
settings.MODULESTORE['default'].get('OPTIONS', {})
340+
)
341+
if settings.FEATURES.get('CUSTOM_COURSES_EDX'):
342+
# TODO: This import prevents a circular import issue, but is
343+
# symptomatic of a lib having a dependency on code in lms. This
344+
# should be updated to have a setting that enumerates modulestore
345+
# wrappers and then uses that setting to wrap the modulestore in
346+
# appropriate wrappers depending on enabled features.
347+
from lms.djangoapps.ccx.modulestore import CCXModulestoreWrapper
348+
mixed_modulestore = CCXModulestoreWrapper(mixed_modulestore)
352349

353-
return _MIXED_MODULESTORE
350+
return mixed_modulestore
354351

355352

356353
def clear_existing_modulestores():
@@ -360,8 +357,7 @@ def clear_existing_modulestores():
360357
361358
This is useful for flushing state between unit tests.
362359
"""
363-
global _MIXED_MODULESTORE # pylint: disable=global-statement
364-
_MIXED_MODULESTORE = None
360+
RequestCache(MODULESTORE_REQUEST_CACHE_NAMESPACE).clear()
365361

366362

367363
class XBlockI18nService:

0 commit comments

Comments
 (0)