Skip to content

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:

diagram100%
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):

  1. Query users/*/submissions (collection-group) for docs tagged oaAttemptId and not yet judged.
  2. Claim each via a patch with an updateTime precondition — if another invocation already claimed it, the precondition fails and this one skips. No locks, no double-judging.
  3. 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.
  4. 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):

  1. Find events whose d7 / d3 / d1 / h1 windows are due and unsent.
  2. Take an optimistic claim per event+window (reminderClaims with a create precondition) — overlapping invocations can't double-send.
  3. 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

ConcernJudging WorkerReminders Worker
Triggercron (drains queue)cron, per-minute
IdempotencyupdateTime precondition claimreminderClaims create-precondition
External callsPages Function → Judge0Resend API
Secretsservice-account key, site URLservice-account key, RESEND_API_KEY
Failure postureunclaimed docs picked up next tickclaim released, retried next minute
Deploywrangler 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.

Ediky Workflow — internal engineering documentation.