Fixed global role and access-control rows not rotating partial cache keys - refs #1400 - #1402
Open
philayres wants to merge 1 commit into
Open
Fixed global role and access-control rows not rotating partial cache keys - refs #1400#1402philayres wants to merge 1 commit into
philayres wants to merge 1 commit into
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.
Summary
Phase 1 of #1400 (the final stage of #1362). Cache-key correctness only — deliberately no behaviour change to when the login-scoped key rotates.
current_sign_in_atremains inpartial_cache_keythroughout, so the existing every-login cache bust stays fully in place.This is intentionally shipped ahead of the rest of #1400. Every change here only increases invalidation sensitivity, so there is no window in which anything can be served staler than it is today. The hazard runs the other way: removing
current_sign_in_at(Phase 3) before fixing the global-row gap below would drop the login safety net while the gap still exists.The bug
ApplicationHelper#latest_updated_at_tokenscoped its role/UAC lookups withwhere(app_type_id:), excluding global rows (app_type_id IS NULL) — the shared roles and access controls that apply across all app types via role-name matching. A change to a global role or access control therefore never rotatedpartial_cache_key,template_version, thepages#templateETag, or any fragment cached under them.HandlebarsPrecompilerHelper#app_type_access_control_timestampsalready did this correctly with[app_type_id, nil](PR #1283). The two had drifted because the same query existed twice. This is the last row of #1289's issue 4.Currently masked in production because
current_sign_in_atbusts the key on every login — which is exactly what the rest of #1400 removes.Changes
Fixed the global-row scoping. Role/UAC timestamp lookups now include
app_type_id: nilrows, matching the scoping pattern used byUserAndRoles#where_user_and_roleandPageLayoutsHelper#page_layout_panels.Consolidated the duplicated query. The role/UAC timestamp lookup now lives once, on
HandlebarsPrecompiler, alongsideitem_update_classesanditem_updates_key— which already serve as the shared source of truth for cache-key inputs.ApplicationHelper#latest_updated_at_tokenis removed (it had no other callers) and both helpers delegate. Defined as a method rather than a constant, matching the existing convention there: referencing autoloaded AR classes from a top-level constant in an initializer breaks Zeitwerk boot ordering.Since the divergence fixed above was caused by having two copies of this query, leaving a second copy in place would have preserved the exact failure mode.
Delegated the item-updates class list in
partial_cache_keytoHandlebarsPrecompiler.item_update_classes, replacing an inline duplicate of the same array. Identical content today; done so the two cannot drift.Reportis deliberately not in that list, and a spec now locks that in. The list also feedsHandlebarsPrecompiler.generation_key, so addingReportwould rotate every compiled Handlebars generation on every report config edit — forcing full node-CLI recompiles of content that does not depend on report definitions. Verified there is no user-facing report dependency amongpartial_cache_key's consumers:report_table_headerself-scopes by embedding@report.id/@report.updated_atin its partial name, andpage_layouts/_show_row'scache_resultsits in theelsif resource_defbranch that reports never reach.Collapsed a duplicated query pair.
pages#templatecomputespartial_cache_keytwice per request (once for the ETag, once viatemplate_version), differing only in thepartialprefix. The role/UAC timestamps are now memoized perapp_type_idon the view context, so the second call reuses them. Measured: a 304 revalidation drops from 22 to 20 SQL queries.Removed the
@@prev_partial_cache_keychange-logging. It warned on every key change and retained a class-variable hash keyed by partial/auth-type/user id that was never bounded or evicted — a slow leak fed by the per-login key churn this stage exists to remove.Specs
spec/helpers/application_helper_spec.rbgains coverage for the global-row fix using realAdmin::UserAccessControlandAdmin::UserRolerows rather than stubbed relations, so it exercises the actual query.Two details are load-bearing, and both are documented inline:
clear_rails_cache_on_save?tofalse. Without it, creating the row clears the whole Rails cache, droppingserver_cache_version— which is itself part of the key — so the key rotates for an unrelated reason and the example passes even with the fix reverted. Same maskingspec/models/admin/app_type_available_ids_cache_spec.rbguards against.updated_at. Cache-key timestamps interpolate at second granularity, so a same-second write does not rotate the key.Both examples were verified to fail with the fix reverted (before/after keys byte-identical) and pass with it restored.
Also added:
current_sign_in_atstill rotates the key for bothUserandAdmin(guards Phase 1 against straying into Phase 3's scope), the ETag/template_versiontimestamp reuse, absence of the removed logging, andReport's absence from the item-update list.spec/system/user/app_type_switch_master_tabs_spec.rbgains a multi-user login/reload smoke example, with a comment stating plainly that it is smoke coverage — it would pass identically before and after this fix, and is not the regression test for it.Also included
spec/requests/pages/handlebars_compile_cost_benchmark_spec.rbdocumented itself as opt-in viaRUN_BENCHMARKS, but carried nobenchmark: truetag, sofilter_run_excludingnever excluded it and it ran in every normal suite pass. Tagged correctly. Verified: 0 examples withoutRUN_BENCHMARKS, 2 with.spec/requests/pages/template_revalidation_cost_benchmark_spec.rbis new — it measures the cost of revalidatingpages#templaterather than caching it immutably, which is the evidence base for #1400's Phase 4 decision to dropCache-Control: immutable:Confirms the ETag needs no ERB render —
stale?returns beforerender— so a revalidation costs a fraction of the cold render that currently happens on every login.Verification
spec/helpers/application_helper_spec.rb,spec/helpers/handlebars_precompiler_helper_spec.rb,spec/controllers/pages_controller_spec.rb,spec/requests/pages/— 151 examples, 0 failures.app_type_access_control_timestampsfeedshandlebars_cache_keyandaccess_control_version, which are load-bearing for the Stage 1 content addressing in Hardened and shared the on-disk Handlebars template compile cache across users - refs #1362 #1377.Refs #1400, #1362, #1289, #1283.