Skip to content

WIP: Student data retention feature - #1448

Open
lupari wants to merge 23 commits into
devfrom
CSCEXAM-1658
Open

lupari wants to merge 23 commits into
devfrom
CSCEXAM-1658

Conversation

@lupari

@lupari lupari commented Sep 25, 2026

Copy link
Copy Markdown
Collaborator

No description provided.

@lupari
lupari force-pushed the CSCEXAM-1658 branch 4 times, most recently from d861e4d to 15c761e Compare September 28, 2026 21:23
lupari and others added 22 commits September 30, 2026 12:47
- One retention module in app/features/retention runs all passes daily:
  auto-lock of unassessed attempts, removal of expired attempt content,
  grading records, bookings, host-side visitor reservations, host copies
  of visiting attempts and inactive pure-student accounts
- Periods per data type under exam.retention.* in application.conf,
  institution-chosen ranges enforced at startup
- New exam.locked_at (evolution 148) set when an assessment is locked,
  backfilled from exam records or grading time
- Visiting reservations are deleted at XM before local rows go
- Ships with exam.retention.dryRun = true. While dry run is on, the old
  ExamExpirationService and ExternalExamExpirationService keep running
- Unit, database and fake-XM tests in test/retention

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- /app/config returns the effective retention policy (periods after
  range checks, dry-run mode, batch size) instead of expirationPeriod
- Admin settings page lists the retention periods and whether the job
  is still in dry run
- Removed the unused ConfigReader.getExamExpirationPeriod and the
  i18n_exam_expiration_period translation

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- RetentionSafetySpec snapshots every table and fails if a run removes
  or changes any row not created for the expired student
- DB-level tests for maturity, course end and exam period windows,
  aborted, already DELETED, never started and host reservations
- Fixed attempt content removal failing for question copies with an
  attachment: the file path was read after the row was deleted
- Retention passes have readable labels, logged one line per pass
- Shared spec helpers moved to RetentionSpecBase

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Runs nightly at 02:00 in the default time zone instead of an hour
  after startup
- Candidate queries read pages of 500 rows and stop once a batch is
  found; dry run still counts everything, page by page
- Replaced per-candidate queries with EXISTS subqueries
- Evolution 149 indexes the date columns the passes filter by
- Batch size default raised to 2000; the log shows "due N+" when more
  remain, and the run duration

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- No-shows, assessment transfers and collaborative assessments leave
  the send queue when XM answers 404, the request cannot be built, or
  they still fail 30 days after they became ready (IopDelivery)
- Giving up is logged once; retries log XM's status and answer
- Given-up attempts are kept and marked delivery_abandoned_at
  (evolution 150, left out of exchanged JSON); the retention job
  removes them with their booking

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The retention job deletes a visiting booking locally when the XM
  delete still fails 30 days after the booking became due, so a
  missing host no longer keeps a student's data for good
- XM removes its own copy with its nightly expiry
- Booking candidates carry their due time for this

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Exam attempts that still fail 30 days after they became ready are
  retried once a week instead of given up, until the retention job
  removes them; a receiver failing only until fixed (e.g. a version
  mismatch) then gets them without anyone acting
- Only a 404 takes an attempt out of the pipeline for good
- No-shows keep giving up after 30 days
- Evolution 150 adds delivery_attempted_at for the weekly cadence

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Fixed limits common to all deployments live in RetentionLimits:
  the institution-chosen attempt ranges, the nightly run hour and
  the XM give-up and weekly retry intervals
- Per-deployment periods stay in application.conf
- Removed an unused give-up constant from NoShowHandlerImpl

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Retention deletes a user with a plain Ebean delete instead of raw
  SQL; the ManyToMany cascade removes only the role and permission
  links, not the shared rows
- The safety test gives both students a shared permission, so a
  cascade reaching it would fail the test

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The no-show send matches every delivery decision, removing the
  exhaustivity warning; a no-show never gets the weekly retry, so it
  is treated like any other retry

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Candidate queries page by id (keyset) instead of OFFSET, so a page
  costs the same at any depth; on 1M attempts a deep page went from
  1.5 s to 3 ms and a full dry run counts every pass in 26 s
- A dry run still counts every due item, but its report lists only
  the next real run's batch per pass, capping memory and the CSV at a
  real run's size (16 MB instead of ~290 MB on the same data)
- Batch size default raised to 20000: a real run handles 80k items in
  about 2 minutes, clearing a mature installation's backlog in about
  a month instead of a year

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Evolution 151 sets last_login = now() where it is null, so accounts
  that never logged in start their inactivity period at release
- Enrolments with no reservation, event or enrolled_on get the start
  of the student's own attempt, or now(); otherwise they never expire
  and keep the account too
- The spec runs the evolution's SQL against test data, as Play
  applies evolutions before any spec starts

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The no-show check loaded every enrolment not marked as a no-show,
  with its exam, and filtered in Scala; on 1M attempts that was 1.02M
  rows and the server was killed for memory
- The exam state and time limits now go in the query, matching what
  NoShowHandler keeps, so only possible no-shows are loaded
- Belongs on its own ticket; fixed here to unblock retention testing

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- scripts/internal/seed_bulk.py fills a dedicated database with users,
  rooms, courses, exams and deep-copied student attempts in every
  lifecycle state, spread evenly over a configurable history
- Loads with COPY and its own ids, then advances the sequences;
  refuses other databases and runs while the app is connected
- Defaults: 20k users and 1M attempts over 10 years, about 26 min and
  13 GB

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- checkout, setup-node and upload-artifact v4 -> v7, setup-java
  v4 -> v6, moving off the deprecated Node 20 runtime
- None of their breaking changes apply here: hosted runners, the
  temurin distribution, no registry-url, standard artifact inputs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The settings endpoint reports the retention batch size, whose
  default is now 20000

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The seeder's help now shows how to back up the seeded database
  and restore it instead of seeding again, including the analyze
  step pg_restore leaves out

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Seeded users get addresses under example.invalid (RFC 2606), so
  reminders, reports and no-show notices cannot reach anyone when the
  seeder is used on a server whose mailer is not mocked
- The eppn stays under funet.fi, which dev login needs

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Reviews and enrolments are looked up separately for the exams a
  teacher owns and those they inspect; an OR across the two joins
  kept either index from being used, so each call scanned the whole
  exam table
- The parent exam and course are fetched with the reviews instead of
  lazily per participation
- On 1M attempts the review queries per teacher went from 655 and
  262 ms to about 17 ms each

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- The notifier loads only exams whose release type it handles;
  IMMEDIATE and NEVER were never sent by it but were reloaded every
  15 minutes for as long as they stayed GRADED

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
- Each participation and external exam is checked on its own, so one
  that fails no longer keeps the ones after it from ending
- A reservation without a machine falls back to the default time
  zone instead of throwing, a failed teacher email skips only that
  teacher, and the review deadline is read once per run
- ScheduledJobsSpec covers this (checked to fail on the old code)
  and the weekly report's owner and inspector lookups

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Retention keeps the bare student copy, marked DELETED, until pass D
removes it with the booking. The reservation and examination event
listings hid every DELETED exam from teachers, so such bookings
vanished from their view early.

- Teachers see DELETED student copies (with a parent); bookings on an
  exam the teacher deleted stay hidden
- DELETED is a selectable state and sorts after ARCHIVED
- A DELETED copy counts as attended, so a stale noShow doesn't show
- DELETED rows don't link to the assessment page

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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