-
Notifications
You must be signed in to change notification settings - Fork 4.2k
feat: reuse runtime services and wrappers between XBlocks [FC-0026] #32420
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
feat: reuse runtime services and wrappers between XBlocks [FC-0026] #32420
Conversation
|
Thanks for the pull request, @Agrendalath! As a core committer in this repo, you can merge this once the pull request is approved per the core committer reviewer requirements and according to the agreement with your edX Champion. |
60b1588 to
b09cbd2
Compare
|
EdX Release Notice: This PR has been deployed to the staging environment in preparation for a release to production. |
|
EdX Release Notice: This PR has been deployed to the production environment. |
|
EdX Release Notice: This PR has been deployed to the staging environment in preparation for a release to production. |
|
EdX Release Notice: This PR has been deployed to the production environment. |
5c742be to
3736c15
Compare
c242a53 to
0e3dc9f
Compare
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.
This has increased because we renamed get_block_for_descriptor_internal to get_block_for_descriptor.
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.
I moved this code to get_block_for_descriptor. I find this change a bit controversial - allowing None values in positional arguments is inelegant.
However, I had to untangle the dependencies between get_block_for_descriptor and get_block_for_descriptor_internal whenever I read this code. The latter exists primarily because we can use it in the inner_get_block and Instructor Dashboard tasks without the request object. Unifying these two functions makes this code (subjectively) easier to understand. We won't need to jump between four functions (get_block_for_descriptor, get_block_for_descriptor_internal, prepare_runtime_for_user, inner_get_block) to understand why they are calling each other.
xmodule/x_module.py
Outdated
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.
We don't need runtime._runtime_services anymore. Within a single request, each XBlock uses the same instance of the runtime, so we can safely rely on the XBlock library to check the service declaration:
https://github.com/openedx/XBlock/blob/7b8bdeafbabbbc96f11dfbceaf53c89bd416ea6f/xblock/runtime.py#L1120-L1126.
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.
This. Each XBlock in a single request uses the same runtime instance, so we don't need any cache. This was why the services were overriding each other with each XBlock initialization.
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.
Fixed by dc3fd30. This conversation explains why disable_staff_debug_info was added here. The render_xblock view is used in two places:
- In the Learning MFE (as an iframe).
- In the LTI context where Open edX is the LTI Provider.
It looks like the "staff debug info" button has been working correctly in the MFE by a pure accident - this parameter was overridden when the view initialized parent XBlocks.
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.
Added because of the get_block_for_descriptor_internal -> get_block_for_descriptor merge.
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.
We are not checking services because some of them are initialized by the Runtime class from the XBlock library.
xmodule/services.py
Outdated
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.
I don't know why we passed the function to this service - the local import is working fine.
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.
My guesses:
- We were trying to reduce the number of direct dependencies
xmodulehad onlms.*. - We used to pass different methods for LMS/Studio/Preview here?
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.
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.
We removed all XBlock-specific code from this function, so passing the runtime directly (instead of the block) is better.
|
@0x29a, @bradenmacdonald, this is ready for review. |
bradenmacdonald
left a comment
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.
👏🏻 wow, I am so happy to see this.
Again, I'll let @0x29a do the regression testing etc. I've just read through the code and I have to say, I really like it. Much cleaner and easier to understand. Hopefully more efficient and less buggy too :)
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.
What happens here if the user is masquerading? Is user.id and/or user_service._django_user.id the "real" user ID or the masquerading user ID?
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.
When a user is masquerading, the "real" user is under the user.real_user:
https://github.com/openedx/edx-platform/blob/c80fba689a36aa35489ded8018eb997c4ef4e8a1/lms/djangoapps/courseware/masquerade.py#L205-L244
The user service contains our masquerading target:
https://github.com/openedx/edx-platform/blob/27969b3e1e82edf35ac92a2eeae99ad15d6bf949/lms/djangoapps/courseware/block_render.py#L603-L613
Therefore, masquerading would require rebinding the runtime only if the runtime initialization happened from the "real" user (though I'm unsure if this ever happens). Once the runtime is bound to our masquerading target, the user is detected correctly, as the user service is initialized with the user object.
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.
What is this position parameter? I'm not familiar with it.
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.
As far as I understand, this is used by the SequenceBlock/subsection to determine which tab should be active:
https://github.com/openedx/edx-platform/blob/0b455e0336fa54199fd992c2ce83a81c07dd07c2/xmodule/seq_block.py#L335-L363
This is no longer used in the learning MFE, but the Preview still uses the old experience. I tried removing this, but it caused some errors. It should be safe to remove when we completely remove the legacy experience. For now, I would leave it as-is.
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.
I see. Could you perhaps add that information as a comment into the function?
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.
Sure, I added it in d7b898d. I added this information to the get_block function since it contains the description of these parameters. Let me know if you had another function in mind.
xmodule/services.py
Outdated
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.
RebindUserService is only used in the LMS right? I almost wonder if we should move this code out of xmodule and into lms. But no change needed.
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.
Yes, we only use it in LMS. However, we also instantiate it in the XBlockRuntime, which is in the openedx.core.djangoapps.xblock. I don't have enough context about this runtime to determine if we need it there:
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.
That runtime is currently used for "v2" content libraries and was also used for LabXchange where learners could "learn" (LMS mode) directly from components in a content library. I don't think it's used for LabXchange anymore. So the rebind service there is probably not used at the moment but will be in the future.
0x29a
left a comment
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.
👍
Note: this error appears when deleting a block from a unit in Studio. I verified that it happens without the changes from this PR and for courses created earlier, but anyway decided to share, just in case.
2023-06-26 11:42:45,652 INFO 1058 [tracking] [user 3] [ip 172.21.0.1] logger.py:41 - {"name": "/xblock/block-v1:Test+Test+Test+type@openassessment+block@878d7bca725b405ea5ec19ec28059df6", "context": {"user_id": 3, "path": "/xblock/block-v1:Test+Test+Test+type@openassessment+block@878d7bca725b405ea5ec19ec28059df6", "course_id": "", "org_id": "", "enterprise_uuid": ""}, "username": "edx", "session": "74b02955cff3654d45e4205f713fe972", "ip": "172.21.0.1", "agent": "Mozilla/5.0 (Windows NT 10.0; rv:114.0) Gecko/20100101 Firefox/114.0", "host": "localhost:18010", "re
ferer": "http://localhost:18010/container/block-v1:Test+Test+Test+type@vertical+block@552e0a3f2484401192e53b16cda50d9a?action=new", "accept_language": "en-US,en;q=0.5", "event": "{\"GET\": {}, \"POST\": {}}", "time": "2023-06-26T11:42:45.651868+00:00", "event_type": "/xblock/block-v1:Test+Test+Test+type@openassessment+block@878d7bca725b405ea5ec19ec28059df6", "event_source": "server", "page": null}
Error calling handle_item_deleted in Signal.send_robust() (BlockKey(type='openassessment', id='878d7bca725b405ea5ec19ec28059df6')) Traceback (most recent call last):
File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/dispatch/dispatcher.py", line 212, in send_robust response = receiver(signal=self, sender=sender, **named)
File "/edx/app/edxapp/edx-platform/cms/djangoapps/contentstore/signals/handlers.py", line 254, in handle_item_deleted deleted_block = modulestore().get_item(usage_key)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/mixed.py", line 84, in inner retval = func(field_decorator=strip_key_collection, *args, **kwargs)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/mixed.py", line 250, in get_item
return store.get_item(usage_key, depth, **kwargs) File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/split_draft.py", line 283, in get_item
return super().get_item(usage_key, depth=depth, **kwargs)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/split.py", line 1182, in get_item items = self._load_items(course, [BlockKey.from_usage_key(usage_key)], depth, **kwargs)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/split.py", line 779, in _load_items
return [runtime.load_item(block_key, course_entry, **kwargs) for block_key in block_keys]
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/split.py", line 779, in <listcomp>
return [runtime.load_item(block_key, course_entry, **kwargs) for block_key in block_keys]
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/caching_descriptor_system.py", line 122, in _load_item
block_data = self.get_module_data(block_key, course_key) File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/caching_descriptor_system.py", line 149, in get_module_data raise ItemNotFoundError(block_key) xmodule.modulestore.exceptions.ItemNotFoundError: BlockKey(type='openassessment', id='878d7bca725b405ea5ec19ec28059df6')
2023-06-26 11:42:45,661 ERROR 1058 [django.dispatch] [user 3] [ip 172.21.0.1] dispatcher.py:214 - Error calling handle_item_deleted in Signal.send_robust() (BlockKey(type='openassessment', id='878d7bca725b405ea5ec19ec28059df6'))
Traceback (most recent call last): File "/edx/app/edxapp/venvs/edxapp/lib/python3.8/site-packages/django/dispatch/dispatcher.py", line 212, in send_robust response = receiver(signal=self, sender=sender, **named)
File "/edx/app/edxapp/edx-platform/cms/djangoapps/contentstore/signals/handlers.py", line 254, in handle_item_deleted
deleted_block = modulestore().get_item(usage_key)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/mixed.py", line 84, in inner
retval = func(field_decorator=strip_key_collection, *args, **kwargs)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/mixed.py", line 250, in get_item
return store.get_item(usage_key, depth, **kwargs)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/split_draft.py", line 283, in get_item
return super().get_item(usage_key, depth=depth, **kwargs)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/split.py", line 1182, in get_item
items = self._load_items(course, [BlockKey.from_usage_key(usage_key)], depth, **kwargs)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/split.py", line 779, in _load_items
return [runtime.load_item(block_key, course_entry, **kwargs) for block_key in block_keys]
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/split.py", line 779, in <listcomp>
return [runtime.load_item(block_key, course_entry, **kwargs) for block_key in block_keys]
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/caching_descriptor_system.py", line 122, in _load_item
block_data = self.get_module_data(block_key, course_key)
File "/edx/app/edxapp/edx-platform/xmodule/modulestore/split_mongo/caching_descriptor_system.py", line 149, in get_module_data
raise ItemNotFoundError(block_key)
xmodule.modulestore.exceptions.ItemNotFoundError: BlockKey(type='openassessment', id='878d7bca725b405ea5ec19ec28059df6')- I did plenty of regression tests.
- I read through the changes.
- Includes comments where needed.
ormsbee
left a comment
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.
This PR brings me so much joy. 😄 Great stuff!
54ad729 to
03a661e
Compare
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.
Why are these conditional imports?
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.
@ormsbee, typing.TYPE_CHECKING (defined in PEP-484) is False at runtime. It should only be True during static analysis (usually in an IDE or type checker). Thanks to this, and the fact we don't evaluate annotations in function definitions (PEP-563), we:
- Reduce the number of imports. These imports are not heavy, but we're not instantiating these classes, so we don't need them at runtime.
- Avoid potential circular imports at runtime.
7e19462 to
6a8e695
Compare
|
@Agrendalath 🎉 Your pull request was merged! Please take a moment to answer a two question survey so we can improve your experience in the future. |
|
2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production. |
|
2U Release Notice: This PR has been deployed to the edX production environment. |
|
2U Release Notice: This PR has been deployed to the edX staging environment in preparation for a release to production. |
|
2U Release Notice: This PR has been deployed to the edX production environment. |
Description
The goal of FC-0026 is to remove the XBlock-specific handling from the prepare_runtime_for_user function. This part handles the final step, which is introducing runtime service reusability.
In a single request, each XBlocks use the same runtime instance. Therefore, when the services are initialized for the user in the course, these services are overridden1, so all XBlock-specific data is lost. In previous FC-0026 PRs, we have removed the block-specific code from the
prepare_runtime_for_userfunction. This way, it no longer matters which XBlock bound the runtime to the user. This PR ensures that the runtime binding happens only once for each user2.Supporting information
Private-ref: BB-7448
Testing instructions
Ensure that XBlocks work correctly in the LMS, Preview, and Studio. Check that importing and exporting the course works.
Other notes
Please ignore the first two commits, as they come from #32357.
Footnotes
You can see this comment, which explains the fragile behavior related to the runtime initialization order. ↩
There are two use cases for this:
- While rebinding the user in the LTI XBlock.
- In the Instructor Dashboard tasks - when a Problem Block is rescored for multiple users.
↩