Skip to content

Commit 5b80967

Browse files
refactor: get rid of XBlockRuntimeSystem for v2 libraries (#35624)
1 parent b8c79ab commit 5b80967

File tree

4 files changed

+46
-107
lines changed

4 files changed

+46
-107
lines changed

openedx/core/djangoapps/xblock/api.py

Lines changed: 11 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,13 @@
2525
from xblock.exceptions import NoSuchUsage, NoSuchViewError
2626
from xblock.plugin import PluginMissingError
2727

28+
from openedx.core.types import User as UserType
2829
from openedx.core.djangoapps.xblock.apps import get_xblock_app_config
2930
from openedx.core.djangoapps.xblock.learning_context.manager import get_learning_context_impl
3031
from openedx.core.djangoapps.xblock.runtime.learning_core_runtime import (
3132
LearningCoreFieldData,
3233
LearningCoreXBlockRuntime,
3334
)
34-
from openedx.core.djangoapps.xblock.runtime.runtime import XBlockRuntimeSystem as _XBlockRuntimeSystem
3535
from .utils import get_secure_token_for_xblock_handler, get_xblock_id_for_anonymous_user
3636

3737
from .runtime.learning_core_runtime import LearningCoreXBlockRuntime
@@ -54,33 +54,21 @@ class CheckPerm(Enum):
5454
CAN_EDIT = 3
5555

5656

57-
def get_runtime_system():
57+
def get_runtime(user: UserType):
5858
"""
59-
Return a new XBlockRuntimeSystem.
60-
61-
TODO: Refactor to get rid of the XBlockRuntimeSystem entirely and just
62-
create the LearningCoreXBlockRuntime and return it. We used to want to keep
63-
around a long lived runtime system (a factory that returns runtimes) for
64-
caching purposes, and have it dynamically construct a runtime on request.
65-
Now we're just re-constructing both the system and the runtime in this call
66-
and returning it every time, because:
67-
68-
1. We no longer have slow, Blockstore-style definitions to cache, so the
69-
performance of this is perfectly acceptable.
70-
2. Having a singleton increases complexity and the chance of bugs.
71-
3. Creating the XBlockRuntimeSystem every time only takes about 10-30 µs.
72-
73-
Given that, the extra XBlockRuntimeSystem class just adds confusion. But
74-
despite that, it's tested, working code, and so I'm putting off refactoring
75-
for now.
59+
Return a new XBlockRuntime.
60+
61+
Each XBlockRuntime is bound to one user (and usually one request or one
62+
celery task). It is typically used just to load and render a single block,
63+
but the API _does_ allow a single runtime instance to load multiple blocks
64+
(as long as they're for the same user).
7665
"""
77-
params = get_xblock_app_config().get_runtime_system_params()
66+
params = get_xblock_app_config().get_runtime_params()
7867
params.update(
79-
runtime_class=LearningCoreXBlockRuntime,
8068
handler_url=get_handler_url,
8169
authored_data_store=LearningCoreFieldData(),
8270
)
83-
runtime = _XBlockRuntimeSystem(**params)
71+
runtime = LearningCoreXBlockRuntime(user, **params)
8472

8573
return runtime
8674

@@ -121,7 +109,7 @@ def load_block(usage_key, user, *, check_permission: CheckPerm | None = CheckPer
121109
# e.g. a course might specify that all 'problem' XBlocks have 'max_attempts'
122110
# set to 3.
123111
# field_overrides = context_impl.get_field_overrides(usage_key)
124-
runtime = get_runtime_system().get_runtime(user=user)
112+
runtime = get_runtime(user=user)
125113

126114
try:
127115
return runtime.get_block(usage_key)

openedx/core/djangoapps/xblock/apps.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,9 @@ class XBlockAppConfig(AppConfig):
1616
verbose_name = 'New XBlock Runtime'
1717
label = 'xblock_new' # The name 'xblock' is already taken by ORA2's 'openassessment.xblock' app :/
1818

19-
def get_runtime_system_params(self):
19+
def get_runtime_params(self):
2020
"""
21-
Get the XBlockRuntimeSystem parameters appropriate for viewing and/or
21+
Get the LearningCoreXBlockRuntime parameters appropriate for viewing and/or
2222
editing XBlock content.
2323
"""
2424
raise NotImplementedError
@@ -43,9 +43,9 @@ class LmsXBlockAppConfig(XBlockAppConfig):
4343
LMS-specific configuration of the XBlock Runtime django app.
4444
"""
4545

46-
def get_runtime_system_params(self):
46+
def get_runtime_params(self):
4747
"""
48-
Get the XBlockRuntimeSystem parameters appropriate for viewing and/or
48+
Get the LearningCoreXBlockRuntime parameters appropriate for viewing and/or
4949
editing XBlock content in the LMS
5050
"""
5151
return dict(
@@ -65,9 +65,9 @@ class StudioXBlockAppConfig(XBlockAppConfig):
6565
Studio-specific configuration of the XBlock Runtime django app.
6666
"""
6767

68-
def get_runtime_system_params(self):
68+
def get_runtime_params(self):
6969
"""
70-
Get the XBlockRuntimeSystem parameters appropriate for viewing and/or
70+
Get the LearningCoreXBlockRuntime parameters appropriate for viewing and/or
7171
editing XBlock content in Studio
7272
"""
7373
return dict(

openedx/core/djangoapps/xblock/runtime/learning_core_runtime.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ def get_block(self, usage_key, for_parent=None):
211211
# We've pre-loaded the fields for this block, so the FieldData shouldn't
212212
# consider these values "changed" in its sense of "you have to persist
213213
# these because we've altered the field values from what was stored".
214-
self.system.authored_data_store.mark_unchanged(block)
214+
self.authored_data_store.mark_unchanged(block)
215215

216216
return block
217217

@@ -221,7 +221,7 @@ def save_block(self, block):
221221
222222
This gets called by block.save() - do not call this directly.
223223
"""
224-
if not self.system.authored_data_store.has_changes(block):
224+
if not self.authored_data_store.has_changes(block):
225225
return # No changes, so no action needed.
226226

227227
# Verify that the user has permission to write to authored data in this
@@ -254,7 +254,7 @@ def save_block(self, block):
254254
},
255255
created=now,
256256
)
257-
self.system.authored_data_store.mark_unchanged(block)
257+
self.authored_data_store.mark_unchanged(block)
258258

259259
def _get_component_from_usage_key(self, usage_key):
260260
"""

openedx/core/djangoapps/xblock/runtime/runtime.py

Lines changed: 26 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -95,18 +95,30 @@ class XBlockRuntime(RuntimeShim, Runtime):
9595
# currently only used to track if we're in the studio_view (see below under service())
9696
view_name: str | None
9797

98-
def __init__(self, system: XBlockRuntimeSystem, user: UserType | None):
98+
def __init__(
99+
self,
100+
user: UserType | None,
101+
*,
102+
handler_url: Callable[[UsageKey, str, UserType | None], str],
103+
student_data_mode: StudentDataMode,
104+
id_reader: Optional[IdReader] = None,
105+
authored_data_store: Optional[FieldData] = None,
106+
):
99107
super().__init__(
100-
id_reader=system.id_reader,
108+
id_reader=id_reader or OpaqueKeyReader(),
101109
mixins=(
102110
LmsBlockMixin, # Adds Non-deprecated LMS/Studio functionality
103111
XBlockShim, # Adds deprecated LMS/Studio functionality / backwards compatibility
104112
),
105113
default_class=None,
106114
select=None,
107-
id_generator=system.id_generator,
115+
id_generator=MemoryIdManager(), # We don't really use id_generator until we need to support asides
108116
)
109-
self.system = system
117+
assert student_data_mode in (StudentDataMode.Ephemeral, StudentDataMode.Persisted)
118+
self.authored_data_store = authored_data_store
119+
self.children_data_store = None
120+
self.student_data_mode = student_data_mode
121+
self.handler_url_fn = handler_url
110122
self.user = user
111123
# self.user_id must be set as a separate attribute since base class sets it:
112124
if self.user is None:
@@ -126,7 +138,7 @@ def handler_url(self, block, handler_name: str, suffix='', query='', thirdparty=
126138
if thirdparty:
127139
log.warning("thirdparty handlers are not supported by this runtime for XBlock %s.", type(block))
128140

129-
url = self.system.handler_url(block.scope_ids.usage_id, handler_name, self.user)
141+
url = self.handler_url_fn(block.scope_ids.usage_id, handler_name, self.user)
130142
if suffix:
131143
if not url.endswith('/'):
132144
url += '/'
@@ -275,7 +287,7 @@ def service(self, block: XBlock, service_name: str):
275287
# the preview engine, and 'main' otherwise.
276288
# For backwards compatibility, we check the student_data_mode (Ephemeral indicates CMS) and the
277289
# view_name for 'studio_view.' self.view_name is set by render() below.
278-
if self.system.student_data_mode == StudentDataMode.Ephemeral and self.view_name != 'studio_view':
290+
if self.student_data_mode == StudentDataMode.Ephemeral and self.view_name != 'studio_view':
279291
return MakoService(namespace_prefix='lms.')
280292
return MakoService()
281293
elif service_name == "i18n":
@@ -301,14 +313,12 @@ def service(self, block: XBlock, service_name: str):
301313
return EventPublishingService(self.user, context_key, make_track_function())
302314
elif service_name == 'enrollments':
303315
return EnrollmentsService()
316+
elif service_name == 'error_tracker':
317+
return make_error_tracker()
304318

305-
# Check if the XBlockRuntimeSystem wants to handle this:
306-
service = self.system.get_service(block, service_name)
307319
# Otherwise, fall back to the base implementation which loads services
308320
# defined in the constructor:
309-
if service is None:
310-
service = super().service(block, service_name)
311-
return service
321+
return super().service(block, service_name)
312322

313323
def _init_field_data_for_block(self, block: XBlock) -> FieldData:
314324
"""
@@ -322,7 +332,7 @@ def _init_field_data_for_block(self, block: XBlock) -> FieldData:
322332
assert isinstance(self.user_id, str) and self.user_id.startswith("anon")
323333
kvs = EphemeralKeyValueStore()
324334
student_data_store = KvsFieldData(kvs)
325-
elif self.system.student_data_mode == StudentDataMode.Ephemeral:
335+
elif self.student_data_mode == StudentDataMode.Ephemeral:
326336
# We're in an environment like Studio where we want to let the
327337
# author test blocks out but not permanently save their state.
328338
kvs = EphemeralKeyValueStore()
@@ -341,10 +351,10 @@ def _init_field_data_for_block(self, block: XBlock) -> FieldData:
341351
student_data_store = KvsFieldData(kvs=DjangoKeyValueStore(field_data_cache))
342352

343353
return SplitFieldData({
344-
Scope.content: self.system.authored_data_store,
345-
Scope.settings: self.system.authored_data_store,
346-
Scope.parent: self.system.authored_data_store,
347-
Scope.children: self.system.children_data_store,
354+
Scope.content: self.authored_data_store,
355+
Scope.settings: self.authored_data_store,
356+
Scope.parent: self.authored_data_store,
357+
Scope.children: self.children_data_store,
348358
Scope.user_state_summary: student_data_store,
349359
Scope.user_state: student_data_store,
350360
Scope.user_info: student_data_store,
@@ -407,62 +417,3 @@ def _lookup_asset_url(self, block: XBlock, asset_path: str): # pylint: disable=
407417
"""
408418
# Subclasses should override this
409419
return None
410-
411-
412-
class XBlockRuntimeSystem:
413-
"""
414-
This class is essentially a factory for XBlockRuntimes. This is a
415-
long-lived object which provides the behavior specific to the application
416-
that wants to use XBlocks. Unlike XBlockRuntime, a single instance of this
417-
class can be used with many different XBlocks, whereas each XBlock gets its
418-
own instance of XBlockRuntime.
419-
"""
420-
def __init__(
421-
self,
422-
handler_url: Callable[[UsageKey, str, UserType | None], str],
423-
student_data_mode: StudentDataMode,
424-
runtime_class: type[XBlockRuntime],
425-
id_reader: Optional[IdReader] = None,
426-
authored_data_store: Optional[FieldData] = None,
427-
):
428-
"""
429-
args:
430-
handler_url: A method to get URLs to call XBlock handlers. It must
431-
implement this signature:
432-
handler_url(
433-
usage_key: UsageKey,
434-
handler_name: str,
435-
user: User | AnonymousUser | None
436-
) -> str
437-
student_data_mode: Specifies whether student data should be kept
438-
in a temporary in-memory store (e.g. Studio) or persisted
439-
forever in the database.
440-
runtime_class: What runtime to use, e.g. LearningCoreXBlockRuntime
441-
"""
442-
self.handler_url = handler_url
443-
self.id_reader = id_reader or OpaqueKeyReader()
444-
self.id_generator = MemoryIdManager() # We don't really use id_generator until we need to support asides
445-
self.runtime_class = runtime_class
446-
self.authored_data_store = authored_data_store
447-
self.children_data_store = None
448-
assert student_data_mode in (StudentDataMode.Ephemeral, StudentDataMode.Persisted)
449-
self.student_data_mode = student_data_mode
450-
451-
def get_runtime(self, user: UserType | None) -> XBlockRuntime:
452-
"""
453-
Get the XBlock runtime for the specified Django user. The user can be
454-
a regular user, an AnonymousUser, or None.
455-
"""
456-
return self.runtime_class(self, user)
457-
458-
def get_service(self, block, service_name: str):
459-
"""
460-
Get a runtime service
461-
462-
Runtime services may come from this XBlockRuntimeSystem,
463-
or if this method returns None, they may come from the
464-
XBlockRuntime.
465-
"""
466-
if service_name == 'error_tracker':
467-
return make_error_tracker()
468-
return None # None means see if XBlockRuntime offers this service

0 commit comments

Comments
 (0)