Appearance
Cloudflare Workers
Two standalone Workers handle the jobs that need to run without a browser open: judging OA submissions in the background, and sending reminder emails on time. Each is its own Wrangler project with its own deploy lifecycle — a website deploy never touches them.
Shared foundation: service-account access to Firestore
Workers can't use the Firebase SDK (weight, Node APIs). Both talk to Firestore over REST, authenticating as a service account:
rendering diagram…
drag to pan · ctrl / ⌘ + scroll to zoom
The private key lives in Worker env via wrangler secret put, never in the repo. Because service accounts bypass Firestore rules, every write these Workers make uses an explicit precondition — that discipline replaces the rules.
worker/ — background OA judging
Problem it solves: during an OA, a student shouldn't sit watching a spinner while hidden tests run — and shouldn't be able to avoid judging by closing the tab.
Cycle (cron):
- Query
users/*/submissions(collection-group) for docs taggedoaAttemptIdand not yet judged. - Claim each via a patch with an
updateTimeprecondition — if another invocation already claimed it, the precondition fails and this one skips. No locks, no double-judging. - Call the same Pages Function judging path the interactive runner uses, over HTTP. One judging implementation in the whole system — the Worker adds orchestration, never a second grader.
- Write the verdict back; the student's client sees it via
onSnapshot.
Deliberate non-goal: the Worker does not compute attempt analytics. Scoring/aggregation is oaEngine.js#computeAttemptResult on the result page — the Worker judges submissions, the client scores attempts. Keeping those separate means a scoring tweak never requires a Worker deploy.
worker-reminders/ — reminder emails
Problem it solves: GitHub Actions cron is best-effort and silently skips under load — unacceptable for "your OA starts in 1 hour."
Cycle (every minute):
- Find events whose
d7 / d3 / d1 / h1windows are due and unsent. - Take an optimistic claim per event+window (
reminderClaimswith a create precondition) — overlapping invocations can't double-send. - Send via Resend; on success set
remindersSent.<window>permanently; on failure release the claim so the next tick retries.
Recipient email comes from the users/{uid} profile — the field whose staleness caused the silent-skip bug, fixed by sign-in-time sync + backfill (Scheduling, Authentication).
Operational notes
| Concern | Judging Worker | Reminders Worker |
|---|---|---|
| Trigger | cron (drains queue) | cron, per-minute |
| Idempotency | updateTime precondition claim | reminderClaims create-precondition |
| External calls | Pages Function → Judge0 | Resend API |
| Secrets | service-account key, site URL | service-account key, RESEND_API_KEY |
| Failure posture | unclaimed docs picked up next tick | claim released, retried next minute |
| Deploy | wrangler deploy in worker/ | wrangler deploy in worker-reminders/ |
Both Workers are intentionally stateless between ticks — all state is in Firestore, so a Worker redeploy mid-cycle loses nothing and the claim semantics make replays harmless.