identity_cache 1.1.0 → 1.2.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.
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