Core Arch Working Group
A weekly meeting to discuss the code and data which underpins the platform’s teaching & learning capabilities, centered around the openedx-core repository.
Resources:
#wg-core-architecturefor quick discussions and logisticGitHub issues on openedx-core for deep discussions
Recordings and transcripts: https://drive.google.com/drive/folders/1B6yomR8RTaQLmazQMjJi6EcwwJxb_stx
Current projects:
CBE
Pathways
MongoDB deprecation
Past projects:
openedx-core 1.0 https://github.com/openedx/openedx-learning/issues/353
Agendas and notes:
- 1 Aug 11, 2026
- 1.1 1. Deadlocks in competency status evaluation (Jesper)
- 1.1.1 Correcting the framing
- 1.1.2 The problem
- 1.1.3 How bad is it?
- 1.1.4 Three candidate approaches
- 1.1.5 Outcome
- 1.2 2. Is a Usage table needed for CBE in Willow? (Kyle)
- 1.3 3. MongoDB-free static assets + external asset management (Kyle, Braden)
- 1.4 Action items
- 1.1 1. Deadlocks in competency status evaluation (Jesper)
- 2 Aug 4, 2026
- 3 Jul 27, 2026
- 4 Jul 20, 2026
- 5 Jul 14, 2026
- 6 Jul 7, 2026
- 7 Jun 16, 2026
- 8 Jun 9, 2026
- 9 May 5, 2026
- 10 Apr 28, 2026
- 11 Apr 21, 2026
- 12 Apr 14, 2026
- 13 Apr 7, 2026
- 14 Mar 31, 2026
- 15 Mar 24, 2026
- 16 Mar 17, 2026
- 17 Mar 11, 2026
- 18 Mar 6, 2026
- 19 Feb 10, 2026
- 20 Jan 20, 2026
- 21 Jan 13, 2026
- 22 Jan 6, 2026
- 23 Dec 16, 2025
- 24 Nov 25, 2025
- 25 Oct 21, 2025
- 26 Oct 7, 2025
- 27 Aug 19, 2025
- 28 Jul 29, 2025
- 29 Jul 22, 2025
- 30 Jun 17, 2025
- 31 Jun 10, 2025
- 31.1.1 Outline (45 min talk)
- 31.2 Outline Strawman:
- 32 Jun 3, 2025
- 32.1 May 15, 2025
- 33 2025-05-15
- 34 2025-05-06
- 35 2025-04-29
- 36 2025-04-02
- 37 2025-03-05
- 38 2025-02-05
- 39 2025-01-09
- 40 2024-12-18
- 41 Old Notes
- 41.1 Talk Proposal
- 41.1.1 Title
- 41.1.2 Description (<500 words)
- 41.1.3 Type
- 41.1.4 Target Audience
- 41.1.5 Proposal
- 41.1.6 Rough Talk Outline
- 41.1.7 Additional Notes
- 41.2 2024-11-20
- 41.3 2024-11-13
- 41.1 Talk Proposal
Aug 11, 2026
Attendees: Kyle McCormick, Braden MacDonald, Jesper Hodge, Mary Gwozdz (Dave Ormsbee on PTO)
Notes generated with Claude by combining human notes, transcript, and chat log
1. Deadlocks in competency status evaluation (Jesper)
Correcting the framing
Kyle's summary was close but assumed a new signal handler. In fact, per Dave's review comment on the ADR, no new handler is being built: an existing Django signal already invokes a celery task that updates the subsection grade, and the competency status re-evaluation will happen inside that same task, in the same atomic transaction, synchronously — no secondary async hop.
The problem
A subsection grade change triggers re-evaluation of the learner's competency status tree. ADR 0004 (proposed) allows these tasks to run concurrently and takes a lock on group (non-leaf) nodes. Kyle's worked example clarified why the lock exists: if two criteria under the same parent flip to demonstrated at nearly the same moment, each child's task reads stale sibling state and can leave the parent marked not-demonstrated (or partially demonstrated) when it should be demonstrated. Locking the parent makes the second writer wait its turn.
Two deadlock shapes were identified:
Two threads contending for the same group node — solvable by ordering.
A grade change that manifests as multiple leaves in one tree (multiple assignment versions), where each thread holds one group and waits for another. Braden pointed out that ordering doesn't save you here: task 1 locks A+C, task 2 locks B+C, and they still deadlock over C.
Mary confirmed from Claude in chat that a row lock is held for the entire transaction, not just the statement that took it — which is where the exposure comes from.
How bad is it?
A real deadlock would make celery error and retry; because it's one transaction, the grade rolls back automatically with no manual cleanup. Braden noted that celery-only impact is bounded — fixed worker pool, tasks just queue — and the dangerous scenario is anything that touches LMS workers, which can spike resources and take a site down. Jesper was still concerned: this is the task that evaluates subsection grades, and stalling it during timed exams (hundreds or thousands of submissions in a narrow window) is time-sensitive, plus he's seen celery task explosions cause bad downstream effects at edX. Kyle and Braden agreed rare race-condition bugs are miserable to diagnose from learner reports, so it's worth solving up front rather than as a later bugfix.
Three candidate approaches
Approach | Assessment |
|---|---|
Coarser per-learner (or deployment-wide) lock | Already in the ADR's rejected alternatives — overkill, less performant, and adds lock-lifecycle machinery over a huge key space. Braden floated it as a fallback if deadlocks turn out to be common. |
No lock; read-after-write | Commit the group node, then re-read the children (likely needing a second transaction) and fix if anything changed. Django's READ COMMITTED on MySQL guarantees a fresh snapshot. Cascading repeats up the tree are possible but rare. |
Dirty flag + delayed sweep | Leaf and dirty flag written in the subsection transaction; a delayed or scheduled task walks dirty trees. Braden noted openedx-completion-aggregator already does exactly this — leaf updated immediately, tree flagged dirty, hourly rollup, with real-time computation for individual learners so they never see stale data. Caveat: there's one course tree per learner there, but many competency trees per learner here. |
Outcome
Consensus to drop locking. Mary, Braden, and Kyle all leaned toward read-after-write for its simplicity, with Braden's caveat that he can't yet fully reason through every interleaving and be certain it's correct. Kyle framed the deciding principle as preferring an approach the team understands, even at the cost of twice the celery tasks or slightly stale data, over one where a known deadlock risk isn't fully characterized.
Jesper will write both non-locking options into the ADR as alternatives so it can merge (unblocking other work), with the final pick deferred to Dave when he's back from PTO next week — Braden expects Dave to recommend against locking generally. Jesper also suggested exhaustive test cases enumerating every race-condition constellation, partly as documentation. Kyle asked for logging around whatever mechanism lands so operators can see it, plus buffer in the implementation issue to scrutinize the code. Implementation is expected around end of September.
2. Is a Usage table needed for CBE in Willow? (Kyle)
Conclusion: no.
CompetencyCriteria points at ObjectTag, which carries the UsageKey — already implemented, so nothing is blocked.
CompetencyCriteria will point at CourseRun rather than CourseKey.
Mary and Kyle confirmed in chat that
user_idis already a proper FK to User, not a usage key.Braden and Kyle both want ObjectTag to eventually hold real foreign keys to the objects it tags, but that needs courses and content in openedx-core first (learning context and openedx-content), so it's a future item.
This spun into a side discussion about ADR churn: these ADRs keep needing edits as models firm up. Options raised were putting less implementation detail in ADRs, moving class/table specifics into Confluence or a separate design doc that can be updated freely while the ADR stays the durable source of truth, or waiting until the models from #613 land and then simplifying. Left for Mary and Jesper to settle.
3. MongoDB-free static assets + external asset management (Kyle, Braden)
Braden's estimates are waiting on Chris's review and should go to Kyle same-day. WGU has separately asked about asset management as a first-class citizen in Open edX and may be able to put a couple of engineers on it; Kyle asked whether that overlaps with getting assets out of MongoDB. Braden thinks yes, and it relates to openedx-core#499.
Braden's sketch (shared on screen): a new top-level asset entity in libraries, alongside collections/units/subsections/sections. One asset may bundle multiple files — video renditions, streaming formats, subtitle tracks — kept grouped rather than scattered through the library, and referenceable from components. Longer term the same model replaces files-and-uploads in courses, including assets scoped to a single component so they don't need separate management. Crucially, if asset is a top-level entity it doesn't have to be backed by openedx-content: it could point at an external DAM with metadata pulled in.
On versioning, Braden was firm that nothing should change under anything — the asset would carry its own asset versions, with a sync process minting new version metadata when the external system publishes a new version. Open questions: how to discover and sync external assets and their updates, and whether to surface an entire external catalog or only a chosen subset. Note that Dave has previously said we don't want to reimplement a DAM inside Open edX, only support external ones.
Jesper recalls http://edX.org overriding the MongoDB-backed asset store with an S3 plugin and thinks some of this may already exist in open source (S3 configuration thread), and will dig up the actual code tomorrow rather than rely on memory. Braden believes the S3 settings don't cover the files-and-uploads page specifically, and pointed to platform PR #38950, which documents how asset storage works today — he'd like corrections folded in there. Jesper also flagged that whatever changes land must not break http://edX.org 's existing S3 overrides. Migration will be course-by-course with both systems runnable in parallel.
Action items
Jesper — revise ADR 0004 (PR #713): remove the locking approach, present read-after-write and dirty-flag/cleanup-job as alternatives, note that deadlocks need handling and add logging; get the PR into a shape that can merge regardless of which is chosen.
Kyle / Jesper — put the two options to Dave when he's back from PTO next week for a recommendation.
Braden — send static-asset estimates to Kyle once Chris reviews.
Jesper — investigate http://edX.org 's S3 asset store overrides, send code and findings to Braden and comment on PR #38950.
Mary / Jesper — decide on an approach to reduce ADR churn (design doc vs. genericizing vs. waiting on #613).
Implementation of the CBE competency-status mechanism: ~end of September.
Aug 4, 2026
[Mary, Jesper] How to deal with overly massive historic competency attempt data?
Work-in-progress CBE ADR 0005 deals with storing learner subsection attempts (and other scored content) that demonstrate competency mastery. If competencies should be very widely adopted, as we hope, the largest OpenEdx instances could eventually have as many rows as the largest historic platform tables for learner attempts, possibly tens of billions of rows. As the new history table would scale with learner attempts, it can grow very quickly. Truncating the table after 2 years of historic data is accepted.
Question: do we need the history table at all? https://github.com/openedx/openedx-core/pull/657#issuecomment-5170123588
If we avoid it, we’ll need to change a couple of architectural things on the competency criteria tables
If we do want the history separately:
Just using clickhouse / aspects analytics and avoid the MySQL table?
Or: compressing data older than 1 month, maybe offloading to S3?
If we want the table currently discussed as storing all attempts separately in MySQL without compressing, what non-functional constraints do we need to adhere to, and how?
Discussion
Braden: can we reconstruct the data using student response history (CSMH) and historic CBE criteria info?
Jesper: That’s one of the questions I’m asking, it would be good to avoid the need to store all this data if possible.
Dave: problem is CSMH is not kept around for too long. Maybe a month or so on edx.org?
Is the history ever student facing? Yes, in the future.
Aspects is currently a staff facing thing only.
Is that history… we put 2 years as a rough marker in which we cand elete the history. on teh courseo r student module historys ide, we keep the history around for a very limited amount of time (1 month) because this is primarily for a debugging tool or a remote grader. The history is only useful when the course is actively running, so we kill it afterwards. Is tehre a point wehre the user has achieved htis thing so the history of how they’ve achieved it doesn’t matter anymore?
note: this is not for the willow MVP. but might affect data modelling decisions in MVP.
dave: CSMH was hastily built and has been operationally very challenging ever since. would want to wait until we have a solid use case and solid understanding of how we’ll query it before implementing another history table.
is CSMH truncation built in, or decided by operator?
decided by operator
thus, we can probably reconstruct mastery history
is there a point in time event where the user has achieved something, and thus the history no longer matters?
few different use cases
big one: learner dispute - they believe they should have earned mastery but system didn’t give it to them. a month or two?
for learners to understand how they did, knowing what they should go back and study for their summatives.
legal compliance--records need to be around for some amount time. 2+ years?
why is this different than CSMH?
probably isn’t. might need to persist CSMH for longer, too.
is a business decision of that organization.
could also be compressed and stored in a cheap (S3 glacier) way. but this could be complicated.
aspects is OK for this maybe?
for instructional designers to understand what course materials are/aren’t working well. but, aggregated data / aspects could work for that.
agreed that this should be in aspects.
Needs for Willow
Disputes ?
We actually do want to store history in willow, but just for admins to reverse engineer if necessary ?
What is the confidence level of dispute resolution being a Willow MVP requirement
Would like product documentation on this requirement
Working with EDL, disputes came up very frequently
Would resolving disputes require looking closely at history?
For EDL, they had to ask themselves “was this the learner’s fault or ours?” So being able to look back at attempt history was important.
We already store grades--this is about attempts, not grades
Aspects probably won’t have this for Willow
If we don’t do it for willow, then we wouldn’t have that data in Willow
Need for subsection-level history table?
Note that subsection-level history would involve one row per component attempt per competency.
Will ask Jenna & Tammy tmrw about needs for history
Jul 27, 2026
[Mary, Jesper] Talk through ADRs to move them along
Questions worth talking about here?
Okay to use a bit of this meeting to get progress on ADR reviews?
https://github.com/openedx/openedx-core/pull/656
will merge
https://github.com/openedx/openedx-core/pull/657
dave will rereview
https://github.com/openedx/openedx-core/pull/662
will merge
[Jesper] Is there a good pattern for database reads in openedx-core?
edx-platform and openedx-core share the same database, while the dependency is one-way: edx-platform depends on openedx-core. This makes it awkward when openedx-core needs to read from models that may be defined in edx-platform.
Sometimes it may not be practical or clean to move the whole model from edx-platform to openedx-core if it’s just for a database read and we think the model would belong in edx-platform otherwise.
Can we find a good pattern that everyone is architecturally happy with?
Abstract Base Classes in openedx-core with minimal interface that the edx-platform model can inherit?
Some kind of bridge or adapter pattern?
Some kind of Python API?
Pass a reference to the model from edx-platform to the openedx-core library? (
def register_model(self, django_model))Example: we’ll need to read the grade of a subsection (currently stored in platform) in order to determine mastery (stored in core)
Generally, we want to avoid this. Would want to bring grades into core.
For example, we brought Course and CourseRun models into Core
Core should not even understand platform models
When complex stuff can’t be pulled into Core, then build the things that depend on it in Platform. e.g., the part of Content Libraries that works with the XBlock runtime, since the runtime is in Platform
CBE could have this same split.
Historical context: We’d been trying to extract large systems from openedx-platform. It’s very very hard, notably bc the dependencies are pointing all over the place. Want to keep them all pointing one direction.
Jesper: Sometimes it won’t work. We could pass individual callbacks in, like a
get_gradecallbackKyle: aiming for openedx-core as self-contained data packged which can be pushed into. do not want to emulate xblock service system, which has very leaky callbacks
The “push to core” model basically always works
[Kyle] MongoDB-free courseware – what to target for Willow?
files and uploads?
meta
Vertical (course-by-course flag) or Horizontal (feature by feature)
Jul 20, 2026
[Dave]: We're soon going to be hitting a point where we'll want to store user state against content in
openedx_learning, and I would very much like to avoid writing usage keys everywhere. A large site like edx.org wastes terabytes of space because we don't normalize those references. Which means we should have a scalable, flexible data model representation for "this piece of content in this course" and a way to join that to student state.Background
Right now there’s no database table for individual components in a course
When you add an xblock instance (usage), there’s no model instance representing that. Just a usage key (
block-v1:MyOrg+MyCourse+MyRun+block@abcd+type@problem)Every bit of state, including an answer to a problem, uses that key string
For every user in the course (10s of thousands)
Summary: We need primary keys for xblock usages
99% of the time when we talk about student state, we’re talking about interactions between a user and an xblock. Things like “they attempte the problem by answering C” or “they stopped the video at 0:39”
We have one gigantic table called CoursewareStudentModule. It’s verrry large and very inefficient. Uses string keys, it’s denormalized, and stores student state as a student string.
Why?
Yes, primary key will be shorter than a usage key string
Can make the other columns more efficient – JSONField
We’ll be able to have FK constraints related to Usages and LearningContexts, which we couldn’t before
Migration
Will try to migrate many existing UsageKey strings over to the new tables
But will probably not be able to do it for the huge tables (CSM, CSMH)
Model sketch
LearningContext
Do we want to store student state directly against a library in the LMS?
Maybe yes - imagine
Does MIT use the library embed endpoint? Used for physics problems?
Two embed endpoints: one is for LTI, one is for iframe embed
There’s talk of things like micro-courses, “duolingo” type courses. Wouldn’t it make more sense to store that student state against Pathways (or some other learning context) instead of libraries?
Libraries probably shouldn’t have state stored against them. They’re a repository of libraries, not an instantiation.
LMS shouldn’t be aware of libraries? On principal, at least
LearningContextItemType ???
It’s only Course and Library now, but will probably include Learning Pathways soon, or possibly many types of Learning Pathways.
Usage
This means: Content Instantiation. Practically speaking in 2026, XBlock instantiation
I had a lot of kind of “out there” thoughts on Usage, but I think the simplest one is what I’d put forth as a proposal:
fkey to LearningContext, so a Usage is owned one and only one LearningContext.
nullable fkey to PublishableEntity, for a couple of reasons:
this allows us to bring over modulestore-backed content Usages for some uses (e.g. CBE), even before those courses get moved into openedx_content data models.
it guards against PublishableEntity deletion, e.g. deleting content won’t cause a cascade that wipes out student grades.
Usage has its own way to define namespace and identifier. This can often be redundant with LearningPackages, which also define their own namespace. But those do it from an authoring perspective. Usages have to be unique within a LearningContext, and Learning Pathways may want to use content that exists in different Libraries. Also, this would let us model something like a CCX, where Usages are M:1 on backing PublishableEntities.
Question: Will this data apply to Containers too, or just Components?
For instance, the Subsection container remembers the index of the Unit you were on. This is student state.
One option is for the LearningContext to remember it
Will we have usage keys for Containers, going forward?
We definitely need a key for the Container
But will we use them as UsageKeys going forward?
Jesper: Could have a lookup table which maps Container FKs to usage keys
But the system will probably need string keys for containers anyway
Braden: Could have ComponentUsage and ContainerUsage. Plugins will probably want to store data against containers, esp. Units
Mary: People will want analytics off of containers
Also grades, and completion.
What if we decouple string usage keys from the tables, use FKs as much as possible?
yes, we will just want to use the human-readable keys at the boundaries of the system
We’ll want to congizent of the extra lookup to translate to/from human-reaedable keys
Particularly we’ll want to not lock these tables, mostly used in read transactions
Think carefully about when the tables are written
e.g., constantly writing analytics info to these tables
We do have mechanisms for storing student state against libraries. Was used for labxchange, but isn’t used now.
in
CSMunder the library keyno way to it from the UI. public embed URL
In studio, we store temporary state against components for purpose of testing content
How to store student state in a Pathway
e.g., you have 3 courses, and a standalone final exam in a pathway. is each course a learningcontext, and the final exam is too? is the entire pathway a learning context?
One option: make courses more lightweight, so the subsection could be a final exam.
This would break many assumptions baked into courseware. Would loosening those assumption be tractable as compared to building the standalone final exam as a separate kind of LC?
Braden: we could have backcompat wrappers for code which assumes the 3-tier structure. Like a ghost section
Mary: CC->Openedx conversion requires creating ghost units
[all] Any outstanding ADRs that would be good to hash out in person?
Question: any strategies when we want to access the database from openedx-core?
Jul 14, 2026
Conflict for Axim – sync up in Slack
Jul 7, 2026
came to consensus on https://github.com/openedx/openedx-core/pull/637
Jun 16, 2026
keep fixing libraries bugs
keep merging in openedx-core docs
Jun 9, 2026
[Kyle] Verawood status
[dave] braden got a bunch of stuff done in terms of backported fixes
https://github.com/openedx/openedx-platform/pull/38695
should help the import speed issue
[dave] Would appreciate this docs PR getting early review so I don’t have to rebase: https://github.com/openedx/openedx-core/pull/583
Recent report https://discuss.openedx.org/t/out-of-sync-library-components-counted-after-unit-deletion-from-course/19087
Known bugs:https://github.com/openedx/frontend-app-authoring/issues/3045
Are we going to target all of these for verawood.1?
^ No, but they are priority ordered, and the first few are labelled with
release blockerRelease is still scheduled for Jun 23, 2026
Kyle will run these by Jenna
[Dave]: I have some longer term topics that are post-Verawood concerns (and can be postponed to address any Verawood ones):
Do we want to investigate django-polymorphic for some of our data models?
Worth looking into. Small, well-maintained.
[kyle] 5 minute skim – looks solidly maintained and serves a good use case. no objections
When would be a good time to cut over to using pyproject.toml?
How do folks feel about django-ninja as the basis of an openedx_content REST API?
Bring it up at standup, get feanil’s opinion
Pilot it with one REST API first, then evaluate
Possibility: Pathways REST API
But we need to get authz into core.
Now that there’s a separate repo for authz, should we be bringing more of libraries into openedx-core in Willow?
May 5, 2026
Next steps between now and verawood.1
[Dave] Course import into a library is painfully slow. The current pattern is to add and publish components one by one, meaning that even if events are processed on a PublishLog basis, we’re doing hundreds to thousands of them per import. Can we draft all the changes first and then publish all at once?
[Braden] PR: https://github.com/openedx/openedx-platform/pull/38508 Does that speed it up?
[Dave] It speeds up the actual database part of it, but it’s the meilisearch re-indexing that’s glacial. It looks like each block gets its own task. I guess ideally, we’d want to fire off the search updates in bulk based on the DraftChangeLog/PublishLog events.
[Braden] Thought that the modulestore side was slower than the meili side
[Dave] Not sure
[Dave] Also, this reminds me that we should have a
.draftsproperty onDraftChangeLogso we can do something likepublish_from_drafts(lp.id, change_log.drafts). “Publish the DraftChangeLog I just made” has to be a really common use case. Maybe its own function entirely.[Dave] This is the top priority bugfix