Summary
Follow-up to #1279, #1287, and PRs #1271 / #1283. A full audit of all Rails.cache call sites (57 usages, 25 files) was performed. Most are correct. Four categories of issue were identified: two involving cross-worker cache poisoning, one involving per-process state in a shared cache, and one group that is currently masked by whole-cache clears (and will become bugs as the #1270 direction continues).
Issue 1 — def_handler.rb: item_types cache key is global but content is per-process
Location: app/models/dynamic/def_handler.rb (~L218–L226)
Key: "DynamicModel.item_types" / "ActivityLog.item_types" / "ExternalIdentifier.item_types" — global, no invalidation inputs.
Content: Built from implementation_classes, which calls implementation_class_defined? and skips any class not yet generated in the current process. In a multi-worker deployment, whichever worker first populates this key wins — workers with incomplete runtime models poison the shared cache with an incomplete list. This is the exact same pattern as the master-tabs bug (#1279): shared cache keyed globally, content dependent on per-process state.
Impact: Admin GeneralSelection item-type dropdowns silently lose entries (no error, just missing options).
Fix direction: Key on a DB-derived timestamp (e.g., the dynamic-definition latest_updates for DynamicModel, ActivityLog, ExternalIdentifier) rather than a static class name. Alternatively, exclude implementation_class_defined? from the cached path and do the filtering post-cache.
Issue 2 — reports_helper.rb: select_from_model_resource_name_options keyed on per-process Resources::Models.updated_at
Location: app/helpers/reports_helper.rb (~L253)
Key: "select_from_model_resource_name_options-#{Resources::Models.updated_at}"
Problem: Resources::Models.updated_at is a mattr_accessor set to Time.now per process when the registry is rebuilt. Every worker generates a different key (cache bloat, no sharing), and a worker with an incomplete or different-generation registry caches an incomplete options list under a key that no other worker ever reads. mattr_accessor state diverges between workers and is invisible to the cache key consumer.
Fix direction: Replace Resources::Models.updated_at with a DB-derived composite of DynamicModel.latest_update, ActivityLog.latest_update, ExternalIdentifier.latest_update — consistent across workers.
Issue 3 — secure_view: rendered file path cached in shared store
Location: app/models/secure_view/base_previewer.rb and image_previewer.rb (~L214, L288, L132)
Key: "SecureView-doc-#{orig_path}-#{file_type}-#{type}-#{view_type}"
Value: A Dir.mktmpdir path (local temp directory).
Problem: The cached value is a local filesystem path stored in shared memcached. In multi-server production:
- Server B fetches a rendered-path cache entry written by Server A, but the temp directory does not exist on Server B → file-not-found or silent failure.
- After any process restart or tmp-dir cleanup, stale cache entries point to deleted paths on every server.
Fix direction: Either include a host identifier and boot/startup token in the key (so each server's entries are isolated), or move rendered-file caching to local in-memory / filesystem cache rather than shared memcached.
Issue 4 — Masked by whole-cache clears: keys that will break when clears are scoped further
The following sites have under-scoped keys that currently survive only because saving the relevant admin models still calls Rails.cache.clear (the pre-#1271 behaviour). As #1270 continues to scope down those clears, these will surface:
| Location |
Key |
Missing inputs |
app/controllers/admin/user_access_controls_controller.rb |
admin_user_access_control_resource_names-#{UAC.latest_update} |
dynamic-definition and report latest_updates (content includes their resource names) |
app/views/admin/reports/_info_block.html.erb |
'info-block-protocol-block' / 'info-block-gs-block' |
No invalidation inputs at all — static keys, never expire |
app/models/admin/migration_generator.rb |
"db_column_comments-#{Application.version}" etc. |
Dynamic-model migrations change DB schema comments/FKs without a version bump |
app/controllers/definitions_controller.rb |
"definition_#{def_type}-#{Application.server_cache_version}" |
Freshness relies entirely on whole clears; no per-user scoping (though content is global, so no user-leak risk) |
app/helpers/application_helper.rb partial_cache_key |
userrole/uac queries use where(app_type_id: apptype) |
Global (app_type_id: nil) role/UAC rows are excluded, inconsistent with PR #1283's fix in handlebars_precompiler_helper (app_type_id: [app_type_id, nil]). Global access-control changes don't rotate template_version/fragment/etag keys. |
Issue 5 — master_handler.rb index_cache_key omits app_type_id
Location: app/controllers/concerns/master_handler.rb (~L187–L211)
The key includes current_user.id, params, and per-record updated_ats, but not current_user.app_type_id or role/UAC timestamps. In-session app-type switches could replay stale index JSON for the same user/record combination. Currently narrow (30-second browser TTL, record updates rotate it), but same pattern class.
Cross-cutting caveat: second-granularity timestamp keys
All keys built on latest_update / cache_key_for_access_for interpolate timestamps via Time#to_s (second precision). Two access-control changes within the same wall-clock second do not rotate dependent cache keys. Harmless in normal operation but relevant when writing cache-correctness specs (explicit updated_at offsets are needed, as documented in the specs added for #1279).
Side observation: definitions_controller exposes all active user emails
DefinitionsController exposes users → active_id_name_list (all active user emails) to any authenticated user with no access-control check beyond login. Worth a separate access-control review.
Related
Summary
Follow-up to #1279, #1287, and PRs #1271 / #1283. A full audit of all
Rails.cachecall sites (57 usages, 25 files) was performed. Most are correct. Four categories of issue were identified: two involving cross-worker cache poisoning, one involving per-process state in a shared cache, and one group that is currently masked by whole-cache clears (and will become bugs as the #1270 direction continues).Issue 1 —
def_handler.rb:item_typescache key is global but content is per-processLocation:
app/models/dynamic/def_handler.rb(~L218–L226)Key:
"DynamicModel.item_types"/"ActivityLog.item_types"/"ExternalIdentifier.item_types"— global, no invalidation inputs.Content: Built from
implementation_classes, which callsimplementation_class_defined?and skips any class not yet generated in the current process. In a multi-worker deployment, whichever worker first populates this key wins — workers with incomplete runtime models poison the shared cache with an incomplete list. This is the exact same pattern as the master-tabs bug (#1279): shared cache keyed globally, content dependent on per-process state.Impact: Admin GeneralSelection item-type dropdowns silently lose entries (no error, just missing options).
Fix direction: Key on a DB-derived timestamp (e.g., the dynamic-definition
latest_updates for DynamicModel, ActivityLog, ExternalIdentifier) rather than a static class name. Alternatively, excludeimplementation_class_defined?from the cached path and do the filtering post-cache.Issue 2 —
reports_helper.rb:select_from_model_resource_name_optionskeyed on per-processResources::Models.updated_atLocation:
app/helpers/reports_helper.rb(~L253)Key:
"select_from_model_resource_name_options-#{Resources::Models.updated_at}"Problem:
Resources::Models.updated_atis amattr_accessorset toTime.nowper process when the registry is rebuilt. Every worker generates a different key (cache bloat, no sharing), and a worker with an incomplete or different-generation registry caches an incomplete options list under a key that no other worker ever reads.mattr_accessorstate diverges between workers and is invisible to the cache key consumer.Fix direction: Replace
Resources::Models.updated_atwith a DB-derived composite ofDynamicModel.latest_update,ActivityLog.latest_update,ExternalIdentifier.latest_update— consistent across workers.Issue 3 —
secure_view: rendered file path cached in shared storeLocation:
app/models/secure_view/base_previewer.rbandimage_previewer.rb(~L214, L288, L132)Key:
"SecureView-doc-#{orig_path}-#{file_type}-#{type}-#{view_type}"Value: A
Dir.mktmpdirpath (local temp directory).Problem: The cached value is a local filesystem path stored in shared memcached. In multi-server production:
Fix direction: Either include a host identifier and boot/startup token in the key (so each server's entries are isolated), or move rendered-file caching to local in-memory / filesystem cache rather than shared memcached.
Issue 4 — Masked by whole-cache clears: keys that will break when clears are scoped further
The following sites have under-scoped keys that currently survive only because saving the relevant admin models still calls
Rails.cache.clear(the pre-#1271 behaviour). As #1270 continues to scope down those clears, these will surface:app/controllers/admin/user_access_controls_controller.rbadmin_user_access_control_resource_names-#{UAC.latest_update}latest_updates (content includes their resource names)app/views/admin/reports/_info_block.html.erb'info-block-protocol-block'/'info-block-gs-block'app/models/admin/migration_generator.rb"db_column_comments-#{Application.version}"etc.app/controllers/definitions_controller.rb"definition_#{def_type}-#{Application.server_cache_version}"app/helpers/application_helper.rbpartial_cache_keywhere(app_type_id: apptype)app_type_id: nil) role/UAC rows are excluded, inconsistent with PR #1283's fix inhandlebars_precompiler_helper(app_type_id: [app_type_id, nil]). Global access-control changes don't rotatetemplate_version/fragment/etag keys.Issue 5 —
master_handler.rbindex_cache_keyomitsapp_type_idLocation:
app/controllers/concerns/master_handler.rb(~L187–L211)The key includes
current_user.id, params, and per-recordupdated_ats, but notcurrent_user.app_type_idor role/UAC timestamps. In-session app-type switches could replay stale index JSON for the same user/record combination. Currently narrow (30-second browser TTL, record updates rotate it), but same pattern class.Cross-cutting caveat: second-granularity timestamp keys
All keys built on
latest_update/cache_key_for_access_forinterpolate timestamps viaTime#to_s(second precision). Two access-control changes within the same wall-clock second do not rotate dependent cache keys. Harmless in normal operation but relevant when writing cache-correctness specs (explicitupdated_atoffsets are needed, as documented in the specs added for #1279).Side observation:
definitions_controllerexposes all active user emailsDefinitionsControllerexposesusers → active_id_name_list(all active user emails) to any authenticated user with no access-control check beyond login. Worth a separate access-control review.Related
viewable-tables-cache-scope-1279/ PR Added YAML object edit and result fields - fixes #1269 #1284 (viewable_tables + all_ids_available_to + remake_from_attributes fixes)