identity_cache 1.0.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +76 -9
  3. data/.github/workflows/cla.yml +22 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +7 -3
  6. data/.spin/bootstrap +7 -0
  7. data/.spin/svc.yml +2 -0
  8. data/CAVEATS.md +25 -0
  9. data/CHANGELOG.md +56 -22
  10. data/Gemfile +15 -5
  11. data/LICENSE +1 -1
  12. data/README.md +27 -8
  13. data/Rakefile +13 -12
  14. data/dev.yml +5 -4
  15. data/gemfiles/Gemfile.latest-release +12 -5
  16. data/gemfiles/Gemfile.min-supported +12 -0
  17. data/gemfiles/Gemfile.rails-edge +9 -5
  18. data/identity_cache.gemspec +15 -24
  19. data/{railgun.yml → isogun.yml} +0 -5
  20. data/lib/identity_cache/belongs_to_caching.rb +1 -0
  21. data/lib/identity_cache/cache_fetcher.rb +241 -16
  22. data/lib/identity_cache/cache_hash.rb +7 -6
  23. data/lib/identity_cache/cache_invalidation.rb +2 -1
  24. data/lib/identity_cache/cache_key_generation.rb +22 -19
  25. data/lib/identity_cache/cache_key_loader.rb +2 -2
  26. data/lib/identity_cache/cached/association.rb +2 -4
  27. data/lib/identity_cache/cached/attribute.rb +3 -3
  28. data/lib/identity_cache/cached/attribute_by_multi.rb +1 -1
  29. data/lib/identity_cache/cached/belongs_to.rb +24 -14
  30. data/lib/identity_cache/cached/embedded_fetching.rb +2 -0
  31. data/lib/identity_cache/cached/prefetcher.rb +12 -2
  32. data/lib/identity_cache/cached/primary_index.rb +3 -3
  33. data/lib/identity_cache/cached/recursive/association.rb +55 -12
  34. data/lib/identity_cache/cached/recursive/has_many.rb +1 -0
  35. data/lib/identity_cache/cached/recursive/has_one.rb +1 -0
  36. data/lib/identity_cache/cached/reference/association.rb +1 -0
  37. data/lib/identity_cache/cached/reference/has_many.rb +3 -2
  38. data/lib/identity_cache/cached/reference/has_one.rb +3 -2
  39. data/lib/identity_cache/cached.rb +1 -0
  40. data/lib/identity_cache/configuration_dsl.rb +1 -0
  41. data/lib/identity_cache/encoder.rb +2 -1
  42. data/lib/identity_cache/expiry_hook.rb +2 -1
  43. data/lib/identity_cache/fallback_fetcher.rb +6 -1
  44. data/lib/identity_cache/mem_cache_store_cas.rb +63 -0
  45. data/lib/identity_cache/memoized_cache_proxy.rb +33 -23
  46. data/lib/identity_cache/parent_model_expiration.rb +6 -3
  47. data/lib/identity_cache/query_api.rb +29 -66
  48. data/lib/identity_cache/railtie.rb +1 -0
  49. data/lib/identity_cache/should_use_cache.rb +1 -0
  50. data/lib/identity_cache/version.rb +2 -1
  51. data/lib/identity_cache/with_primary_index.rb +37 -10
  52. data/lib/identity_cache/without_primary_index.rb +7 -3
  53. data/lib/identity_cache.rb +66 -26
  54. data/performance/cache_runner.rb +12 -51
  55. data/performance/cpu.rb +7 -6
  56. data/performance/externals.rb +6 -5
  57. data/performance/profile.rb +7 -6
  58. metadata +32 -112
  59. data/.github/probots.yml +0 -2
  60. data/.travis.yml +0 -45
  61. data/gemfiles/Gemfile.rails52 +0 -6
@@ -2,15 +2,10 @@
2
2
  name: identity-cache
3
3
 
4
4
  vm:
