Skip to content

Pre-load Definitions in Split Modulestore when accessing all blocks - #14213

Merged
nasthagiri merged 1 commit into
masterfrom
beryl/mongocrazy
Jan 3, 2017
Merged

Pre-load Definitions in Split Modulestore when accessing all blocks#14213
nasthagiri merged 1 commit into
masterfrom
beryl/mongocrazy

Conversation

@nasthagiri

@nasthagiri nasthagiri commented Dec 23, 2016

Copy link
Copy Markdown
Contributor

TNL-6222

Description

TLDR;

This PR addresses the excessive calls to modulestore.definitions find_one and modulestore.active_versions find_one in lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2. It also reduces the number of calls to modulestore.definitions find_one in courseware.views.index:CoursewareIndex.get.

Details

When monitoring the staged rollout of Persistent Grades, we noticed that the celery task for updating grades was making unexpected number of mongo queries - some even up to 2,000 mongo queries!

We were able to reduce the extra calls to modulestore.active_versions find_one by surrounding the task with a transactional bulk_operations context manager. Those grew linearly by the number of subsections in the course.

However, there were still excessive calls to modulestore.definitions find_one (growing linearly by the number of problems in the course). After further testing, we narrowed down the issue to: (1) cache misses on the Block Structure and (2) Split Modulestore courses.

For Block Structures, we expected all course blocks to be pre-loaded in a few (4) queries since we loaded the course with depth=None. Apparently, there is another parameter that we should have also set: lazy=False. So this PR corrects the Block Structure's call to the modulestore to pre-load all definitions since they are needed during the "collect" phase.

Additionally, when investigating this issue, we noticed that loading the Sequential on the Courseware page had the same issue (since lazy=False was not specified along with depth=None, although definitions of all descendant blocks of the Sequential need to be accessed). So this PR updates Courseware to do so, and adds query count tests to validate the change.

Finally, the implementation of the Split Modulestore had to be updated to:

  1. Support pre-loading of Definitions with the lazy=False parameter even after the runtime for the course had been previously cached. This allows callers to be agnostic to whether a course is pre-fetched without the depth=None, lazy=False parameters, but then later accessed with those parameters.

  2. Fix a bug in get_definitions so the bulk_write_record.definitions_in_db object is also updated. Otherwise, the Mongo collection was mistakenly "Inserted" with pre-fetched records at the end of the bulk-operation - since the code incorrectly calculated that those definitions didn't pre-exist in the database.

Note: There are still linearly growing SQL queries that will be addressed by a separate ticket: https://openedx.atlassian.net/browse/TNL-6225

Reviewers

If you've been tagged for review, please check your corresponding box once you've given the 👍.

FYI: @jcdyer

Post-review

  • Rebase and squash commits

@nasthagiri
nasthagiri force-pushed the beryl/mongocrazy branch 5 times, most recently from 914188f to ce2e699 Compare December 27, 2016 18:17
@estute

estute commented Dec 27, 2016

Copy link
Copy Markdown
Contributor

jenkins run bokchoy

@nasthagiri

Copy link
Copy Markdown
Contributor Author

@nasthagiri
nasthagiri force-pushed the beryl/mongocrazy branch 2 times, most recently from 7420466 to 8df9851 Compare December 28, 2016 14:06
@nasthagiri nasthagiri changed the title Fix Split Modulestore so definitions are pre-loaded when depth=None Pre-load Definitions in Split Modulestore when accessing all blocks Dec 28, 2016
@ormsbee

ormsbee commented Dec 28, 2016

Copy link
Copy Markdown
Contributor

Just so I'm clear on this -- this addresses the excessive calls to modulestore.active_versions find_one in lms.djangoapps.grades.tasks.recalculate_subsection_grade_v2 ?

@nasthagiri

Copy link
Copy Markdown
Contributor Author

@ormsbee Yes. Sorry if that wasn't clear. I'll update the description.

