solid_cache 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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