5
- image: /opt/dev/misc/railgun-images/default
6
5
  ip_address: 192.168.64.98
7
6
  memory: 1G
8
7
  cores: 2
9
8
 
10
- volumes:
11
- root: 1G
12
-
13
9
  services:
14
10
  - mysql
15
- - postgresql
16
11
  - memcached
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module BelongsToCaching
4
5
  extend ActiveSupport::Concern
@@ -1,8 +1,53 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+
2
5
  module IdentityCache
3
6
  class CacheFetcher
4
7
  attr_accessor :cache_backend
5
8
 
9
+ EMPTY_HASH = {}.freeze
10
+
11
+ class FillLock
12
+ FILL_LOCKED = :fill_locked
13
+ FAILED_CLIENT_ID = "fill_failed"
14
+
15
+ class << self
16
+ def from_cache(marker, client_id, data_version)
17
+ raise ArgumentError unless marker == FILL_LOCKED
18
+
19
+ new(client_id: client_id, data_version: data_version)
20
+ end
21
+
22
+ def cache_value?(cache_value)
23
+ cache_value.is_a?(Array) && cache_value.length == 3 && cache_value.first == FILL_LOCKED
24
+ end
25
+ end
26
+
27
+ attr_reader :client_id, :data_version
28
+
29
+ def initialize(client_id:, data_version:)
30
+ @client_id = client_id
31
+ @data_version = data_version
32
+ end
33
+
34
+ def cache_value
35
+ [FILL_LOCKED, client_id, data_version]
36
+ end
37
+
38
+ def mark_failed
39
+ @client_id = FAILED_CLIENT_ID
40
+ end
41
+
42
+ def fill_failed?
43
+ @client_id == FAILED_CLIENT_ID
44
+ end
45
+
46
+ def ==(other)
47
+ self.class == other.class && client_id == other.client_id && data_version == other.data_version
48
+ end
49
+ end
50
+
6
51
  def initialize(cache_backend)
7
52
  @cache_backend = cache_backend
8
53
  end
@@ -25,27 +70,204 @@ module IdentityCache
25
70
  results
26
71
  end
27
72
 
28
- def fetch(key)
29
- result = nil
30
- yielded = false
31
- @cache_backend.cas(key) do |value|
32
- yielded = true
33
- unless IdentityCache::DELETED == value
34
- result = value
35
- break
73
+ def fetch(key, fill_lock_duration: nil, lock_wait_tries: 2, &block)
74
+ if fill_lock_duration && IdentityCache.should_fill_cache?
75
+ fetch_with_fill_lock(key, fill_lock_duration, lock_wait_tries, &block)
76
+ else
77
+ fetch_without_fill_lock(key, &block)
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ def fetch_without_fill_lock(key)
84
+ data = nil
85
+ upsert(key) do |value|
86
+ value = nil if value == IdentityCache::DELETED || FillLock.cache_value?(value)
87
+ unless value.nil?
88
+ return value
36
89
  end
37
- result = yield
90
+
91
+ data = yield
38
92
  break unless IdentityCache.should_fill_cache?