@nasthagiri nasthagiri Dec 28, 2016

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: This is a bug fix (No 2 in the description). Otherwise, the Mongo collection was mistakenly "Inserted" with pre-fetched records at the end of the bulk-operation - since the code incorrectly calculated that those definitions didn't pre-exist in the database.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: This change allows callers to be agnostic to whether a course was previously prefetched into the runtime. This is No 2 in the description.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you explain this more please? Why would we ever not want to cache_items? Or does cache_items imply pre-fetch here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good question. My change was an iterative, conservative change - changing the behavior only when lazy=False.

Now that you ask the question, I do wonder why we aren't always caching? For some reason, the previous code chose to cache only when the runtime for the course wasn't cached. Since there are many calls to _load_items from within split.py, there may be a good reason. (?). Here's the precondition listed in cache_items:

Handles caching of items once inheritance and any other one time per course per fetch operations are done.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: This TODO was addressed by passing a request_cache object to the modulestore in these tests.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: I have verified that this test breaks on master (without the changes in this PR).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: Renaming from _data to data allows this stub cache object to be used as a request_cache in the modulestore.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just curious how the name affects how it's passed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because Split Modulestore expects the given request_cache object to store its dict in a data field. For example: https://github.com/edx/edx-platform/blob/master/common/lib/xmodule/xmodule/modulestore/split_mongo/split.py#L792

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: Adding kwargs parameters here allows test code to specify optional parameters to the Modulestore, including a request_cache for this PR.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some questions:

  1. I see it being passed through the layers with **kwargs, but where does the request_cache actually get popped off and read?
  2. Is it necessary to pass an explicitly created request cache all the way down? Could the the test case inherit from CacheIsolationTestCase, do its setup, and then test in uncached vs. cached states?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, thinking on item 2, are we avoiding it because of the Django dependency...?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes - exactly. This test runs just under nosetools on Jenkins.
And as we both know, running with python manage.py adds much more overhead. So unless absolutely necessary, I thought about keeping it as a non-django test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: The query counts here went up by 1 only because the cache is now cleared before measuring the mongo calls. Otherwise, it was resulting in discrepancies between CMS and LMS runtimes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to reviewers: On master, instead of 4, this results in 23 mongo calls for Split and grows linearly with the number of blocks in the course.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This. Is. Awesome. (I'm on vacation, so I won't be giving a broader review.)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice!

@nasthagiri nasthagiri Dec 28, 2016

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note to @sanfordstudent: When the query context manager is used in a conjunction, only the latter query count is checked. Not the former. So all the check_mongo_calls in this file were not being called. A surprise for me as well. :(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow, that's good to know.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎆

Comment thread lms/djangoapps/grades/tasks.py Outdated

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

store here instead of modulestore()?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch. Thanks.

@ormsbee

ormsbee commented Dec 28, 2016

Copy link
Copy Markdown
Contributor

Tiny nitpick, but 👍 on the PR either way. Thank you for the reviewer's notes -- they were most helpful.

@sanfordstudent

Copy link
Copy Markdown
Contributor

Like Dave said, thanks for the notes for reviewers. They made understanding what was going on much easier. This looks great! 👍

@nasthagiri

Copy link
Copy Markdown
Contributor Author

@nasthagiri

Copy link
Copy Markdown
Contributor Author

@nasthagiri
nasthagiri merged commit 1de87f9 into master Jan 3, 2017
@nasthagiri
nasthagiri deleted the beryl/mongocrazy branch January 3, 2017 14:37
@cpennington

cpennington commented Jan 3, 2017

Copy link
Copy Markdown
Contributor

This PR has been deployed to edx.org Staging in preparation for a release to production on Weds, 1/4/2017. If you feel you need to validate it manually, please do so by then, and contact me if there is an urgent enough issue to scrap the deployment.

@doctoryes

Copy link
Copy Markdown
Contributor

👍 Thanks, @nasthagiri !

@cpennington

Copy link
Copy Markdown
Contributor

This PR has been deployed to edx.org.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants