identity_cache 1.1.0 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +5 -4
  3. data/.github/workflows/cla.yml +22 -0
  4. data/.rubocop.yml +7 -3
  5. data/.spin/bootstrap +7 -0
  6. data/.spin/svc.yml +2 -0
  7. data/CHANGELOG.md +10 -0
  8. data/Gemfile +15 -5
  9. data/LICENSE +1 -1
  10. data/README.md +5 -6
  11. data/Rakefile +13 -12
  12. data/dev.yml +2 -4
  13. data/gemfiles/Gemfile.latest-release +12 -7
  14. data/gemfiles/Gemfile.min-supported +11 -6
  15. data/gemfiles/Gemfile.rails-edge +8 -5
  16. data/identity_cache.gemspec +15 -27
  17. data/{railgun.yml → isogun.yml} +0 -5
  18. data/lib/identity_cache/belongs_to_caching.rb +1 -0
  19. data/lib/identity_cache/cache_fetcher.rb +241 -16
  20. data/lib/identity_cache/cache_hash.rb +7 -6
  21. data/lib/identity_cache/cache_invalidation.rb +1 -0
  22. data/lib/identity_cache/cache_key_generation.rb +22 -19
  23. data/lib/identity_cache/cache_key_loader.rb +2 -2
  24. data/lib/identity_cache/cached/association.rb +2 -4
  25. data/lib/identity_cache/cached/attribute.rb +3 -3
  26. data/lib/identity_cache/cached/attribute_by_multi.rb +1 -1
  27. data/lib/identity_cache/cached/belongs_to.rb +3 -0
  28. data/lib/identity_cache/cached/embedded_fetching.rb +2 -0
  29. data/lib/identity_cache/cached/primary_index.rb +3 -2
  30. data/lib/identity_cache/cached/recursive/association.rb +2 -0
  31. data/lib/identity_cache/cached/recursive/has_many.rb +1 -0
  32. data/lib/identity_cache/cached/recursive/has_one.rb +1 -0
  33. data/lib/identity_cache/cached/reference/association.rb +1 -0
  34. data/lib/identity_cache/cached/reference/has_many.rb +1 -0
  35. data/lib/identity_cache/cached/reference/has_one.rb +1 -0
  36. data/lib/identity_cache/cached.rb +1 -0
  37. data/lib/identity_cache/configuration_dsl.rb +1 -0
  38. data/lib/identity_cache/encoder.rb +2 -1
  39. data/lib/identity_cache/expiry_hook.rb +1 -0
  40. data/lib/identity_cache/fallback_fetcher.rb +6 -1
  41. data/lib/identity_cache/mem_cache_store_cas.rb +15 -5
  42. data/lib/identity_cache/memoized_cache_proxy.rb +15 -15
  43. data/lib/identity_cache/parent_model_expiration.rb +3 -1
  44. data/lib/identity_cache/query_api.rb +3 -0
  45. data/lib/identity_cache/railtie.rb +1 -0
  46. data/lib/identity_cache/should_use_cache.rb +1 -0
  47. data/lib/identity_cache/version.rb +2 -1
  48. data/lib/identity_cache/with_primary_index.rb +37 -10
  49. data/lib/identity_cache/without_primary_index.rb +7 -3
  50. data/lib/identity_cache.rb +38 -24
  51. data/performance/cache_runner.rb +12 -9
  52. data/performance/cpu.rb +6 -5
  53. data/performance/externals.rb +6 -5
  54. data/performance/profile.rb +7 -6
  55. metadata +27 -123
  56. data/.github/probots.yml +0 -2
@@ -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]
@@ -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.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:
@@ -42,6 +43,7 @@ module IdentityCache
42
43
  records.each do |owner_record|
43
44
  associated_id = owner_record.send(reflection.foreign_key)
44
45
  next unless associated_id && !owner_record.instance_variable_defined?(records_variable_name)
46
+
45
47
  foreign_type_fetcher = Object.const_get(
46
48
  owner_record.send(reflection.foreign_type)
47
49
  ).cached_model.cached_primary_index
@@ -55,6 +57,7 @@ module IdentityCache
55
57
  records.each do |owner_record|
56
58
  associated_id = owner_record.send(reflection.foreign_key)
