perf(admin): batch the delete cascade - #121
Merged
udaycodespace merged 1 commit intoAug 30, 2026
Merged
Conversation
Deleting an account issued one awaited write per enrolment and a full five-call cascade per authored course. A teacher with 40 courses and 300 enrolments was around 500 sequential operations, each paying a latency hop, inside one HTTP request the admin dashboard awaits with no progress indication. The same defect udaycodespace#96 and udaycodespace#104 fixed on the read path. decrementEnrolledCounts is one bulkWrite now. The operations are unchanged — still one guarded $inc: -1 per enrolment rather than a single $inc: -count, because `enrolled` has drifted on existing data and the `enrolled > 0` filter re-evaluated per operation is what stops the counter going negative. ordered: true is load-bearing: two unordered decrements against a course sitting at 1 could both see the guard satisfied. Confirmed against a real MongoDB, not only the stub. removeCoursesDependents takes many ids and cascades them in four deleteMany calls whatever the count; removeCourseDependents is the single-course wrapper, unchanged for courseDeletionController. A lone id stays an equality match rather than a one-element $in, so the common path reads in the profiler as the query it always was. The authored-course read asks for `_id sections` instead of whole documents. sections carries every section's title, description and path, so on a twenty-section course it is the largest field there is, and the loop reads nothing else. authoredCourses now counts what the delete removed rather than what the find saw. All 10 existing tests pass unchanged, which is the point — same rows, same counters, same summary. The 11 added ones assert the round-trip count, which nothing was watching. The stub collection grew a roundTrips log, a bulkWrite, a chainable select() and $in matching to make that assertable. Video cleanup stays one call per course: every section video is its own unlink, so batching it would trade latency for file descriptors.
udaycodespace
self-requested a review
August 28, 2026 09:31
Owner
|
Reviewed this, @MOHITKOURAV01 . The delete cascade optimization is well scoped and preserves the existing behaviour while significantly reducing database round trips. The All backend tests are passing, including the 11 new tests. Approved and merging. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #116.
The defect
removeUserDependentsworked one row and one course at a time.The learner counts were one awaited round trip per enrolment, not per
course:
The authored courses were a full five-call cascade each.
E5 × AA teacher with 40 courses and 300 enrolments was ~500 sequential operations,
each paying a latency hop, inside one HTTP request that
deleteUserControllerawaits before answering — with no progress indication on the admin dashboard.
find({ userId })also loaded whole course documents;sectionscarries everysection's title, description and path, so on a twenty-section course it is the
largest field there is, fetched to be thrown away.
Same defect class as #96 and #104, on the write path.
What is here
decrementEnrolledCountsis onebulkWrite. The operations are unchanged— still one guarded
$inc: -1per enrolment rather than a single$inc: -count, becauseenrolledhas drifted on existing data and theenrolled: { $gt: 0 }filter re-evaluated per operation is what stops thecounter going negative and rendering
LEARNERS: -3on a card.ordered: trueis load-bearing: two unordered decrements against a coursesitting at 1 could both see the guard satisfied. Verified against a real
MongoDB, not only the stub:
removeCoursesDependents(courseIds, ...)takes many ids and cascades themin four
deleteManycalls whatever the count.removeCourseDependentsis thesingle-course wrapper around it, unchanged in signature and return shape,
because
courseDeletionControllercalls it directly. A lone id stays anequality match rather than a one-element
$in, so the common path reads in theprofiler as the query it always was.
Projections:
"_id sections"for the authored courses and"courseId"forthe user's own enrolments — the only fields either loop reads.
authoredCoursesnow counts what the delete removed rather than what thefind saw.
What stayed sequential
The video cleanup. Every section video is its own
unlink, so it is the onepart of a cascade that is genuinely per-item, and firing an unbounded number at
once trades a latency problem for a file-descriptor one. Still one call per
course, receiving the course document, with a test pinning that.
Tests
All 10 existing tests pass unchanged against both implementations. That is
the point: same rows, same counters, same summary.
The 11 added ones assert the thing that actually changed, which nothing was
watching — 60 decrements across three courses is one round trip with the
arithmetic still 40→10 / 40→20 / 40→30; the bulk write is
ordered; an emptymap issues no write; 25 courses cascade in one
deleteManyper collection;deleting a teacher with 20 courses touches
Courseexactly twice; theprojections are what they should be; one course still emits
{ courseId: "c1" }.The stub collection grew a
roundTripslog, abulkWrite, a chainableselect()and$inmatching, so cost is assertable without a database.Checklist
cd backend && npm test— 424 pass (413 onmain, 11 added)removedblock on the delete-user response unchangednpm run lintinfrontend/— not run; no frontend files touched.Note
The
enrolledcounter drift is not fixed here, only kept from getting worse. Arecount would rewrite history an admin has been looking at, which is a decision
rather than a cleanup.
Write-up:
docs/issue-116-cascade-delete-batching.md.