Added template pre-warming (Stage 2) - refs #1362 - #1393
Merged
Conversation
philayres
force-pushed
the
prewarm-templates-1362
branch
from
August 27, 2026 16:22
276eb85 to
928a5d3
Compare
This was referenced Sep 2, 2026
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.
Summary
Stage 2 of #1362 (template pre-warming). Stage 1 (#1377) content-addressed compiled Handlebars artifacts so identical rendered content is shared across users; this closes the remaining gap - the very first login after a restart/deploy still pays the full node-CLI compile cost, and a brand-new user's first login is always cold.
Design diverges from the original phases 1/3/4 - see the full writeup posted to the issue: #1362 (comment)
Key differences from the original plan, both driven by what Stage 1 already left in place:
rake prewarm:templatesspawned once at server boot, not an in-process background thread with a poll loop/quiet-period debounce. Trade-off: a mid-day generation rotation from a config change is not pre-warmed - the next request pays that cost, unchanged from today.Phase 0 - measurement gate
Before building phases 1-4, benchmarked
GET /pages/:id/templatefor the 1st/2nd/3rd distinct user after a simulated restart (spec/requests/pages/handlebars_compile_cost_benchmark_spec.rb, opt-in viaRUN_BENCHMARKS=true, tag:benchmark):This confirms Stage 1 already eliminates the cost for every user after the first. Went ahead with Stage 2 anyway, since the first-request-per-restart and brand-new-user cases are still real.
What's added
PrewarmController+Prewarm::MasterTemplates- offline render harness viaActionController::Renderer, no HTTP request/session involved. Renders the master template partial for a real user with an in-memory-onlyapp_type_id, never persisted.Prewarm::Candidates- selects one representative user per distinct(app_type, access variant)from recently-signed-in, non-template, non-disabled users. The variant key is a coverage heuristic only - content addressing means an over-sharing key can only cause a missed warm, never a wrong artifact.Prewarm::Runner+rake prewarm:templates- drives one warm pass, lock-guarded (zero-wait, skip-on-contention) so it's never able to block a real request, with a warm marker per combination and per-combination rescue.Prewarm::Spawner- spawns the rake task as a detached child process (IO.popen+Process.detach) from the existingafter_initializeblock inconfig/initializers/handlebars_precompiler.rb, afterstartup_cleanup!- required ordering, sinceserver_cache_versionis shared (memcached) andstartup_cleanup!deletes it on boot; spawning first would orphan every warmed artifact under a stale generation key. Never runs in the delayed_job worker or console/runner. Does not block server startup -IO.popen(no block form) returns immediately after fork/exec; the actual pass runs in a separate OS process.PrewarmTemplatesEnabled- opt-in, forced off in test;PrewarmSignInWindowDays;PrewarmMaxVariants;PrewarmThrottleSeconds).Payoff spec
spec/requests/pages/handlebars_prewarm_payoff_spec.rbis the spec that actually matters: after a simulated restart, a real user's first-ever request hits zero node-CLI compile calls oncePrewarm::MasterTemplateshas warmed their variant - paired with a negative example proving the same request does invoke the CLI when nothing warmed it first.Security review
PrewarmControlleris not reachable over HTTP: no route points at it anywhere, andconfig/routes.rbends with a catch-all (match '*path', via: :all, to: 'bad_route#not_routed').ActionController::Renderer(the only way it's invoked) never goes through routing/middleware/sessions.current_user/current_admin/user_signed_in?overrides areprivate(a public method on a controller is a dispatchable action by default in Rails -helper_methodstill works with private methods viasend), so even an accidental future route couldn't dispatch them as actions.user_signed_in?reflects actual presence of the injected user rather than being hardcodedtrue.app_type_idis set in memory only via a duplicate load (User.find(user.id)), never saved - a warm pass can never change which app type a real user is assigned to.%w[bundle exec rake prewarm:templates]), not a shell string built from any input - no command injection surface.Testing
All new/modified specs pass, plus a full re-run of the existing Stage 1 regression suite (159 examples, 0 failures) confirming no regressions:
Rubocop clean on all new/touched files.
Out of scope (deliberately)
Rails.cachefragment warming (key includescurrent_sign_in_atby design - belongs with the cache-key audit in Cache key audit: under-scoped and shared-cache/local-state mismatches across Rails.cache usages #1289).Admin::AppTypeImport.import_in_progress?check from the original plan's phase 4 - replaced by the shelled rake task.Refs #1362