57
59
  next unless associated_id && !owner_record.instance_variable_defined?(records_variable_name)
60
+
58
61
  foreign_type_fetcher = Object.const_get(
59
62
  owner_record.send(reflection.foreign_type)
60
63
  ).cached_model.cached_primary_index
@@ -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
@@ -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
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  module Recursive
@@ -77,6 +78,7 @@ module IdentityCache
77
78
 
78
79
  def set_inverse(record, association_target)
79
80
  return if association_target.nil?
81
+
80
82
  associated_class = reflection.klass
81
83
  inverse_cached_association = associated_class.cached_belongs_tos[inverse_name]
82
84
  return unless inverse_cached_association
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  module Recursive
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  module Recursive
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  module Reference
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  module Reference
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  module Reference
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Cached
4
5
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module ConfigurationDSL
4
5
  extend ActiveSupport::Concern
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module Encoder
4
5
  DEHYDRATE_EVENT = "dehydration.identity_cache"
@@ -66,7 +67,7 @@ module IdentityCache
66
67
  end
67
68
  end
68
69
 
69
- def record_from_coder(coder, klass) #:nodoc:
70
+ def record_from_coder(coder, klass) # :nodoc:
70
71
  record = klass.instantiate(coder[:attributes].dup)
71
72
 
72
73
  if coder.key?(:associations)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  class ExpiryHook
4
5
  def initialize(cached_association)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  class FallbackFetcher
4
5
  attr_accessor :cache_backend
@@ -32,7 +33,11 @@ module IdentityCache
32
33
  results
33
34
  end
34
35
 
35
- def fetch(key)
36
+ def fetch(key, **cache_fetcher_options)
37
+ unless cache_fetcher_options.empty?
38
+ raise ArgumentError, "unsupported cache_fetcher options: #{cache_fetcher_options.keys.join(", ")}"
39
+ end
40
+
36
41
  result = @cache_backend.read(key)
37
42
  if result.nil?
38
43
  result = yield
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
- require 'dalli/cas/client'
2
+
3
+ require "dalli/cas/client" unless Dalli::VERSION > "3"
3
4
 
4
5
  module IdentityCache
5
6
  module MemCacheStoreCAS
@@ -11,7 +12,7 @@ module IdentityCache
11
12
  instrument(:cas, key, options) do
12
13
  @data.with do |connection|
13
14
  connection.cas(key, options[:expires_in].to_i, options) do |raw_value|
14
- entry = deserialize_entry(raw_value)
15
+ entry = deserialize_entry(raw_value, raw: options[:raw])
15
16
  value = yield entry.value
16
17
  entry = ActiveSupport::Cache::Entry.new(value, **options)
17
18
  options[:raw] ? entry.value.to_s : entry
@@ -29,11 +30,11 @@ module IdentityCache
29
30
  keys = keys_to_names.keys
30
31
  rescue_error_with(false) do
31
32
  instrument(:cas_multi, keys, options) do
32
- raw_values = @data.get_multi_cas(keys)
33
+ raw_values = @data.with { |c| c.get_multi_cas(keys) }
33
34
 
34
35
  values = {}
35
36
  raw_values.each do |key, raw_value|
36
- entry = deserialize_entry(raw_value.first)
37
+ entry = deserialize_entry(raw_value.first, raw: options[:raw])
37
38
  values[keys_to_names[key]] = entry.value unless entry.expired?
38
39
  end
39
40
 
@@ -44,10 +45,19 @@ module IdentityCache
44
45
  cas_id = raw_values[key].last
45
46
  entry = ActiveSupport::Cache::Entry.new(value, **options)
46
47
  payload = options[:raw] ? entry.value.to_s : entry
47
- @data.replace_cas(key, payload, cas_id, options[:expires_in].to_i, options)
48
+ @data.with { |c| c.replace_cas(key, payload, cas_id, options[:expires_in].to_i, options) }
48
49
  end
49
50
  end
50
51
  end
51
52
  end
53
+
54
+ if ActiveSupport::Cache::MemCacheStore.instance_method(:deserialize_entry).arity == 1
55
+
56
+ private
57
+
58
+ def deserialize_entry(payload, raw: nil)
59
+ super(payload)
60
+ end
61
+ end
52
62
  end
53
63
  end