39
- result
93
+
94
+ data
95
+ end
96
+ data
97
+ end
98
+
99
+ def fetch_with_fill_lock(key, fill_lock_duration, lock_wait_tries, &block)
100
+ raise ArgumentError, "fill_lock_duration must be greater than 0.0" unless fill_lock_duration > 0.0
101
+ raise ArgumentError, "lock_wait_tries must be greater than 0" unless lock_wait_tries > 0
102
+
103
+ lock = nil
104
+ using_fallback_key = false
105
+ expiration_options = EMPTY_HASH
106
+ (lock_wait_tries + 2).times do # +2 is for first attempt and retry with fallback key
107
+ result = fetch_or_take_lock(key, old_lock: lock, **expiration_options)
108
+ case result
109
+ when FillLock
110
+ lock = result
111
+ if lock.client_id == client_id # have lock
112
+ data = begin
113
+ yield
114
+ rescue
115
+ mark_fill_failure_on_lock(key, expiration_options)
116
+ raise
117
+ end
118
+
119
+ if !fill_with_lock(key, data, lock, expiration_options) && !using_fallback_key
120
+ # fallback to storing data in the fallback key so it is available to clients waiting on the lock
121
+ expiration_options = fallback_key_expiration_options(fill_lock_duration)
122
+ @cache_backend.write(lock_fill_fallback_key(key, lock), data, expiration_options)
123
+ end
124
+ return data
125
+ else
126
+ raise LockWaitTimeout if lock_wait_tries <= 0
127
+
128
+ lock_wait_tries -= 1
129
+
130
+ # If fill failed in the other client, then it might be failing fast
131
+ # so avoid waiting the typical amount of time for a lock wait. The
132
+ # semian gem can be used to handle failing fast when the database is slow.
133
+ if lock.fill_failed?
134
+ return fetch_without_fill_lock(key, &block)
135
+ end
136
+
137
+ # lock wait
138
+ sleep(fill_lock_duration)
139
+ # loop around to retry fetch_or_take_lock
140
+ end
141
+ when IdentityCache::DELETED # interrupted by cache invalidation
142
+ if using_fallback_key
143
+ raise "unexpected cache invalidation of versioned fallback key"
144
+ elsif lock
145
+ # Cache invalidated during lock wait, use a versioned fallback key
146
+ # to avoid further cache invalidation interruptions.
147
+ using_fallback_key = true
148
+ key = lock_fill_fallback_key(key, lock)
149
+ expiration_options = fallback_key_expiration_options(fill_lock_duration)
150
+ # loop around to retry with fallback key
151
+ else
152
+ # Cache invalidation prevented lock from being taken or read, so we don't
153
+ # have a data version to use to build a shared fallback key. In the future
154
+ # we could add the data version to the cache invalidation value so a fallback
155
+ # key could be used here. For now, we assume that a cache invalidation occuring
156
+ # just after the cache wasn't filled is more likely a sign of a key that is
157
+ # written more than read (which this cache isn't a good fit for), rather than
158
+ # a thundering herd or reads.
159
+ return yield
160
+ end
161
+ when nil # Errors talking to memcached
162
+ return yield
163
+ else # hit
164
+ return result
165
+ end
166
+ end
167
+ raise "unexpected number of loop iterations"
168
+ end
169
+
170
+ def mark_fill_failure_on_lock(key, expiration_options)
171
+ @cache_backend.cas(key, expiration_options) do |value|
172
+ break unless FillLock.cache_value?(value)
173
+
174
+ lock = FillLock.from_cache(*value)
175
+ break if lock.client_id != client_id
176
+
177
+ lock.mark_failed
178
+ lock.cache_value
179
+ end
180
+ end
181
+
182
+ def upsert(key, expiration_options = EMPTY_HASH)
183
+ yielded = false
184
+ upserted = @cache_backend.cas(key, expiration_options) do |value|
185
+ yielded = true
186
+ yield value
40
187
  end
41
188
  unless yielded
42
- result = yield
43
- add(key, result)
189
+ data = yield nil
190
+ upserted = add(key, data, expiration_options)
44
191
  end
45
- result
192
+ upserted
46
193
  end
47
194
 
48
- private
195
+ def fetch_or_take_lock(key, old_lock:, **expiration_options)
196
+ new_lock = nil
197
+ upserted = upsert(key, expiration_options) do |value|
198
+ if value.nil? || value == IdentityCache::DELETED
199
+ if old_lock # cache invalidated
200
+ return value
201
+ else
202
+ new_lock = FillLock.new(client_id: client_id, data_version: SecureRandom.uuid)
203
+ end
204
+ elsif FillLock.cache_value?(value)
205
+ fetched_lock = FillLock.from_cache(*value)
206
+ if old_lock == fetched_lock
207
+ # preserve data version since there hasn't been any cache invalidations
208
+ new_lock = FillLock.new(client_id: client_id, data_version: old_lock.data_version)
209
+ elsif old_lock && fetched_lock.data_version != old_lock.data_version
210
+ # Cache was invalidated, then another lock was taken during a lock wait.
211
+ # Treat it as any other cache invalidation, where the caller will switch
212
+ # to the fallback key.
213
+ return IdentityCache::DELETED
214
+ else
215
+ return fetched_lock
216
+ end
217
+ else # hit
218
+ return value
219
+ end
220
+ new_lock.cache_value # take lock
221
+ end
222
+
223
+ return new_lock if upserted
224
+
225
+ value = @cache_backend.read(key)
226
+ if FillLock.cache_value?(value)
227
+ FillLock.from_cache(*value)
228
+ else
229
+ value
230
+ end
231
+ end
232
+
233
+ def fill_with_lock(key, data, my_lock, expiration_options)
234
+ upserted = upsert(key, expiration_options) do |value|
235
+ return false if value.nil? || value == IdentityCache::DELETED
236
+ return true unless FillLock.cache_value?(value) # already filled
237
+
238
+ current_lock = FillLock.from_cache(*value)
239
+ if current_lock.data_version != my_lock.data_version
240
+ return false # invalidated then relocked
241
+ end
242
+
243
+ data
244
+ end
245
+
246
+ upserted
247
+ end
248
+
249
+ def lock_fill_fallback_key(key, lock)
250
+ "lock_fill:#{lock.data_version}:#{key}"
251
+ end
252
+
253
+ def fallback_key_expiration_options(fill_lock_duration)
254
+ # Override the default TTL for the fallback key lock since it won't be used for very long.
255
+ expires_in = fill_lock_duration * 2
256
+
257
+ # memcached uses integer number of seconds for TTL so round up to avoid having
258
+ # the cache store round down with `to_i`
259
+ expires_in = expires_in.ceil
260
+
261
+ # memcached TTL only gets part of the first second (https://github.com/memcached/memcached/issues/307),
262
+ # so increase TTL by 1 to compensate
263
+ expires_in += 1
264
+
265
+ { expires_in: expires_in }
266
+ end
267
+
268
+ def client_id
269
+ @client_id ||= SecureRandom.uuid
270
+ end
49
271
 
50
272
  def cas_multi(keys)
51
273
  result = nil
@@ -70,6 +292,7 @@ module IdentityCache
70
292
 
71
293
  break if updates.empty?
72
294
  break unless IdentityCache.should_fill_cache?
295
+
73
296
  updates
74
297
  end
75
298
  result
@@ -81,8 +304,10 @@ module IdentityCache
81
304
  result.each { |k, v| add(k, v) }
82
305
  end
83
306
 
84
- def add(key, value)
85
- @cache_backend.write(key, value, unless_exist: true) if IdentityCache.should_fill_cache?
307
+ def add(key, value, expiration_options = EMPTY_HASH)
308
+ return false unless IdentityCache.should_fill_cache?
309
+
310
+ @cache_backend.write(key, value, { unless_exist: true, **expiration_options })
86
311
  end
87
312
  end
88
313
  end
@@ -1,9 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  # Use CityHash for fast hashing if it is available; use Digest::MD5 otherwise
3
4
  begin
4
- require 'cityhash'
5
+ require "cityhash"
5
6
  rescue LoadError
6
- unless RUBY_PLATFORM == 'java'
7
+ unless RUBY_PLATFORM == "java"
7
8
  warn(<<-NOTICE)
8
9
  ** Notice: CityHash was not loaded. **
9
10
 
@@ -15,20 +16,20 @@ rescue LoadError
15
16
  NOTICE
16
17
  end
17
18
 
18
- require 'digest/md5'
19
+ require "digest/md5"
19
20
  end
20
21
 
21
22
  module IdentityCache
22
23
  module CacheHash
23
24
  if defined?(CityHash)
24
25
 
25
- def memcache_hash(key) #:nodoc:
26
+ def memcache_hash(key) # :nodoc:
26
27
  CityHash.hash64(key)
27
28
  end
28
29
  else
29
30
 
30
- def memcache_hash(key) #:nodoc:
31
- a = Digest::MD5.digest(key).unpack('LL')
31
+ def memcache_hash(key) # :nodoc:
32
+ a = Digest::MD5.digest(key).unpack("LL")
32
33
  (a[0] << 32) | a[1]
33
34
  end
34
35
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module CacheInvalidation
4
5
  CACHE_KEY_NAMES = [:ids_variable_name, :id_variable_name, :records_variable_name]
@@ -11,7 +12,7 @@ module IdentityCache
11
12
  private
12
13
 
13
14
  def clear_cached_associations
14
- self.class.send(:all_cached_associations).each_value do |association|
15
+ self.class.all_cached_associations.each_value do |association|
15
16
  association.clear(self)
16
17
  end
17
18
  end
@@ -1,33 +1,36 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module CacheKeyGeneration
4
5
  extend ActiveSupport::Concern
5
6
  DEFAULT_NAMESPACE = "IDC:#{CACHE_VERSION}:"
6
7
 
7
- def self.schema_to_string(columns)
8
- columns.sort_by(&:name).map { |c| "#{c.name}:#{c.type}" }.join(',')
9
- end
8
+ class << self
9
+ def schema_to_string(columns)
10
+ columns.sort_by(&:name).map { |c| "#{c.name}:#{c.type}" }.join(",")
11
+ end
10
12
 
11
- def self.denormalized_schema_string(klass)
12
- schema_to_string(klass.columns).tap do |schema_string|
13
- klass.send(:all_cached_associations).sort.each do |name, association|
14
- klass.send(:check_association_scope, name)
15
- association.validate if association.embedded?
16
- case association
17
- when Cached::Recursive::Association
18
- schema_string << ",#{name}:(#{denormalized_schema_hash(association.reflection.klass)})"
19
- when Cached::Reference::HasMany
20
- schema_string << ",#{name}:ids"
21
- when Cached::Reference::HasOne
22
- schema_string << ",#{name}:id"
13
+ def denormalized_schema_string(klass)
14
+ schema_to_string(klass.columns).tap do |schema_string|
15
+ klass.all_cached_associations.sort.each do |name, association|
16
+ klass.send(:check_association_scope, name)
17
+ association.validate if association.embedded?
18
+ case association
19
+ when Cached::Recursive::Association
20
+ schema_string << ",#{name}:(#{denormalized_schema_hash(association.reflection.klass)})"
21
+ when Cached::Reference::HasMany
22
+ schema_string << ",#{name}:ids"
23
+ when Cached::Reference::HasOne
24
+ schema_string << ",#{name}:id"
25
+ end
23
26
  end
24
27
  end
25
28
  end
26
- end
27
29
 
28
- def self.denormalized_schema_hash(klass)
29
- schema_string = denormalized_schema_string(klass)
30
- IdentityCache.memcache_hash(schema_string)
30
+ def denormalized_schema_hash(klass)
31
+ schema_string = denormalized_schema_string(klass)
32
+ IdentityCache.memcache_hash(schema_string)
33
+ end
31
34
  end
32
35
 
33
36
  module ClassMethods
@@ -25,12 +25,12 @@ module IdentityCache
25
25
  # @param cache_fetcher [_CacheFetcher]
26
26
  # @param db_key Reference to what to load from the database.
27
27
  # @return The database value corresponding to the database key.
28
- def load(cache_fetcher, db_key)
28
+ def load(cache_fetcher, db_key, cache_fetcher_options = {})
29
29
  cache_key = cache_fetcher.cache_key(db_key)
