fix: stop orphaning google calendar events on schedule re-upload#501
Merged
Conversation
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #500
Every course event was appearing twice (or more) in users' Google Calendars.
Root cause
Since 035a4bd (June 13),
CourseProcessorServicerancourse.meeting_times.destroy_allon everyPOST /api/process_courses(every extension schedule upload). Meeting times carriedhas_many :google_calendar_events, dependent: :destroy, so the tracking rows were destroyed without any Google API call — the real events stayed on the calendar, now invisible to the app. The re-ingested meeting times got new IDs, so the next sync created a fresh copy of every event next to the stranded one. Each re-upload added another generation, for every user enrolled in the course, and no cleanup job could see the orphans because nothing reconciles against the Google side.sequenceDiagram participant E as Extension participant P as CourseProcessorService participant DB as google_calendar_events participant G as Google Calendar E->>P: POST /api/process_courses P->>DB: meeting_times.destroy_all (cascade) Note over DB,G: rows destroyed, no API call —<br/>events stranded in Google P->>P: re-ingest meeting times (new IDs) P->>G: next sync: no tracked event for new IDs G->>G: duplicate created next to orphanChanges
CourseProcessorService: upsert meeting times in place instead of destroy-all-and-recreate.MeetingTimesIngestServicenow returns the IDs it touched; only rows absent from the upload are destroyed. This keeps meeting time IDs (and therefore event tracking rows andevent_preferencerecords, which were also being wiped every upload) stable.Course::MeetingTime/FinalExam:google_calendar_eventsassociation changed fromdependent: :destroytodependent: :nullify. The tracking row is the only pointer to the real Google event — orphaned rows are now picked up byCleanupOrphanedCalendarEventsJob, which deletes from Google before removing the row. (UniversityCalendarEventalready worked this way.)CleanupUntrackedGoogleEventsJob(new, one-time remediation): lists events on each app-managed calendar via the service account and deletes any not tracked ingoogle_calendar_events. Skips cancelled events and user-edited instances of tracked recurring series. Supportsdry_run: trueand per-calendar scoping.Remediation rollout
Tests
CourseProcessorService: meeting time IDs stable across re-processing, tracking rows preserved, stale meeting times removed with events nullifiedMeetingTimesIngestService: returns touched IDs, reuses rows on re-ingestGoogleCalendarEvent: nullified (not destroyed) when meeting time / final exam is destroyedCleanupUntrackedGoogleEventsJob: deletes only untracked events, dry-run mode, 404 toleranceFull suite passes locally (
bundle exec rspec, 19 examples, 0 failures); rubocop clean.