identity_cache 1.0.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +76 -9
- data/.github/workflows/cla.yml +22 -0
- data/.gitignore +1 -0
- data/.rubocop.yml +7 -3
- data/.spin/bootstrap +7 -0
- data/.spin/svc.yml +2 -0
- data/CAVEATS.md +25 -0
- data/CHANGELOG.md +56 -22
- data/Gemfile +15 -5
- data/LICENSE +1 -1
- data/README.md +27 -8
- data/Rakefile +13 -12
- data/dev.yml +5 -4
- data/gemfiles/Gemfile.latest-release +12 -5
- data/gemfiles/Gemfile.min-supported +12 -0
- data/gemfiles/Gemfile.rails-edge +9 -5
- data/identity_cache.gemspec +15 -24
- data/{railgun.yml → isogun.yml} +0 -5
- data/lib/identity_cache/belongs_to_caching.rb +1 -0
- data/lib/identity_cache/cache_fetcher.rb +241 -16
- data/lib/identity_cache/cache_hash.rb +7 -6
- data/lib/identity_cache/cache_invalidation.rb +2 -1
- data/lib/identity_cache/cache_key_generation.rb +22 -19
- data/lib/identity_cache/cache_key_loader.rb +2 -2
- data/lib/identity_cache/cached/association.rb +2 -4
- data/lib/identity_cache/cached/attribute.rb +3 -3
- data/lib/identity_cache/cached/attribute_by_multi.rb +1 -1
- data/lib/identity_cache/cached/belongs_to.rb +24 -14
- data/lib/identity_cache/cached/embedded_fetching.rb +2 -0
- data/lib/identity_cache/cached/prefetcher.rb +12 -2
- data/lib/identity_cache/cached/primary_index.rb +3 -3
- data/lib/identity_cache/cached/recursive/association.rb +55 -12
- data/lib/identity_cache/cached/recursive/has_many.rb +1 -0
- data/lib/identity_cache/cached/recursive/has_one.rb +1 -0
- data/lib/identity_cache/cached/reference/association.rb +1 -0
- data/lib/identity_cache/cached/reference/has_many.rb +3 -2
- data/lib/identity_cache/cached/reference/has_one.rb +3 -2
- data/lib/identity_cache/cached.rb +1 -0
- data/lib/identity_cache/configuration_dsl.rb +1 -0
- data/lib/identity_cache/encoder.rb +2 -1
- data/lib/identity_cache/expiry_hook.rb +2 -1
- data/lib/identity_cache/fallback_fetcher.rb +6 -1
- data/lib/identity_cache/mem_cache_store_cas.rb +63 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +33 -23
- data/lib/identity_cache/parent_model_expiration.rb +6 -3
- data/lib/identity_cache/query_api.rb +29 -66
- data/lib/identity_cache/railtie.rb +1 -0
- data/lib/identity_cache/should_use_cache.rb +1 -0
- data/lib/identity_cache/version.rb +2 -1
- data/lib/identity_cache/with_primary_index.rb +37 -10
- data/lib/identity_cache/without_primary_index.rb +7 -3
- data/lib/identity_cache.rb +66 -26
- data/performance/cache_runner.rb +12 -51
- data/performance/cpu.rb +7 -6
- data/performance/externals.rb +6 -5
- data/performance/profile.rb +7 -6
- metadata +32 -112
- data/.github/probots.yml +0 -2
- data/.travis.yml +0 -45
- data/gemfiles/Gemfile.rails52 +0 -6
data/{railgun.yml → isogun.yml}
RENAMED
@@ -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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
90
|
+
|
91
|
+
data = yield
|
38
92
|
break unless IdentityCache.should_fill_cache?
|
39
|
-
|
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
|
-
|
43
|
-
add(key,
|
189
|
+
data = yield nil
|
190
|
+
upserted = add(key, data, expiration_options)
|
44
191
|
end
|
45
|
-
|
192
|
+
upserted
|
46
193
|
end
|
47
194
|
|
48
|
-
|
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
|
-
|
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
|
5
|
+
require "cityhash"
|
5
6
|
rescue LoadError
|
6
|
-
unless RUBY_PLATFORM ==
|
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
|
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)
|
26
|
+
def memcache_hash(key) # :nodoc:
|
26
27
|
CityHash.hash64(key)
|
27
28
|
end
|
28
29
|
else
|
29
30
|
|
30
|
-
def memcache_hash(key)
|
31
|
-
a = Digest::MD5.digest(key).unpack(
|
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.
|
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
|
-
|
8
|
-
|
9
|
-
|
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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
29
|
-
|
30
|
-
|
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 ||=
|
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 ?
|
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(
|
119
|
+
"#{alias_name}_by_#{key_fields.join("_and_")}"
|
120
120
|
end
|
121
121
|
end
|
122
122
|
end
|
@@ -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
|
-
|
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
|
-
|
46
|
+
|
47
|
+
foreign_type_fetcher = Object.const_get(
|
42
48
|
owner_record.send(reflection.foreign_type)
|
43
49
|
).cached_model.cached_primary_index
|
44
|
-
|
45
|
-
|
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(
|
54
|
+
load_strategy.load_batch(type_fetcher_to_db_ids_hash) do |batch_load_result|
|
51
55
|
batch_records = []
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
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
|
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
|
41
|
-
|
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)
|