Skip to content

fix(instructor): bulk-enroll auto-async, chunking, duplicate guards - #100

Merged
AliAlfaifi merged 2 commits into
open-release/teak.nelpfrom
fix/bulk-enroll-limits
Oct 6, 2026
Merged

AliAlfaifi merged 2 commits into
open-release/teak.nelpfrom
fix/bulk-enroll-limits

Conversation

@AliAlfaifi

Copy link
Copy Markdown

Why

Bulk enrollment from the instructor dashboard (Membership → Batch Enrollment) fails in prod in three measured ways (2026-09-06→10-06):

  1. Sync + notify costs ~2 s/learner → ~60 learners exceed Cloudflare's 125 s → browser error → re-click → duplicate runs; learners mailed up to 8×.
  2. Async refuses JSON task_input > 10,000 chars (~330–380 emails) with an uncaught 500 "Task was not created".
  3. No guard against the same batch being submitted repeatedly.

What (each divergence marked # NELC: for the Dec-2026 re-port)

  • instructor_task/models.py: TASK_INPUT_LENGTH 10,000 → 60,000 (task_input is a TEXT column, no migration). Over-limit raises TaskInputTooLongError (subclass of AttributeError, so existing callers keep working).
  • instructor_task/api.py: split_enrollment_identifiers chunks a batch so each task fits; raises instead of truncating.
  • instructor/views/api.py (StudentsUpdateEnrollmentView, dashboard POST only):
    • auto-switch to async above BATCH_ENROLLMENT_SYNC_MAX_NOTIFY (30, notify on) / BATCH_ENROLLMENT_SYNC_MAX (100, notify off); response carries auto_switched_to_async, sync_limit;
    • batches too big for one task become several tasks (task_ids);
    • unfittable input → 400 with a clear message (was 500);
    • sync path takes a cache.add lock on (course, action, md5 of sorted identifiers) → 409 if already running, released in finally (BATCH_ENROLLMENT_LOCK_TIMEOUT_SECONDS=900).
  • instructor/utils.py: skip re-sending the notify email when the learner's latest audit row for this course already reached the requested state within BATCH_ENROLLMENT_EMAIL_DEDUPE_SECONDS (900); result row gets email_skipped: true.
  • membership.js: buttons disabled while a request is pending; server 400/409 messages shown; auto-switch/split explained to the instructor.
  • /bulk_enroll REST API unchanged (guarded by a guard_ui_request flag) so its response contract does not break.

Testing

Container (overhangio/openedx:20.0.5 + this fork, sqlite + mongo): baseline 348 passed → 378 passed in the touched modules (30 new tests); 856 passed across instructor, instructor_task, bulk_enroll. pylint/pycodestyle clean on changed files; node --check on the JS.

Known limits / to verify on stage

  • The lock needs a cache shared across LMS pods (Tutor uses Redis; not confirmed on the pods).
  • Email dedupe does not cover invited-but-unregistered learners (no audit→enrollment link).
  • One chunk can hold ~1,700 identifiers (~1 h with notify on until the pooled SMTP backend lands).
  • JS behaviour not exercised in a browser; ESLint not run.

Related: nelc/eox-nelp feat/pooled-smtp-email-backend (email speed), nelc/nassau feat/pooled-smtp-email-backend-stage.

🤖 Generated with Claude Code

AliAlfaifi and others added 2 commits October 6, 2026 12:46
…-submit guards

Instructor-dashboard batch enrollment broke as the learner count grew:
- sync + "notify by email" costs ~2 s/learner, so ~60 learners exceed the
  proxy's 125 s; the browser errors, the instructor re-clicks, and the same
  learners are mailed again (up to 8x observed);
- the async path refused task_input > 10,000 chars with an uncaught
  AttributeError -> HTTP 500.

Changes (each divergence marked `# NELC:` for the next upgrade re-port):
- TASK_INPUT_LENGTH 10000 -> 60000 (task_input is a 65,535-byte TEXT column;
  no migration). Over-limit raises TaskInputTooLongError (an AttributeError
  subclass) which the view turns into a 400 JSON message; nothing truncated.
- StudentsUpdateEnrollmentView: a batch above BATCH_ENROLLMENT_SYNC_MAX_NOTIFY
  (30, email on) / BATCH_ENROLLMENT_SYNC_MAX (100, email off) switches to async
  and says so in the JSON (auto_switched_to_async, sync_limit); a batch larger
  than one task input is split into several tasks (task_ids). Only the
  dashboard POST is guarded; the bulk_enroll REST API keeps its contract.
- Sync path takes a cache.add lock per (course, action, identifiers) held for
  the request (BATCH_ENROLLMENT_LOCK_TIMEOUT_SECONDS=900, released in finally);
  a second identical request gets 409.
- A learner whose latest ManualEnrollmentAudit row (same course) already
  reached the requested state within BATCH_ENROLLMENT_EMAIL_DEDUPE_SECONDS
  (900) is not mailed again; enrollment itself still runs.
- membership.js disables the enroll/unenroll buttons while a request is
  pending, shows the server's 400/409 message, and explains an automatic
  switch to background processing / chunking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…static-assets-check

The runner's pip 26 builds the loremipsum sdist against setuptools>=82,
which removed pkg_resources, so 'Install Full Python Deps for Collection'
fails whenever the pip cache misses. Install requirements/pip.txt first,
as the other workflows already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@AliAlfaifi
AliAlfaifi merged commit da1807c into open-release/teak.nelp Oct 6, 2026
47 checks passed
@AliAlfaifi
AliAlfaifi deployed to open-release/teak.nelp October 6, 2026 18:10 — with GitHub Actions Active
@AliAlfaifi
AliAlfaifi deployed to open-release/teak.nelp October 6, 2026 18:10 — with GitHub Actions Active

This branch was successfully deployed

1 active deployment
open-release/teak.nelp — ee8daffc Deployed Oct 6, 2026 by AliAlfaifi via create-jira-issue / create_jira_issue #38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant