solid_cache 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: '0649423e526a66fa9554f73d215e7e49c6ff093c004c1641d72a670a33688468'
4
+ data.tar.gz: ad56d9f53b6b0c6f9dcc049771a187e95c5e2cceeabeaa07286f96212f5b0fb7
5
+ SHA512:
6
+ metadata.gz: 900011771476bff6c3b3ec5abe087a0dcf00596f6e200dec961aa7e13f3ad4d978d9ac8858d3912b8bc0bc63a8742de4e925bd04b120d15badb890448946f4c2
7
+ data.tar.gz: c9fd66e658ec1e38f628c592053f330179571a7299a1d4e6bae88a959996c10c4d7df158c30c9827464e0c3af526a4b5c0100f55f51c5b3e563e73b5af93226a
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2023 37signals
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # Solid Cache
2
+
3
+ Solid Cache is a database-backed Active Support cache store implementation.
4
+
5
+ Using SQL databases backed by SSDs we can have caches that are much larger and cheaper than traditional memory only Redis or Memcached backed caches.
6
+
7
+ Testing on [HEY](https://hey.com) shows that reads and writes are 25%-50% slower than with a Redis cache (1.2ms vs 0.8-1ms per single-key read), but this is not a significant percentage of the overall request time.
8
+
9
+ If cache misses are expensive (up to 50x the cost of a hit on HEY), then there are big advantages to caches that can hold months rather than days of data.
10
+
11
+ ## Usage
12
+
13
+ To set Solid Cache as your Rails cache, you should add this to your environment config:
14
+
15
+ ```ruby
16
+ config.cache_store = :solid_cache_store
17
+ ```
18
+
19
+ Solid Cache is a FIFO (first in, first out) cache. While this is not as efficient as an LRU cache, this is mitigated by the longer cache lifespans.
20
+
21
+ A FIFO cache is much easier to manage:
22
+ 1. We don't need to track when items are read
23
+ 2. We can estimate and control the cache size by comparing the maximum and minimum IDs.
24
+ 3. By deleting from one end of the table and adding at the other end we can avoid fragmentation (on MySQL at least).
25
+
26
+ ### Installation
27
+ Add this line to your application's Gemfile:
28
+
29
+ ```ruby
30
+ gem "solid_cache"
31
+ ```
32
+
33
+ And then execute:
34
+ ```bash
35
+ $ bundle
36
+ ```
37
+
38
+ Or install it yourself as:
39
+ ```bash
40
+ $ gem install solid_cache
41
+ ```
42
+
43
+ Add the migration to your app:
44
+
45
+ ```bash
46
+ $ bin/rails solid_cache:install:migrations
47
+ ```
48
+
49
+ Then run it:
50
+ ```bash
51
+ $ bin/rails db:migrate
52
+ ```
53
+
54
+ ### Configuration
55
+
56
+ #### Engine configuration
57
+
58
+ There are two options that can be set on the engine:
59
+
60
+ - `executor` - the [Rails executor](https://guides.rubyonrails.org/threading_and_code_execution.html#executor) used to wrap asynchronous operations, defaults to the app executor
61
+ - `connects_to` - a custom connects to value for the abstract `SolidCache::Record` active record model. Requires for sharding and/or using a separate cache database to the main app.
62
+
63
+ These can be set in your Rails configuration:
64
+
65
+ ```ruby
66
+ Rails.application.configure do
67
+ config.solid_cache.connects_to = {
68
+ shards: {
69
+ shard1: { writing: :cache_primary_shard1 },
70
+ shard2: { writing: :cache_primary_shard2 }
71
+ }
72
+ }
73
+ end
74
+ ```
75
+
76
+ #### Cache configuration
77
+
78
+ Solid Cache supports these options in addition to the standard `ActiveSupport::Cache::Store` options.
79
+
80
+ - `error_handler` - a Proc to call to handle any `ActiveRecord::ActiveRecordError`s that are raises (default: log errors as warnings)
81
+ - `expiry_batch_size` - the batch size to use when deleting old records (default: `100`)
82
+ - `expiry_method` - what expiry method to use `thread` or `job` (default: `thread`)
83
+ - `max_age` - the maximum age of entries in the cache (default: `2.weeks.to_i`)
84
+ - `max_entries` - the maximum number of entries allowed in the cache (default: `2.weeks.to_i`)
85
+ - `cluster` - a Hash of options for the cache database cluster, e.g `{ shards: [:database1, :database2, :database3] }`
86
+ - `clusters` - and Array of Hashes for multiple cache clusters (ignored if `:cluster` is set)
87
+ - `active_record_instrumentation` - whether to instrument the cache's queries (default: `true`)
88
+ - `clear_with` - clear the cache with `:truncate` or `:delete` (default `truncate`, except for when Rails.env.test? then `delete`)
89
+ - `max_key_bytesize` - the maximum size of a normalized key in bytes (default `1024`)
90
+
91
+ For more information on cache clusters see [Sharding the cache](#sharding-the-cache)
92
+
93
+ ### Cache expiry
94
+
95
+ Solid Cache tracks writes to the cache. For every write it increments a counter by 1. Once the counter reaches 80% of the `expiry_batch_size` it add a task to run on a background thread. That task will:
96
+
97
+ 1. Check if we have exceeded the `max_entries` value (if set) by subtracting the max and min IDs from the `SolidCache::Entry` table (this is an estimate that ignores any gaps).
98
+ 2. If we have it will delete `expiry_batch_size` entries
99
+ 3. If not it will delete up to `expiry_batch_size` entries, provided they are all older than `max_age`.
100
+
101
+ Expiring when we reach 80% of the batch size allows us to expire records from the cache faster than we write to it when we need to reduce the cache size.
102
+
103
+ Only triggering expiry when we write means that the if the cache is idle, the background thread is also idle.
104
+
105
+ If you want the cache expiry to be run in a background job instead of a thread, you can set `expiry_method` to `:job`. This will enqueue a `SolidCache::ExpiryJob`.
106
+
107
+ ### Using a dedicated cache database
108
+
109
+ Add database configuration to database.yml, e.g.:
110
+
111
+ ```
112
+ development
113
+ cache:
114
+ database: cache_development
115
+ host: 127.0.0.1
116
+ migrations_paths: "db/cache/migrate"
117
+ ```
118
+
119
+ Create database:
120
+ ```
121
+ $ bin/rails db:create
122
+ ```
123
+
124
+ Install migrations:
125
+ ```
126
+ $ bin/rails solid_cache:install:migrations
127
+ ```
128
+
129
+ Move migrations to custom migrations folder:
130
+ ```
131
+ $ mkdir -p db/cache/migrate
132
+ $ mv db/migrate/*.solid_cache.rb db/cache/migrate
133
+ ```
134
+
135
+ Set the engine configuration to point to the new database:
136
+ ```
137
+ Rails.application.configure do
138
+ config.solid_cache.connects_to = { default: { writing: :cache } }
139
+ end
140
+ ```
141
+
142
+ Run migrations:
143
+ ```
144
+ $ bin/rails db:migrate
145
+ ```
146
+
147
+ ### Sharding the cache
148
+
149
+ Solid Cache uses the [Maglev](https://static.googleusercontent.com/media/research.google.com/en//pubs/archive/44824.pdf) consistent hashing scheme to shard the cache across multiple databases.
150
+
151
+ To shard:
152
+
153
+ 1. Add the configuration for the database shards to database.yml
154
+ 2. Configure the shards via `config.solid_cache.connects_to`
155
+ 3. Pass the shards for the cache to use via the cluster option
156
+
157
+ For example:
158
+ ```ruby
159
+ # config/database.yml
160
+ production:
161
+ cache_shard1:
162
+ database: cache1_production
163
+ host: cache1-db
164
+ cache_shard2:
165
+ database: cache2_production
166
+ host: cache2-db
167
+ cache_shard3:
168
+ database: cache3_production
169
+ host: cache3-db
170
+
171
+
172
+ # config/environment/production.rb
173
+ Rails.application.configure do
174
+ config.solid_cache.connects_to = {
175
+ shards: {
176
+ cache_shard1: { writing: :cache_shard1 },
177
+ cache_shard2: { writing: :cache_shard2 },
178
+ cache_shard3: { writing: :cache_shard3 },
179
+ }
180
+ }
181
+
182
+ config.cache_store = [ :solid_cache_store, cluster: { shards: [ :cache_shard1, :cache_shard2, :cache_shard3 ] } ]
183
+ end
184
+ ```
185
+
186
+ ### Secondary cache clusters
187
+
188
+ You can add secondary cache clusters. Reads will only be sent to the primary cluster (i.e. the first one listed).
189
+
190
+ Writes will go to all clusters. The writes to the primary cluster are synchronous, but asyncronous to the secondary clusters.
191
+
192
+ To specific multiple clusters you can do:
193
+
194
+ ```ruby
195
+ Rails.application.configure do
196
+ config.solid_cache.connects_to = {
197
+ shards: {
198
+ cache_primary_shard1: { writing: :cache_primary_shard1 },
199
+ cache_primary_shard2: { writing: :cache_primary_shard2 },
200
+ cache_secondary_shard1: { writing: :cache_secondary_shard1 },
201
+ cache_secondary_shard2: { writing: :cache_secondary_shard2 },
202
+ }
203
+ }
204
+
205
+ primary_cluster = { shards: [ :cache_primary_shard1, :cache_primary_shard2 ] }
206
+ secondary_cluster = { shards: [ :cache_primary_shard1, :cache_primary_shard2 ] }
207
+ config.cache_store = [ :solid_cache_store, clusters: [ primary_cluster, secondary_cluster ] ]
208
+ end
209
+ ```
210
+
211
+ ### Named shard destinations
212
+
213
+ By default, the node key used for sharding is the name of the database in `database.yml`.
214
+
215
+ It is possible to add names for the shards in the cluster config. This will allow you to shuffle or remove shards without breaking consistent hashing.
216
+
217
+ ```ruby
218
+ Rails.application.configure do
219
+ config.solid_cache.connects_to = {
220
+ shards: {
221
+ cache_primary_shard1: { writing: :cache_primary_shard1 },
222
+ cache_primary_shard2: { writing: :cache_primary_shard2 },
223
+ cache_secondary_shard1: { writing: :cache_secondary_shard1 },
224
+ cache_secondary_shard2: { writing: :cache_secondary_shard2 },
225
+ }
226
+ }
227
+
228
+ primary_cluster = { shards: { cache_primary_shard1: :node1, cache_primary_shard2: :node2 } }
229
+ secondary_cluster = { shards: { cache_primary_shard1: :node3, cache_primary_shard2: :node4 } }
230
+ config.cache_store = [ :solid_cache_store, clusters: [ primary_cluster, secondary_cluster ] ]
231
+ end
232
+ ```
233
+
234
+
235
+ ### Enabling encryption
236
+
237
+ Add this to an initializer:
238
+
239
+ ```ruby
240
+ ActiveSupport.on_load(:solid_cache_entry) do
241
+ encrypts :value
242
+ end
243
+ ```
244
+
245
+ ### Index size limits
246
+ The Solid Cache migrations try to create an index with 1024 byte entries. If that is too big for your database, you should:
247
+
248
+ 1. Edit the index size in the migration
249
+ 2. Set `max_key_bytesize` on your cache to the new value
250
+
251
+ ## Development
252
+
253
+ Run the tests with `bin/rails test`. These will run against SQLite.
254
+
255
+ You can also run the tests against MySQL and Postgres. First start up the databases:
256
+
257
+ ```shell
258
+ $ docker compose up -d
259
+ ```
260
+
261
+ Then run the tests for the target database
262
+ ```
263
+ $ TARGET_DB=mysql bin/rails test
264
+ $ TARGET_DB=postgres bin/rails test
265
+ ```
266
+
267
+ ## License
268
+ Solid Cache is licensed under MIT.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require "bundler/setup"
2
+
3
+ APP_RAKEFILE = File.expand_path("test/dummy/Rakefile", __dir__)
4
+ load "rails/tasks/engine.rake"
5
+
6
+ load "rails/tasks/statistics.rake"
7
+
8
+ require "bundler/gem_tasks"
@@ -0,0 +1,9 @@
1
+ module SolidCache
2
+ class ExpiryJob < ActiveJob::Base
3
+ def perform(count, shard: nil, max_age:, max_entries:)
4
+ Record.with_shard(shard) do
5
+ Entry.expire(count, max_age: max_age, max_entries: max_entries)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,157 @@
1
+ module SolidCache
2
+ class Entry < Record
3
+ # This is all quite awkward but it achieves a couple of performance aims
4
+ # 1. We skip the query cache
5
+ # 2. We avoid the overhead of building queries and active record objects
6
+ class << self
7
+ def write(key, value)
8
+ upsert_all_no_query_cache([ { key: key, value: value } ])
9
+ end
10
+
11
+ def write_multi(payloads)
12
+ upsert_all_no_query_cache(payloads)
13
+ end
14
+
15
+ def read(key)
16
+ select_all_no_query_cache(get_sql, to_binary(key)).first
17
+ end
18
+
19
+ def read_multi(keys)
20
+ serialized_keys = keys.map { |key| to_binary(key) }
21
+ select_all_no_query_cache(get_all_sql(serialized_keys), serialized_keys).to_h
22
+ end
23
+
24
+ def expire(ids)
25
+ delete_no_query_cache(:id, ids) if ids.any?
26
+ end
27
+
28
+ def delete_by_key(key)
29
+ delete_no_query_cache(:key, to_binary(key))
30
+ end
31
+
32
+ def delete_matched(matcher, batch_size:)
33
+ like_matcher = arel_table[:key].matches(matcher, nil, true)
34
+ where(like_matcher).select(:id).find_in_batches(batch_size: batch_size) do |entries|
35
+ delete_no_query_cache(:id, entries.map(&:id))
36
+ end
37
+ end
38
+
39
+ def clear_truncate
40
+ connection.truncate(table_name)
41
+ end
42
+
43
+ def clear_delete
44
+ in_batches.delete_all
45
+ end
46
+
47
+ def increment(key, amount)
48
+ transaction do
49
+ uncached do
50
+ amount += lock.where(key: key).pick(:value).to_i
51
+ write(key, amount)
52
+ amount
53
+ end
54
+ end
55
+ end
56
+
57
+ def decrement(key, amount)
58
+ increment(key, -amount)
59
+ end
60
+
61
+ def id_range
62
+ uncached do
63
+ pick(Arel.sql("max(id) - min(id) + 1")) || 0
64
+ end
65
+ end
66
+
67
+ def expire(count, max_age:, max_entries:)
68
+ if (ids = expiry_candidate_ids(count, max_age: max_age, max_entries: max_entries)).any?
69
+ delete(ids)
70
+ end
71
+ end
72
+
73
+ private
74
+ def upsert_all_no_query_cache(attributes)
75
+ insert_all = ActiveRecord::InsertAll.new(self, attributes, unique_by: upsert_unique_by, on_duplicate: :update, update_only: [ :value ])
76
+ sql = connection.build_insert_sql(ActiveRecord::InsertAll::Builder.new(insert_all))
77
+
78
+ message = +"#{self} "
79
+ message << "Bulk " if attributes.many?
80
+ message << "Upsert"
81
+ # exec_query does not clear the query cache, exec_insert_all does
82
+ connection.exec_query sql, message
83
+ end
84
+
85
+ def upsert_unique_by
86
+ connection.supports_insert_conflict_target? ? :key : nil
87
+ end
88
+
89
+ def get_sql
90
+ @get_sql ||= build_sql(where(key: "placeholder").select(:value))
91
+ end
92
+
93
+ def get_all_sql(keys)
94
+ if connection.prepared_statements?
95
+ @get_all_sql_binds ||= {}
96
+ @get_all_sql_binds[keys.count] ||= build_sql(where(key: keys).select(:key, :value))
97
+ else
98
+ @get_all_sql_no_binds ||= build_sql(where(key: [ "placeholder1", "placeholder2" ]).select(:key, :value)).gsub("?, ?", "?")
99
+ end
100
+ end
101
+
102
+ def build_sql(relation)
103
+ collector = Arel::Collectors::Composite.new(
104
+ Arel::Collectors::SQLString.new,
105
+ Arel::Collectors::Bind.new,
106
+ )
107
+
108
+ connection.visitor.compile(relation.arel.ast, collector)[0]
109
+ end
110
+
111
+ def select_all_no_query_cache(query, values)
112
+ uncached do
113
+ if connection.prepared_statements?
114
+ result = connection.select_all(sanitize_sql(query), "#{name} Load", Array(values), preparable: true)
115
+ else
116
+ result = connection.select_all(sanitize_sql([ query, values ]), "#{name} Load", nil, preparable: false)
117
+ end
118
+
119
+ result.cast_values(SolidCache::Entry.attribute_types)
120
+ end
121
+ end
122
+
123
+ def delete_no_query_cache(attribute, values)
124
+ uncached do
125
+ relation = where(attribute => values)
126
+ sql = connection.to_sql(relation.arel.compile_delete(relation.table[primary_key]))
127
+
128
+ # exec_delete does not clear the query cache
129
+ if connection.prepared_statements?
130
+ connection.exec_delete(sql, "#{name} Delete All", Array(values)).nonzero?
131
+ else
132
+ connection.exec_delete(sql, "#{name} Delete All").nonzero?
133
+ end
134
+ end
135
+ end
136
+
137
+ def to_binary(key)
138
+ ActiveModel::Type::Binary.new.serialize(key)
139
+ end
140
+
141
+ def expiry_candidate_ids(count, max_age:, max_entries:)
142
+ cache_full = max_entries && max_entries < id_range
143
+ min_created_at = max_age.seconds.ago
144
+
145
+ uncached do
146
+ order(:id)
147
+ .limit(count * 3)
148
+ .pluck(:id, :created_at)
149
+ .filter_map { |id, created_at| id if cache_full || created_at < min_created_at }
150
+ .sample(count)
151
+ end
152
+ end
153
+ end
154
+ end
155
+ end
156
+
157
+ ActiveSupport.run_load_hooks :solid_cache_entry, SolidCache::Entry
@@ -0,0 +1,27 @@
1
+ module SolidCache
2
+ class Record < ActiveRecord::Base
3
+ NULL_INSTRUMENTER = ActiveSupport::Notifications::Instrumenter.new(ActiveSupport::Notifications::Fanout.new)
4
+
5
+ self.abstract_class = true
6
+
7
+ connects_to **SolidCache.connects_to if SolidCache.connects_to
8
+
9
+ class << self
10
+ def disable_instrumentation
11
+ connection.with_instrumenter(NULL_INSTRUMENTER) do
12
+ yield
13
+ end
14
+ end
15
+
16
+ def with_shard(shard, &block)
17
+ if shard && shard != Record.current_shard
18
+ Record.connected_to(shard: shard, &block)
19
+ else
20
+ block.call
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ ActiveSupport.run_load_hooks :solid_cache, SolidCache::Record
@@ -0,0 +1,11 @@
1
+ class CreateSolidCacheEntries < ActiveRecord::Migration[7.0]
2
+ def change
3
+ create_table :solid_cache_entries do |t|
4
+ t.binary :key, null: false, limit: 1024
5
+ t.binary :value, null: false, limit: 512.megabytes
6
+ t.datetime :created_at, null: false
7
+
8
+ t.index :key, unique: true
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,5 @@
1
+ module ActiveSupport
2
+ module Cache
3
+ SolidCacheStore = SolidCache::Store
4
+ end
5
+ end
@@ -0,0 +1,9 @@
1
+ Description:
2
+ Installs solid_cache as the Rails cache store
3
+
4
+ Example:
5
+ bin/rails generate solid_cache:install
6
+
7
+ This will create:
8
+ Installs the solid_cache migrations
9
+ Replaces the cache store in envionment configuration
@@ -0,0 +1,18 @@
1
+ class SolidCache::InstallGenerator < Rails::Generators::Base
2
+ class_option :skip_migrations, type: :boolean, default: nil,
3
+ desc: "Skip migrations"
4
+
5
+ def add_rails_cache
6
+ %w[development test production].each do |env_name|
7
+ if (env_config = Pathname(destination_root).join("config/environments/#{env_name}.rb")).exist?
8
+ gsub_file env_config, /(# )?config\.cache_store = (:(?!null_store).*)/, "config.cache_store = :solid_cache_store"
9
+ end
10
+ end
11
+ end
12
+
13
+ def create_migrations
14
+ unless options[:skip_migrations]
15
+ rails_command "railties:install:migrations FROM=solid_cache", inline: true
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,51 @@
1
+ module SolidCache
2
+ class Cluster
3
+ module Connections
4
+ def initialize(options = {})
5
+ super(options)
6
+ @shard_options = options.fetch(:shards, nil)
7
+
8
+ if [ Hash, Array, NilClass ].none? { |klass| @shard_options.is_a? klass }
9
+ raise ArgumentError, "`shards` is a `#{@shard_options.class.name}`, it should be one of Array, Hash or nil"
10
+ end
11
+ end
12
+
13
+ def with_each_connection(async: false, &block)
14
+ return enum_for(:with_each_connection) unless block_given?
15
+
16
+ connections.with_each do
17
+ execute(async, &block)
18
+ end
19
+ end
20
+
21
+ def with_connection_for(key, async: false, &block)
22
+ connections.with_connection_for(key) do
23
+ execute(async, &block)
24
+ end
25
+ end
26
+
27
+ def with_connection(name, async: false, &block)
28
+ connections.with(name) do
29
+ execute(async, &block)
30
+ end
31
+ end
32
+
33
+ def group_by_connection(keys)
34
+ connections.assign(keys)
35
+ end
36
+
37
+ def connection_names
38
+ connections.names
39
+ end
40
+
41
+ private
42
+ def setup!
43
+ connections
44
+ end
45
+
46
+ def connections
47
+ @connections ||= SolidCache::Connections.from_config(@shard_options)
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,52 @@
1
+ module SolidCache
2
+ class Cluster
3
+ module Execution
4
+ def initialize(options = {})
5
+ super(options)
6
+ @background = Concurrent::SingleThreadExecutor.new(max_queue: 100, fallback_policy: :discard)
7
+ @active_record_instrumentation = options.fetch(:active_record_instrumentation, true)
8
+ end
9
+
10
+ private
11
+ def async(&block)
12
+ # Need current shard right now, not when block is called
13
+ current_shard = Entry.current_shard
14
+ @background << ->() do
15
+ wrap_in_rails_executor do
16
+ connections.with(current_shard) do
17
+ instrument(&block)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def execute(async, &block)
24
+ if async
25
+ async(&block)
26
+ else
27
+ instrument(&block)
28
+ end
29
+ end
30
+
31
+ def wrap_in_rails_executor(&block)
32
+ if SolidCache.executor
33
+ SolidCache.executor.wrap(&block)
34
+ else
35
+ block.call
36
+ end
37
+ end
38
+
39
+ def active_record_instrumentation?
40
+ @active_record_instrumentation
41
+ end
42
+
43
+ def instrument(&block)
44
+ if active_record_instrumentation?
45
+ block.call
46
+ else
47
+ Record.disable_instrumentation(&block)
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end