Repository navigation
Email a class in batches and keep track of who was emailed - #125
Conversation
Emailing more than about 200 students failed (#86): every email went out inside one request, about 0.3 s each, so a large class ran into the server's timeout (61 s on Heroku, 120 s in the Docker image), and since the students were only marked as emailed once at the end, none of those who got the email were recorded. - Save who was emailed after each batch of 50, so a send that stops partway still records the students who got their email. - Prefill the email page with the seated students not emailed yet, and say who was left out, with a link to email them again too, so after a send that stopped partway the page picks up with the rest. - Send from the page a batch of 50 students per request, through a new JSON endpoint, showing progress, so no request comes near a timeout however large the class is. CC and BCC go with the first batch only. Without JavaScript the form still sends everyone in one request. Fixes #86 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage for e2e tests for Python 3.14
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #125 +/- ##
==========================================
+ Coverage 66.14% 66.34% +0.19%
==========================================
Files 29 29
Lines 2369 2383 +14
==========================================
+ Hits 1567 1581 +14
Misses 802 802 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
|
🚀 Staging Preview Deployed! URL: https://preview-seating-pr-125-137456085712.us-west1.run.app |
Coverage for unit tests for Python 3.14
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Coverage for e2e tests for Python 3.14
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
CC is how course staff see a student's reply-all, so every student's email has to carry it: send the form's CC and BCC with every batch, not just the first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage for unit tests for Python 3.14
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Coverage for e2e tests for Python 3.14
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Does the db already track who was emailed? I guess so? (Do we ever expire this if the seat changes? separate issue..) It seems like this requires the user to keep the page open, since the JS is still polling? If so we'll want to prevent people from closing the tab prematurely and maybe give a more clear estimate of progress. This is still definitely better than what's there today, but I think we will still want a background job system. |
Yes, but it only writes to the DB after everyone is emailed. So if the emailing action times out before everyone is emailed, then the DB doesn't update.
This expires when reassigning one student on their assign page, bulk assigning students, deleting an assignment and assigning again later, and if on import a student's preferences no longer fit their seat, the seat is removed and their emailed status is reset. There's two times where the email may be out of date but the email status remains: if you edit a room's name or time, and if you import the student with a new seat and with emailed as true.
True, the user has to keep the page open for everything to finish. If the tab closes partway, the batch already sent finishes on the server and those students are marked emailed, but no subsequent batches are sent. Reloading/navigating back to the page will show accurately who has and who has not been emailed, but I could see staff assuming that they clicked email and didn't get any warnings so everyone got emailed. |
The page sends each batch itself, so closing it stops the students not emailed yet from being emailed. While sending, it now shows a progress bar, a note to keep the page open, and once the first batch is back an estimate of the time left; leaving the page asks first. All of it is removed when sending finishes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Coverage for e2e tests for Python 3.14
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Coverage for unit tests for Python 3.14
|
|||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Ah, I didn't realize we actually saved whether we had emailed a student. We should fix the data issue separately. The progress bar is nice!, I would add a Unfortunately, based on my experience, ~1.5 minutes for 300 emails is actually pretty good. The basic SMTP/sending APIs are just kinda slow. we should still move to a background job service after we get this new deploy running. |
|
The |


Summary
Fixes #86: emailing a large class failed partway, and nobody was marked as emailed.
Why it failed:
--timeout 61killed the worker at about 61 s.GUNICORN_TIMEOUT), roughly 400 students, and the lost-progress bug happens wherever a send stops partway.Changes
Save who was emailed after each batch (
server/services/email/__init__.py):email_about_assignmentsends in batches of 50 (SEND_BATCH_SIZE) and commits after each one, so a send that stops partway still records everyone who got the email.Pick up where a send stopped (
server/views.py,email.html.j2): the email page fills TO with the seated students not emailed yet, instead of every student, and says who it left out:Students without a seat used to be in TO and always failed; now they're left out.
Send a batch per request (
server/views.py,email.html.j2):POST /<exam>/students/email/send/emails the students into_addrand returns{"sent": [...], "failed": [...]}as JSON, or{"errors": ...}with a 400 if the form is invalid.No request comes near a timeout however large the class is, on Heroku, staging or production. This needs no extra resources on staging: sending is waiting on the mail server, not CPU or memory, and Cloud Run only runs while a request is being served.
Tests
tests/unit/test_email_batches.py:?include_emailed=1includes them again.tests/e2e/test_email_batches.py: in Chrome, with 121 seated students, Send makes three requests (50, 50 and 21 students), each with the CC. During each request the page shows the count so far and the progress bar, and leaving the page asks first; once done, both are gone and the page shows the summary and doesn't navigate away. The test server has no SMTP server, so every email fails, and no student is marked emailed.9 of the 11 unit tests and the e2e test fail on
main. Locally: unit 277, e2e 14, a11y 4 passing.Also checked by hand in Chrome against the app with sending replaced by saving each email to a file, with 121 seated students, 3 of them already emailed:
🤖 Generated with Claude Code