30
30
 
31
31
  db_value = nil
32
32
 
33
- cache_value = IdentityCache.fetch(cache_key) do
33
+ cache_value = IdentityCache.fetch(cache_key, cache_fetcher_options) do
34
34
  db_value = cache_fetcher.load_one_from_db(db_key)
35
35
  cache_fetcher.cache_encode(db_value)
36
36
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  class Association # :nodoc:
@@ -50,10 +51,7 @@ module IdentityCache
50
51
  end
51
52
 
52
53
  def inverse_name
53
- @inverse_name ||= begin
54
- reflection.inverse_of&.name ||
55
- reflection.active_record.name.underscore
56
- end
54
+ @inverse_name ||= reflection.inverse_of&.name || reflection.active_record.name.underscore
57
55
  end
58
56
 
59
57
  def validate
@@ -87,11 +87,11 @@ module IdentityCache
87
87
 
88
88
  def cache_key_prefix
89
89
  @cache_key_prefix ||= begin
90
- unique_indicator = unique ? '' : 's'
90
+ unique_indicator = unique ? "" : "s"
91
91
  "attr#{unique_indicator}" \
92
92
  ":#{model.base_class.name}" \
93
93
  ":#{attribute}" \
94
- ":#{key_fields.join('/')}:"
94
+ ":#{key_fields.join("/")}:"
95
95
  end
96
96
  end
97
97
 
@@ -116,7 +116,7 @@ module IdentityCache
116
116
  end
117
117
 
118
118
  def fetch_method_suffix
119
- "#{alias_name}_by_#{key_fields.join('_and_')}"
119
+ "#{alias_name}_by_#{key_fields.join("_and_")}"
120
120
  end
121
121
  end
122
122
  end
@@ -24,7 +24,7 @@ module IdentityCache
24
24
  end
25
25
 
26
26
  def unhashed_values_cache_key_string(key_values)
27
- key_values.map { |v| v.try!(:to_s).inspect }.join('/')
27
+ key_values.map { |v| v.try!(:to_s).inspect }.join("/")
28
28
  end
29
29
 
30
30
  def load_from_db_where_conditions(key_values)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  class BelongsTo < Association # :nodoc:
@@ -27,34 +28,43 @@ module IdentityCache
27
28
  end
28
29
  end
29
30
 
31
+ def write(owner_record, associated_record)
32
+ owner_record.instance_variable_set(records_variable_name, associated_record)
33
+ end
34
+
30
35
  def fetch(records)
31
36
  fetch_async(LoadStrategy::Eager, records) { |associated_records| associated_records }
32
37
  end
33
38
 
34
39
  def fetch_async(load_strategy, records)
35
40
  if reflection.polymorphic?
36
- cache_keys_to_associated_ids = {}
41
+ type_fetcher_to_db_ids_hash = {}
37
42
 
38
43
  records.each do |owner_record|
39
44
  associated_id = owner_record.send(reflection.foreign_key)
40
45
  next unless associated_id && !owner_record.instance_variable_defined?(records_variable_name)
