Skip to content

fix: drop row locks from bulk criterion updates - #914

Draft
jesperhodge wants to merge 1 commit into
openedx:mainfrom
jesperhodge:jesperhodge/cbe-drop-bulk-update-row-locks
Draft

jesperhodge wants to merge 1 commit into
openedx:mainfrom
jesperhodge:jesperhodge/cbe-drop-bulk-update-row-locks

Conversation

@jesperhodge

@jesperhodge jesperhodge commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor

Description

bulk_update_competency_criteria() took two select_for_update() locks: one on the leaf group's lineage (leaf, course-level group, and the competency's root group) and one on the criteria it edits. This PR replaces both with plain reads and keeps everything else in the function, including its transaction.atomic() block and both archived checks.

The locks were added in #874 to keep a concurrent archive from slipping in after the archived check, and to make a concurrent identical request wait and find nothing to change. On main there is no code path that archives a group or a criterion, so the first purpose has no writer to defend against; when an archive path lands it will need its own locks, in a compatible order, for a lock here to protect anything. The second purpose buys one fewer redundant simple_history row in a rare double submit.

What the locks cost is larger than that. The lineage includes the competency's root group, a row shared by every course that uses the competency, so one bulk update serialized every other bulk update for that competency across the whole instance. InnoDB also takes a shared lock on the parent row for every foreign-key check, so while a bulk update held exclusive locks on the root and course-level group rows, every leaf-group or criterion insert under that competency, in any course, waited for it to commit. The two lock chains (tag then course run in associate_competency_criterion(), group rows then criterion rows here) do not cycle today, but the second chain is a deadlock surface for any future path that locks a group and then needs the tag or course-run lock.

Each save() still takes an exclusive row lock on its criterion, so the function now saves in primary-key order and returns in request order. Two overlapping requests therefore always wait on each other in the same order instead of each holding a row the other needs. The remaining cost of removal: each extra identical concurrent request writes one redundant simple_history row, and a cascading delete of a tag, group, or course run that commits between the read and a save() fails that one request with a DatabaseError and rolls it back. No delete path exists in this repo, so that race needs an administrative cascade.

The two locks in associate_competency_criterion() stay. They protect one root group per tag, no duplicate criterion for a derived leaf, and the per-course dominance check. None of those invariants can be expressed as a database constraint on MySQL, and both were designed in review on #847 and #875. The #847 request for a follow-up that replaces the Tag lock with a derived-column constraint is unchanged by this PR.

Testing

No new tests. The change has no behavior visible on SQLite, where Django drops FOR UPDATE silently, and a MySQL-only test asserting that no FOR UPDATE is issued would pin the implementation, which review on #847 asked to avoid. The existing bulk_update_competency_criteria tests cover the archived checks and the "not criteria of this group" path, and the full CBE API test module passes.

.venv/bin/pytest tests/openedx_learning/applets/cbe/test_api.py -q --no-cov
107 passed

pylint, pycodestyle, pydocstyle, isort --check-only, and mypy exit 0 on the touched file.

Related

Closes http://github.com/openedx/openedx-core/issues/916

🤖 Generated with Claude Code

bulk_update_competency_criteria() locked the group lineage and the criteria it edits. The lineage includes the competency's root group, shared by every course that uses the competency, so one bulk update serialized every other bulk update for that competency and, through InnoDB's shared lock on foreign-key parents, stalled criterion inserts under it in any course. The locks guarded against a concurrent archive, and no code path archives groups or criteria.

Without them, each extra identical concurrent request writes one redundant history row. The saves run in primary-key order, so two overlapping requests cannot deadlock on each other's criterion rows.

The tag and course-run locks in associate_competency_criterion() stay: they protect one-root-per-tag, duplicate criteria, and the per-course dominance check, none of which a database constraint can express.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@openedx-webhooks openedx-webhooks added the open-source-contribution PR author is not from Axim or 2U label Oct 28, 2026
@openedx-webhooks

Copy link
Copy Markdown

Thanks for the pull request, @jesperhodge!

This repository is currently maintained by @axim-engineering.

Once you've gone through the following steps feel free to tag them in a comment and let them know that your changes are ready for engineering review.

🔘 Get product approval

If you haven't already, check this list to see if your contribution needs to go through the product review process.

  • If it does, you'll need to submit a product proposal for your contribution, and have it reviewed by the Product Working Group.
    • This process (including the steps you'll need to take) is documented here.
  • If it doesn't, simply proceed with the next step.
🔘 Provide context

To help your reviewers and other members of the community understand the purpose and larger context of your changes, feel free to add as much of the following information to the PR description as you can:

  • Dependencies

    This PR must be merged before / after / at the same time as ...

  • Blockers

    This PR is waiting for OEP-1234 to be accepted.

  • Timeline information

    This PR must be merged by XX date because ...

  • Partner information

    This is for a course on edx.org.

  • Supporting documentation
  • Relevant Open edX discussion forum threads
🔘 Get a green build

If one or more checks are failing, continue working on your changes until this is no longer the case and your build turns green.

🔘 Update the status of your PR

Your PR is currently marked as a draft. After completing the steps above, update its status by clicking "Ready for Review", or removing "WIP" from the title, as appropriate.


Where can I find more information?

If you'd like to get more details on all aspects of the review process for open source pull requests (OSPRs), check out the following resources:

When can I expect my changes to be merged?

Our goal is to get community contributions seen and reviewed as efficiently as possible.

However, the amount of time that it takes to review and merge a PR can vary significantly based on factors such as:

  • The size and impact of the changes that it introduces
  • The need for product review
  • Maintenance status of the parent repository

💡 As a result it may take up to several weeks or months to complete a review and merge your PR.

@github-project-automation github-project-automation Bot moved this to Needs Triage in Contributions Oct 28, 2026
@jesperhodge jesperhodge changed the title perf: drop row locks from bulk criterion updates fix: drop row locks from bulk criterion updates Oct 28, 2026
@mphilbrick211 mphilbrick211 moved this from Needs Triage to Waiting on Author in Contributions Oct 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

open-source-contribution PR author is not from Axim or 2U

Projects

Status: Waiting on Author

Development

Successfully merging this pull request may close these issues.

2 participants