Skip to content

Speed up and cache the iCal feeds - #431

Merged
cycomachead merged 5 commits into
mainfrom
ical-calendar-performance
Nov 2, 2026
Merged

cycomachead merged 5 commits into
mainfrom
ical-calendar-performance

Conversation

@cycomachead

Copy link
Copy Markdown
Member

Problem

/calendar.ics?full=1 and /conferences/:id/schedule.ics are polled by calendar clients and were expensive on our 512 MB dyno. icalendar_proposals called Event#time, #room, #track and #difficulty_level for each event, so query count grew with the number of sessions. /calendar.ics also looked up each conference's program and venue separately.

Fix

  • icalendar_proposals preloads what it reads (event type, difficulty level, program, track with room and submitter, event schedules with room) and finds the selected event schedule in memory. The time and room logic is the same as Event#time and Event#room.
  • /calendar.ics preloads venue, program and selected schedule for all conferences, and loads rooms with the event schedules (they're used for sorting). It no longer loads speakers and submitters, which the feed never used.
  • Both feeds are cached in Rails.cache for up to 12 hours:
    • /calendar.ics: key is the row count and latest updated_at of conferences, programs and venues, plus full/short and the request host.
    • schedule.ics: key is the program, selected schedule, conference and venue cache versions, plus the request host.
    • Events, event schedules, schedules, tracks, event types, difficulty levels and rooms all touch their program (EventSchedule → Schedule → Program cascades). The new specs check that rescheduling a session and editing a venue both invalidate the cache.
    • Neither feed is user-specific. Authorization still runs before the cache lookup.

Two existing bugs in the same lines, fixed here:

  • Feeds containing any event with a track or difficulty level returned a 500. e.categories << "..." pushed a plain String that to_ical can't serialize. It now uses append_categories.
  • The feed was rendered with render inline:, which treats the string as an ERB template, so event text was evaluated. It now uses render plain: with text/calendar.

Query counts

Measured in spec/requests/ical_feeds_spec.rb with the test null cache, so these are uncached renders. Counts include the auth and session queries.

Endpoint Before (2 → 10 events) After (2 → 10 events)
/calendar.ics?full=1 28 → 60 23 → 23
/conferences/:id/schedule.ics 18 → 42 14 → 14

On a cache hit, /calendar.ics runs only the auth queries and three COUNT/MAX queries.

Output

I compared the before and after output on the same seeded data: short feed, full feed with a self-organized track, nil room, nil event type and unscheduled events, and two schedule.ics feeds. The only difference is line endings. They are now CRLF, as RFC 5545 requires. Before, they were LF because the ERB rendering converted them. For "before", I applied the append_categories fix, since the original code returned a 500.

Trade-offs

  • DTSTAMP now records when the feed was cached, not when it was requested. That is allowed by the spec, and clients use UID and LAST-MODIFIED.
  • The cache key misses data written without callbacks (update_column and the like). No app code does that for these models today. Entries expire after 12 hours regardless.
  • The cached feed lives in whatever Rails.cache store production uses. If that is an in-process memory store, a few hundred KB of iCal text sits in RAM.
  • /calendar.ics is admin-only today: Ability never grants :calendar to guests or regular users, so anonymous requests redirect to /. I left that alone. The new spec signs in as admin.

🤖 Generated with Claude Code

cycomachead and others added 2 commits October 31, 2026 01:17
- Schedule page: cache the FullCalendar JSON string (Program#fullcalendar
  _events_json) instead of building it from cached records on every
  request. Drop the view fragment cache: its key included that JSON, so
  it never saved work, and it froze the "now" marker for the cache's life.
- Breaks look up the venue's room guids once, not once per break.
- Personal schedule: User#planned_event_ids gets the user's favourited,
  speaking and volunteering event ids in two queries, replacing
  Event#planned_for_user? (three queries per event).
- All events: find unscheduled events with where.not instead of loading
  every confirmed event and subtracting arrays.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
icalendar_proposals ran several queries per event (Event#time, #room,
#track, #difficulty_level). It now preloads what it reads and looks up
the selected event schedule in memory. /calendar.ics also preloads each
conference's venue, program and selected schedule.

Both feeds are now cached in Rails.cache. /calendar.ics is keyed on the
count and latest updated_at of conferences, programs and venues;
schedule.ics on the program, selected schedule, conference and venue.
Events, event schedules, schedules, tracks and rooms all touch their
program, so edits to sessions invalidate the cache.

Also:
- Render the feed with render plain instead of render inline, so event
  text is no longer evaluated as an ERB template. Lines now end in CRLF
  as RFC 5545 requires.
- Use append_categories for the difficulty and track categories. Pushing
  a plain String onto categories made to_ical raise, so any feed with a
  track or difficulty level returned a 500.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
cycomachead and others added 3 commits November 1, 2026 22:52
* origin/main:
  Render iCal feeds as plain text/calendar responses
  Paginate admin revision history and trim admin events preloads
  Load nav conference links with SQL instead of per-conference can?
  Share cached event cards between viewers who see the same card
  Find happening now sessions in SQL with HappeningNowQuery

# Conflicts:
#	spec/controllers/schedules_controller_spec.rb
…ance

* origin/main:
  Render iCal feeds as plain text/calendar responses
  Paginate admin revision history and trim admin events preloads
  Load nav conference links with SQL instead of per-conference can?
  Share cached event cards between viewers who see the same card
  Find happening now sessions in SQL with HappeningNowQuery

# Conflicts:
#	app/controllers/conferences_controller.rb
#	app/controllers/schedules_controller.rb
Both rewrite SchedulesController#show. Keep #429's per-format structure
and this branch's cached iCal output.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@cycomachead

Copy link
Copy Markdown
Member Author

This branch now also contains #429, which rewrites the same SchedulesController#show. Merge #429 first; this PR's diff will then shrink back to the iCal changes.

@cycomachead
cycomachead merged commit 16676d3 into main Nov 2, 2026
16 checks passed
@cycomachead
cycomachead deleted the ical-calendar-performance branch November 2, 2026 07:21
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