41
- associated_cache_key = Object.const_get(
46
+
47
+ foreign_type_fetcher = Object.const_get(
42
48
  owner_record.send(reflection.foreign_type)
43
49
  ).cached_model.cached_primary_index
44
- unless cache_keys_to_associated_ids[associated_cache_key]
45
- cache_keys_to_associated_ids[associated_cache_key] = {}
46
- end
47
- cache_keys_to_associated_ids[associated_cache_key][associated_id] = owner_record
50
+ db_ids = type_fetcher_to_db_ids_hash[foreign_type_fetcher] ||= []
51
+ db_ids << associated_id
48
52
  end
49
53
 
50
- load_strategy.load_batch(cache_keys_to_associated_ids) do |associated_records_by_cache_key|
54
+ load_strategy.load_batch(type_fetcher_to_db_ids_hash) do |batch_load_result|
51
55
  batch_records = []
52
- associated_records_by_cache_key.each do |cache_key, associated_records|
53
- associated_records.keys.each do |id, associated_record|
54
- owner_record = cache_keys_to_associated_ids.fetch(cache_key).fetch(id)
55
- batch_records << owner_record
56
- owner_record.instance_variable_set(records_variable_name, associated_record)
57
- end
56
+
57
+ records.each do |owner_record|
58
+ associated_id = owner_record.send(reflection.foreign_key)
59
+ next unless associated_id && !owner_record.instance_variable_defined?(records_variable_name)
60
+
61
+ foreign_type_fetcher = Object.const_get(
62
+ owner_record.send(reflection.foreign_type)
63
+ ).cached_model.cached_primary_index
64
+
65
+ associated_record = batch_load_result.fetch(foreign_type_fetcher).fetch(associated_id)
66
+ batch_records << owner_record
67
+ write(owner_record, associated_record)
58
68
  end
59
69
 
60
70
  yield batch_records
@@ -73,7 +83,7 @@ module IdentityCache
73
83
  ) do |associated_records_by_id|
74
84
  associated_records_by_id.each do |id, associated_record|
75
85
  owner_record = ids_to_owner_record.fetch(id)
76
- owner_record.instance_variable_set(records_variable_name, associated_record)
86
+ write(owner_record, associated_record)
77
87
  end
78
88
 
79
89
  yield associated_records_by_id.values.compact
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  module EmbeddedFetching
@@ -22,6 +23,7 @@ module IdentityCache
22
23
  cached_associations.each_value do |cached_association|
23
24
  records.each do |record|
24
25
  next unless (cached_record = cached_records_by_id[record.id])
26
+
25
27
  cached_value = cached_association.read(cached_record)
26
28
  cached_association.write(record, cached_value)
27
29
  end
@@ -37,14 +37,24 @@ module IdentityCache
37
37
  private
38
38
 
39
39
  def fetch_association(load_strategy, klass, association, records, &block)
40
- unless records.first.class.should_use_cache?
41
- ActiveRecord::Associations::Preloader.new.preload(records, association)
40
+ unless klass.should_use_cache?
41
+ preload_records(records, association)
42
42
  return yield
43
43
  end
44
44
 
45
45
  cached_association = klass.cached_association(association)
46
46
  cached_association.fetch_async(load_strategy, records, &block)
47
47
  end
48
+
49
+ if ActiveRecord.gem_version < Gem::Version.new("6.2.0.alpha")
50
+ def preload_records(records, association)
51
+ ActiveRecord::Associations::Preloader.new.preload(records, association)
52
+ end
53
+ else
54
+ def preload_records(records, association)
55
+ ActiveRecord::Associations::Preloader.new(records: records, associations: association).call
56
+ end
57
+ end
48
58
  end
49
59
  end
50
60
  end
@@ -9,11 +9,12 @@ module IdentityCache
9
9
  @model = model
10
10
  end
11
11
 
12
- def fetch(id)
12
+ def fetch(id, cache_fetcher_options)
13
13
  id = cast_id(id)
14
14
  return unless id
15
+
15
16
  record = if model.should_use_cache?
16
- object = CacheKeyLoader.load(self, id)
17
+ object = CacheKeyLoader.load(self, id, cache_fetcher_options)
17
18
  if object && object.id != id
18
19
  IdentityCache.logger.error(
19
20
  <<~MSG.squish
@@ -60,7 +61,6 @@ module IdentityCache
60
61
  def load_multi_from_db(ids)
61
62
  return {} if ids.empty?
62
63
 
63
- ids = ids.map { |id| model.connection.type_cast(id, id_column) }
64
64
  records = build_query(ids).to_a
65
65
  model.send(:setup_embedded_associations_on_miss, records)
66
66
  records.index_by(&:id)