Context
Since adopting Solid Cache, several controller actions that trigger a GQL calculation are flagged by Sentry as N+1 queries. This was not visible before because Memcached round-trips are cheap and don't use SQL; with Solid Cache every cache operation is a SQL query. The fix proposed below is cache-backend agnostic.
During GQL initialization, several etsource modules fetch their static data from Rails.cache individually. Each call produces a separate SQL query against solid_cache_entries:
- Fever → fever_data
- Reconciliation → reconciliation_hash, reconciliation_carriers
- Molecules → molecules.from_energy_keys, molecules.from_molecules_keys
- MeritOrder → merit_order_hash, hydrogen_hash, heat_network_lt_hash, heat_network_mt_hash, heat_network_ht_hash, agriculture_heat_hash
This data is entirely static between ETSource imports, yet it is fetched with individual Rails.cache.fetch calls on every request, producing ~11 SQL queries per GQL calculation.
Proposed solution
MeritOrder and Molecules are the two classes where batching is practical, they have multiple independent keys with uniform compute logic. Fever has a single key so there is nothing to batch. Reconciliation has a dependency between its two keys (reconciliation_hash depends on reconciliation_carriers) so they cannot be fetched in parallel.
For MeritOrder and Molecules, the individual Rails.cache.fetch calls are replaced with a single Rails.cache.fetch_multi that reads all keys for that class in one SQL query, computing and writing only missing keys. Thread.current is used to ensure fetch_multi runs at most once per request, since each MeritOrder caller instantiates a fresh object and Molecules methods may be called independently multiple times. Subsequent individual fetch calls within the same request hit the LocalStore (Rails' in-request memory layer populated by fetch_multi) at no additional cost.
Context
Since adopting Solid Cache, several controller actions that trigger a GQL calculation are flagged by Sentry as N+1 queries. This was not visible before because Memcached round-trips are cheap and don't use SQL; with Solid Cache every cache operation is a SQL query. The fix proposed below is cache-backend agnostic.
During GQL initialization, several etsource modules fetch their static data from
Rails.cacheindividually. Each call produces a separate SQL query againstsolid_cache_entries:This data is entirely static between ETSource imports, yet it is fetched with individual Rails.cache.fetch calls on every request, producing ~11 SQL queries per GQL calculation.
Proposed solution
MeritOrderandMoleculesare the two classes where batching is practical, they have multiple independent keys with uniform compute logic.Feverhas a single key so there is nothing to batch.Reconciliationhas a dependency between its two keys (reconciliation_hashdepends onreconciliation_carriers) so they cannot be fetched in parallel.For
MeritOrderandMolecules, the individualRails.cache.fetchcalls are replaced with a singleRails.cache.fetch_multithat reads all keys for that class in one SQL query, computing and writing only missing keys.Thread.currentis used to ensurefetch_multiruns at most once per request, since eachMeritOrdercaller instantiates a fresh object andMoleculesmethods may be called independently multiple times. Subsequent individualfetchcalls within the same request hit theLocalStore(Rails' in-request memory layer populated byfetch_multi) at no additional cost.