Skip to content

Stop fetching full quiz attempt review just to read the grade - #118

Merged
Celeo merged 1 commit into
masterfrom
fix/academy-quiz-grade-egress
Jul 30, 2026
Merged

Stop fetching full quiz attempt review just to read the grade#118
Celeo merged 1 commit into
masterfrom
fix/academy-quiz-grade-egress

Conversation

@Celeo

@Celeo Celeo commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Summary

  • VATUSAMoodle::getQuizAttempts() called Moodle's mod_quiz_get_attempt_review web service once per attempt to read a single field (grade) — but that endpoint returns the entire rendered attempt review (every question, every response, ~2.17 MB) per call.
  • Driven by moodle:competency (every 10 min) and moodle:sendexamemails (every 5 min), this amounted to ~33,000 calls/day and ~2 TB/month of outbound traffic from academy.vatusa.net — ~98.6% of that droplet's total egress. This was surfaced while costing out an Azure migration: the bill for that traffic alone (~$193/mo on metered Azure egress) would exceed the entire nonprofit grant.
  • The review's grade is just quiz_rescale_grade(sumgrades, quiz) = round(sumgrades / quiz.sumgrades * quiz.grade). Both sumgrades (per attempt, in mdl_quiz_attempts) and quiz.sumgrades/quiz.grade (per quiz, in mdl_quiz) already live in the Moodle database this class already queries via DB::connection('moodle') for other fields — no need to go over HTTP at all.
  • This PR computes the grade from the DB instead: a new getQuizMeta() helper (memoized per quiz id for the life of the instance) plus a single batched whereIn query for attempt sumgrades, replacing the per-attempt review call in getQuizAttempts().
  • Also fixes a second, independent call site in SendAcademyRatingExamEmails.php (the Basic ATC exam branch) that hit mod_quiz_get_attempt_review directly for the same reason, using the same DB-based computation.
  • No behavior change for any consumer: same fields returned, same fail-closed [] on missing/unresolvable data.
  • Added a CLAUDE.md note about compose.dev.yml's Redis not working out of the box (config/database.php hardcodes scheme: tls, incompatible with the plain dev Redis container) — found while setting up local verification for this fix.

Test plan

  • Verified in Docker (compose.dev.yml) against a fixture moodle database (mdl_quiz/mdl_quiz_attempts rows) with the real mod_quiz_get_user_attempts call stubbed (the only thing that stub allows — any other web-service call, e.g. the old review call, would throw): grades match the old review-derived formula exactly for both 1:1 and non-trivially-scaled quizzes, getQuizMeta() memoization confirmed, fail-closed behavior preserved for a missing attempt and an unknown quiz id.
  • ./vendor/bin/phpunit — 18/18 passing, both natively and inside the Docker container.
  • Post-deploy: confirm mod_quiz_get_attempt_review call volume in academy.vatusa.net's Apache access log drops to ~0, and outbound bandwidth drops toward the ~300 GB/month baseline of genuine site traffic.

🤖 Generated with Claude Code

VATUSAMoodle::getQuizAttempts() called Moodle's mod_quiz_get_attempt_review
web service once per attempt to read a single field (grade), but that
endpoint returns the entire rendered attempt review — every question, every
response, ~2.17 MB — per call. Driven by moodle:competency (every 10 min)
and moodle:sendexamemails (every 5 min), this amounted to ~33,000 calls/day
and ~2 TB/month of outbound traffic from academy.vatusa.net, ~98.6% of that
droplet's total egress (see the migration cost analysis that surfaced this).

The review's grade is just quiz_rescale_grade(sumgrades, quiz) =
round(sumgrades / quiz.sumgrades * quiz.grade), and both sumgrades (per
attempt) and quiz.sumgrades/quiz.grade (per quiz) already live in the
Moodle database this class already queries via DB::connection('moodle')
for other fields. Compute the grade from there instead, batched per call
and memoized per quiz for the life of the instance — no behavior change
for any consumer, just no more multi-megabyte HTTP round-trips.

Also fixes a second, independent call site in SendAcademyRatingExamEmails
(the Basic ATC exam branch) that hit the same endpoint directly for the
same reason.

Verified in Docker (compose.dev.yml) against a fixture 'moodle' database:
grades match the old review-derived formula exactly for both 1:1 and
scaled quizzes, fail-closed behavior on missing data is preserved, and no
calls to mod_quiz_get_attempt_review occur.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@Celeo
Celeo merged commit 567a18c into master Jul 30, 2026
3 checks passed
@Celeo
Celeo deleted the fix/academy-quiz-grade-egress branch July 31, 2026 22:50
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