-
Notifications
You must be signed in to change notification settings - Fork 4.3k
feat: cache course index queries #29107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5909102
ab74f18
134959c
87e51f1
ae3328f
04f8cf7
ffa32b2
861cc46
80a0e88
6901f7a
0f681d1
e2e558c
b8f8f7a
eb71c84
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,7 +25,8 @@ | |
| from xmodule.modulestore import BlockData | ||
| from xmodule.modulestore.split_mongo import BlockKey | ||
| from xmodule.mongo_utils import connect_to_mongodb, create_collection_index | ||
|
|
||
| from openedx.core.lib.cache_utils import request_cached | ||
| from edx_django_utils.cache import RequestCache | ||
|
|
||
| log = logging.getLogger(__name__) | ||
|
|
||
|
|
@@ -261,6 +262,9 @@ def __init__( | |
| # only before returning. Also makes pymongo report write errors. | ||
| kwargs['w'] = 1 | ||
|
|
||
| #make sure the course index cache is fresh. | ||
| RequestCache(namespace="course_index_cache").clear() | ||
|
|
||
| self.database = connect_to_mongodb( | ||
| db, host, | ||
| port=port, tz_aware=tz_aware, user=user, password=password, | ||
|
|
@@ -532,6 +536,7 @@ def close_connections(self): | |
| """ | ||
| Closes any open connections to the underlying databases | ||
| """ | ||
| RequestCache(namespace="course_index_cache").clear() | ||
| self.database.client.close() | ||
|
|
||
| def _drop_database(self, database=True, collections=True, connections=True): | ||
|
|
@@ -546,6 +551,7 @@ def _drop_database(self, database=True, collections=True, connections=True): | |
|
|
||
| If connections is True, then close the connection to the database as well. | ||
| """ | ||
| RequestCache(namespace="course_index_cache").clear() | ||
| connection = self.database.client | ||
|
|
||
| if database: | ||
|
|
@@ -571,7 +577,13 @@ class DjangoFlexPersistenceBackend(MongoPersistenceBackend): | |
|
|
||
| # Structures and definitions are only supported in MongoDB for now. | ||
| # Course indexes are read from MySQL and written to both MongoDB and MySQL | ||
|
|
||
| # Course indexes are cached within the process using their key and ignore_case atrributes as keys. | ||
| # This method is request cached. The keys to the cache are the arguements to the method. | ||
| # The `self` arguement is discarded as a key using an isinstance check. | ||
| # This is because the DjangoFlexPersistenceBackend could be different in reference to the same course key. | ||
| @request_cached( | ||
| "course_index_cache", | ||
| arg_map_function=lambda arg: str(arg) if not isinstance(arg, DjangoFlexPersistenceBackend) else "") | ||
| def get_course_index(self, key, ignore_case=False): | ||
| """ | ||
| Get the course_index from the persistence mechanism whose id is the given key | ||
|
|
@@ -650,6 +662,10 @@ def insert_course_index(self, course_index, course_context=None): # pylint: dis | |
| """ | ||
| Create the course_index in the db | ||
| """ | ||
| # clear the whole course_index request cache, required for sucessfully cloning a course. | ||
| # This is a relatively large hammer for the problem, but we mostly only use one course at a time. | ||
| RequestCache(namespace="course_index_cache").clear() | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Optional: FWIW, you might want to add a comment here explaining that this is a bigger hammer than is really necessary (since this code invalidates all course indexes anytime a particular course is modified). FWIW, given that we almost always just work with one course at a time in Studio (where any writes would happen), I think this is a good tradeoff in favor of implementation simplicity. But it might not be obvious to someone reading the code later. |
||
|
|
||
| course_index['last_update'] = datetime.datetime.now(pytz.utc) | ||
| new_index = SplitModulestoreCourseIndex(**SplitModulestoreCourseIndex.fields_from_v1_schema(course_index)) | ||
| new_index.save() | ||
|
|
@@ -669,6 +685,7 @@ def update_course_index(self, course_index, from_index=None, course_context=None | |
| # "last_update not only tells us when this course was last updated but also helps prevent collisions" | ||
| # This code is just copying the behavior of the existing MongoPersistenceBackend | ||
| # See https://github.com/edx/edx-platform/pull/5200 for context | ||
| RequestCache(namespace="course_index_cache").clear() | ||
| course_index['last_update'] = datetime.datetime.now(pytz.utc) | ||
| # Find the SplitModulestoreCourseIndex entry that we'll be updating: | ||
| try: | ||
|
|
@@ -730,6 +747,7 @@ def delete_course_index(self, course_key): | |
| """ | ||
| Delete the course_index from the persistence mechanism whose id is the given course_index | ||
| """ | ||
| RequestCache(namespace="course_index_cache").clear() | ||
| SplitModulestoreCourseIndex.objects.filter(course_id=course_key).delete() | ||
| # TEMP: Also write to MongoDB, so we can switch back to using it if this new MySQL version doesn't work well: | ||
| super().delete_course_index(course_key) | ||
|
|
@@ -738,6 +756,7 @@ def _drop_database(self, database=True, collections=True, connections=True): | |
| """ | ||
| Reset data for testing. | ||
| """ | ||
| RequestCache(namespace="course_index_cache").clear() | ||
| try: | ||
| SplitModulestoreCourseIndex.objects.all().delete() | ||
| except TransactionManagementError as err: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -275,7 +275,7 @@ def my_test(self): | |
| """ | ||
| MODULESTORE = functools.partial(mixed_store_config, mkdtemp_clean(), {}) | ||
| CONTENTSTORE = functools.partial(contentstore_config) | ||
| ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache'] | ||
| ENABLED_CACHES = ['default', 'mongo_metadata_inheritance', 'loc_cache', 'course_index_cache'] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Question: Did adding the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, it did remove some of the flakiness around the ordering of test cases and the number of calls they made. (ex: if test a runs before b, in run 1, test a will make 1 the call to course index, whereas if test b ran before a, test b will make 1 more call) |
||
|
|
||
| # List of modulestore signals enabled for this test. Defaults to an empty | ||
| # list. The list of signals available is found on the SignalHandler class, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a comment explaining why you're doing an
isinstancecheck here. Or you can ditch the decorator and manually create the key and access the request cache in the method–the decorator is a nice convenience, but I forgot the weirdness that happens when you use it on a method and you're gettingselfpassed into it.