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.
- 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
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module IdentityCache
|
3
4
|
module Cached
|
4
5
|
module Recursive
|
@@ -11,25 +12,42 @@ module IdentityCache
|
|
11
12
|
attr_reader :dehydrated_variable_name
|
12
13
|
|
13
14
|
def build
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
)
|
21
|
-
end
|
22
|
-
RUBY
|
15
|
+
cached_association = self
|
16
|
+
|
17
|
+
model = reflection.active_record
|
18
|
+
model.define_method(cached_accessor_name) do
|
19
|
+
cached_association.read(self)
|
20
|
+
end
|
23
21
|
|
24
22
|
ParentModelExpiration.add_parent_expiry_hook(self)
|
25
23
|
end
|
26
24
|
|
27
25
|
def read(record)
|
28
|
-
record.
|
26
|
+
assoc = record.association(name)
|
27
|
+
|
28
|
+
if assoc.klass.should_use_cache? && !assoc.loaded? && assoc.target.blank?
|
29
|
+
if record.instance_variable_defined?(records_variable_name)
|
30
|
+
record.instance_variable_get(records_variable_name)
|
31
|
+
elsif record.instance_variable_defined?(dehydrated_variable_name)
|
32
|
+
dehydrated_target = record.instance_variable_get(dehydrated_variable_name)
|
33
|
+
association_target = hydrate_association_target(assoc.klass, dehydrated_target)
|
34
|
+
record.remove_instance_variable(dehydrated_variable_name)
|
35
|
+
set_with_inverse(record, association_target)
|
36
|
+
else
|
37
|
+
assoc.load_target
|
38
|
+
end
|
39
|
+
else
|
40
|
+
assoc.load_target
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def write(record, association_target)
|
45
|
+
record.instance_variable_set(records_variable_name, association_target)
|
29
46
|
end
|
30
47
|
|
31
|
-
def
|
32
|
-
record
|
48
|
+
def set_with_inverse(record, association_target)
|
49
|
+
set_inverse(record, association_target)
|
50
|
+
write(record, association_target)
|
33
51
|
end
|
34
52
|
|
35
53
|
def clear(record)
|
@@ -58,6 +76,31 @@ module IdentityCache
|
|
58
76
|
|
59
77
|
private
|
60
78
|
|
79
|
+
def set_inverse(record, association_target)
|
80
|
+
return if association_target.nil?
|
81
|
+
|
82
|
+
associated_class = reflection.klass
|
83
|
+
inverse_cached_association = associated_class.cached_belongs_tos[inverse_name]
|
84
|
+
return unless inverse_cached_association
|
85
|
+
|
86
|
+
if association_target.is_a?(Array)
|
87
|
+
association_target.each do |child_record|
|
88
|
+
inverse_cached_association.write(child_record, record)
|
89
|
+
end
|
90
|
+
else
|
91
|
+
inverse_cached_association.write(association_target, record)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def hydrate_association_target(associated_class, dehydrated_value)
|
96
|
+
dehydrated_value = IdentityCache.unmap_cached_nil_for(dehydrated_value)
|
97
|
+
if dehydrated_value.is_a?(Array)
|
98
|
+
dehydrated_value.map { |coder| Encoder.decode(coder, associated_class) }
|
99
|
+
else
|
100
|
+
Encoder.decode(dehydrated_value, associated_class)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
61
104
|
def embedded_fetched?(records)
|
62
105
|
record = records.first
|
63
106
|
super || record.instance_variable_defined?(dehydrated_variable_name)
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module IdentityCache
|
3
4
|
module Cached
|
4
5
|
module Reference
|
@@ -20,8 +21,8 @@ module IdentityCache
|
|
20
21
|
end
|
21
22
|
|
22
23
|
def #{cached_accessor_name}
|
23
|
-
|
24
|
-
if
|
24
|
+
assoc = association(:#{name})
|
25
|
+
if assoc.klass.should_use_cache? && !assoc.loaded? && assoc.target.blank?
|
25
26
|
#{records_variable_name} ||= #{reflection.class_name}.fetch_multi(#{cached_ids_name})
|
26
27
|
else
|
27
28
|
#{name}.to_a
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module IdentityCache
|
3
4
|
module Cached
|
4
5
|
module Reference
|
@@ -21,8 +22,8 @@ module IdentityCache
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def #{cached_accessor_name}
|
24
|
-
|
25
|
-
if
|
25
|
+
assoc = association(:#{name})
|
26
|
+
if assoc.klass.should_use_cache? && !assoc.loaded?
|
26
27
|
#{records_variable_name} ||= #{reflection.class_name}.fetch(#{cached_id_name}) if #{cached_id_name}
|
27
28
|
else
|
28
29
|
#{name}
|
@@ -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)
|
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)
|
@@ -16,7 +17,7 @@ module IdentityCache
|
|
16
17
|
attr_reader :cached_association
|
17
18
|
|
18
19
|
def only_on_foreign_key_change?
|
19
|
-
cached_association.embedded_by_reference?
|
20
|
+
cached_association.embedded_by_reference? && !cached_association.reflection.has_scope?
|
20
21
|
end
|
21
22
|
|
22
23
|
def inverse_name
|
@@ -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
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "dalli/cas/client" unless Dalli::VERSION > "3"
|
4
|
+
|
5
|
+
module IdentityCache
|
6
|
+
module MemCacheStoreCAS
|
7
|
+
def cas(name, options = nil)
|
8
|
+
options = merged_options(options)
|
9
|
+
key = normalize_key(name, options)
|
10
|
+
|
11
|
+
rescue_error_with(false) do
|
12
|
+
instrument(:cas, key, options) do
|
13
|
+
@data.with do |connection|
|
14
|
+
connection.cas(key, options[:expires_in].to_i, options) do |raw_value|
|
15
|
+
entry = deserialize_entry(raw_value, raw: options[:raw])
|
16
|
+
value = yield entry.value
|
17
|
+
entry = ActiveSupport::Cache::Entry.new(value, **options)
|
18
|
+
options[:raw] ? entry.value.to_s : entry
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def cas_multi(*names, **options)
|
26
|
+
return if names.empty?
|
27
|
+
|
28
|
+
options = merged_options(options)
|
29
|
+
keys_to_names = names.each_with_object({}) { |name, hash| hash[normalize_key(name, options)] = name }
|
30
|
+
keys = keys_to_names.keys
|
31
|
+
rescue_error_with(false) do
|
32
|
+
instrument(:cas_multi, keys, options) do
|
33
|
+
raw_values = @data.with { |c| c.get_multi_cas(keys) }
|
34
|
+
|
35
|
+
values = {}
|
36
|
+
raw_values.each do |key, raw_value|
|
37
|
+
entry = deserialize_entry(raw_value.first, raw: options[:raw])
|
38
|
+
values[keys_to_names[key]] = entry.value unless entry.expired?
|
39
|
+
end
|
40
|
+
|
41
|
+
updates = yield values
|
42
|
+
|
43
|
+
updates.each do |name, value|
|
44
|
+
key = normalize_key(name, options)
|
45
|
+
cas_id = raw_values[key].last
|
46
|
+
entry = ActiveSupport::Cache::Entry.new(value, **options)
|
47
|
+
payload = options[:raw] ? entry.value.to_s : entry
|
48
|
+
@data.with { |c| c.replace_cas(key, payload, cas_id, options[:expires_in].to_i, options) }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
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
|
62
|
+
end
|
63
|
+
end
|
@@ -1,6 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require
|
2
|
+
|
3
|
+
require "monitor"
|
4
|
+
require "benchmark"
|
4
5
|
|
5
6
|
module IdentityCache
|
6
7
|
class MemoizedCacheProxy
|
@@ -12,6 +13,16 @@ module IdentityCache
|
|
12
13
|
end
|
13
14
|
|
14
15
|
def cache_backend=(cache_adaptor)
|
16
|
+
if cache_adaptor.class.name == "ActiveSupport::Cache::MemCacheStore"
|
17
|
+
if cache_adaptor.respond_to?(:cas) || cache_adaptor.respond_to?(:cas_multi)
|
18
|
+
unless cache_adaptor.is_a?(MemCacheStoreCAS)
|
19
|
+
raise "#{cache_adaptor} respond to :cas or :cas_multi, that's unexpected"
|
20
|
+
end
|
21
|
+
else
|
22
|
+
cache_adaptor.extend(MemCacheStoreCAS)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
15
26
|
if cache_adaptor.respond_to?(:cas) && cache_adaptor.respond_to?(:cas_multi)
|
16
27
|
@cache_fetcher = CacheFetcher.new(cache_adaptor)
|
17
28
|
else
|
@@ -20,7 +31,7 @@ module IdentityCache
|
|
20
31
|
# no need for CAS support
|
21
32
|
else
|
22
33
|
warn("[IdentityCache] Missing CAS support in cache backend #{cache_adaptor.class} "\
|
23
|
-
|
34
|
+
"which is needed for cache consistency")
|
24
35
|
end
|
25
36
|
@cache_fetcher = FallbackFetcher.new(cache_adaptor)
|
26
37
|
end
|
@@ -40,7 +51,7 @@ module IdentityCache
|
|
40
51
|
|
41
52
|
def write(key, value)
|
42
53
|
memoizing = memoizing?
|
43
|
-
ActiveSupport::Notifications.instrument(
|
54
|
+
ActiveSupport::Notifications.instrument("cache_write.identity_cache", memoizing: memoizing) do
|
44
55
|
memoized_key_values[key] = value if memoizing
|
45
56
|
@cache_fetcher.write(key, value)
|
46
57
|
end
|
@@ -48,7 +59,7 @@ module IdentityCache
|
|
48
59
|
|
49
60
|
def delete(key)
|
50
61
|
memoizing = memoizing?
|
51
|
-
ActiveSupport::Notifications.instrument(
|
62
|
+
ActiveSupport::Notifications.instrument("cache_delete.identity_cache", memoizing: memoizing) do
|
52
63
|
memoized_key_values.delete(key) if memoizing
|
53
64
|
if (result = @cache_fetcher.delete(key))
|
54
65
|
IdentityCache.logger.debug { "[IdentityCache] delete recorded for #{key}" }
|
@@ -59,20 +70,18 @@ module IdentityCache
|
|
59
70
|
end
|
60
71
|
end
|
61
72
|
|
62
|
-
def fetch(key)
|
73
|
+
def fetch(key, cache_fetcher_options = {}, &block)
|
63
74
|
memo_misses = 0
|
64
75
|
cache_misses = 0
|
65
76
|
|
66
|
-
value = ActiveSupport::Notifications.instrument(
|
77
|
+
value = ActiveSupport::Notifications.instrument("cache_fetch.identity_cache") do |payload|
|
67
78
|
payload[:resolve_miss_time] = 0.0
|
68
79
|
|
69
80
|
value = fetch_memoized(key) do
|
70
81
|
memo_misses = 1
|
71
|
-
@cache_fetcher.fetch(key) do
|
82
|
+
@cache_fetcher.fetch(key, **cache_fetcher_options) do
|
72
83
|
cache_misses = 1
|
73
|
-
instrument_duration(payload, :resolve_miss_time)
|
74
|
-
yield
|
75
|
-
end
|
84
|
+
instrument_duration(payload, :resolve_miss_time, &block)
|
76
85
|
end
|
77
86
|
end
|
78
87
|
set_instrumentation_payload(payload, num_keys: 1, memo_misses: memo_misses, cache_misses: cache_misses)
|
@@ -83,7 +92,7 @@ module IdentityCache
|
|
83
92
|
IdentityCache.logger.debug { "[IdentityCache] cache miss for #{key}" }
|
84
93
|
else
|
85
94
|
IdentityCache.logger.debug do
|
86
|
-
"[IdentityCache] #{memo_misses > 0 ?
|
95
|
+
"[IdentityCache] #{memo_misses > 0 ? "(cache_backend)" : "(memoized)"} cache hit for #{key}"
|
87
96
|
end
|
88
97
|
end
|
89
98
|
|
@@ -94,7 +103,7 @@ module IdentityCache
|
|
94
103
|
memo_miss_keys = EMPTY_ARRAY
|
95
104
|
cache_miss_keys = EMPTY_ARRAY
|
96
105
|
|
97
|
-
result = ActiveSupport::Notifications.instrument(
|
106
|
+
result = ActiveSupport::Notifications.instrument("cache_fetch_multi.identity_cache") do |payload|
|
98
107
|
payload[:resolve_miss_time] = 0.0
|
99
108
|
|
100
109
|
result = fetch_multi_memoized(keys) do |non_memoized_keys|
|
@@ -122,7 +131,7 @@ module IdentityCache
|
|
122
131
|
end
|
123
132
|
|
124
133
|
def clear
|
125
|
-
ActiveSupport::Notifications.instrument(
|
134
|
+
ActiveSupport::Notifications.instrument("cache_clear.identity_cache") do
|
126
135
|
clear_memoization
|
127
136
|
@cache_fetcher.clear
|
128
137
|
end
|
@@ -145,6 +154,7 @@ module IdentityCache
|
|
145
154
|
if memoized_key_values.key?(key)
|
146
155
|
return memoized_key_values[key]
|
147
156
|
end
|
157
|
+
|
148
158
|
memoized_key_values[key] = yield
|
149
159
|
end
|
150
160
|
|
@@ -185,15 +195,15 @@ module IdentityCache
|
|
185
195
|
end
|
186
196
|
|
187
197
|
def log_multi_result(keys, memo_miss_keys, cache_miss_keys)
|
188
|
-
IdentityCache.logger.debug
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
198
|
+
return unless IdentityCache.logger.debug?
|
199
|
+
|
200
|
+
memoized_keys = keys - memo_miss_keys
|
201
|
+
cache_hit_keys = memo_miss_keys - cache_miss_keys
|
202
|
+
missed_keys = cache_miss_keys
|
203
|
+
|
204
|
+
memoized_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (memoized) cache hit for #{k} (multi)") }
|
205
|
+
cache_hit_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (backend) cache hit for #{k} (multi)") }
|
206
|
+
missed_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] cache miss for #{k} (multi)") }
|
197
207
|
end
|
198
208
|
end
|
199
209
|
end
|
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module IdentityCache
|
3
4
|
module ParentModelExpiration # :nodoc:
|
4
5
|
extend ActiveSupport::Concern
|
6
|
+
include ArTransactionChanges
|
5
7
|
|
6
8
|
class << self
|
7
9
|
def add_parent_expiry_hook(cached_association)
|
@@ -20,6 +22,8 @@ module IdentityCache
|
|
20
22
|
end
|
21
23
|
|
22
24
|
def install_pending_parent_expiry_hooks(model)
|
25
|
+
return if lazy_hooks.empty?
|
26
|
+
|
23
27
|
name = model.name.demodulize
|
24
28
|
if (hooks = lazy_hooks.delete(name))
|
25
29
|
hooks.each(&:install)
|
@@ -36,11 +40,9 @@ module IdentityCache
|
|
36
40
|
included do
|
37
41
|
class_attribute(:parent_expiration_entries)
|
38
42
|
self.parent_expiration_entries = Hash.new { |hash, key| hash[key] = [] }
|
39
|
-
after_commit(:expire_parent_caches)
|
40
43
|
end
|
41
44
|
|
42
45
|
def expire_parent_caches
|
43
|
-
ParentModelExpiration.install_pending_parent_expiry_hooks(cached_model)
|
44
46
|
parents_to_expire = Set.new
|
45
47
|
add_parents_to_cache_expiry_set(parents_to_expire)
|
46
48
|
parents_to_expire.each do |parent|
|
@@ -49,6 +51,7 @@ module IdentityCache
|
|
49
51
|
end
|
50
52
|
|
51
53
|
def add_parents_to_cache_expiry_set(parents_to_expire)
|
54
|
+
ParentModelExpiration.install_pending_parent_expiry_hooks(cached_model)
|
52
55
|
self.class.parent_expiration_entries.each do |association_name, cached_associations|
|
53
56
|
parents_to_expire_on_changes(parents_to_expire, association_name, cached_associations)
|
54
57
|
end
|
@@ -84,7 +87,7 @@ module IdentityCache
|
|
84
87
|
|
85
88
|
cached_associations.each do |parent_class, only_on_foreign_key_change|
|
86
89
|
if new_parent&.is_a?(parent_class) &&
|
87
|
-
|
90
|
+
should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
|
88
91
|
add_record_to_cache_expiry_set(parents_to_expire, new_parent)
|
89
92
|
end
|
90
93
|
|
@@ -1,12 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module IdentityCache
|
3
4
|
module QueryAPI
|
4
5
|
extend ActiveSupport::Concern
|
5
6
|
|
6
|
-
included do |base|
|
7
|
-
base.after_commit(:expire_cache)
|
8
|
-
end
|
9
|
-
|
10
7
|
module ClassMethods
|
11
8
|
# Prefetches cached associations on a collection of records
|
12
9
|
def prefetch_associations(includes, records)
|
@@ -18,6 +15,11 @@ module IdentityCache
|
|
18
15
|
cached_has_manys[name] || cached_has_ones[name] || cached_belongs_tos.fetch(name)
|
19
16
|
end
|
20
17
|
|
18
|
+
# @api private
|
19
|
+
def all_cached_associations # :nodoc:
|
20
|
+
cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
|
21
|
+
end
|
22
|
+
|
21
23
|
private
|
22
24
|
|
23
25
|
def raise_if_scoped
|
@@ -67,6 +69,7 @@ module IdentityCache
|
|
67
69
|
def setup_embedded_associations_on_miss(records,
|
68
70
|
readonly: IdentityCache.fetch_read_only_records && should_use_cache?)
|
69
71
|
return if records.empty?
|
72
|
+
|
70
73
|
records.each(&:readonly!) if readonly
|
71
74
|
each_id_embedded_association do |cached_association|
|
72
75
|
preload_id_embedded_association(records, cached_association)
|
@@ -81,10 +84,11 @@ module IdentityCache
|
|
81
84
|
association = record.association(association_name)
|
82
85
|
target = association.target
|
83
86
|
target = readonly_copy(target) if readonly
|
84
|
-
|
87
|
+
cached_association.set_with_inverse(record, target)
|
85
88
|
association.reset
|
86
89
|
# reset inverse associations
|
87
90
|
next unless target && association_reflection.has_inverse?
|
91
|
+
|
88
92
|
inverse_name = association_reflection.inverse_of.name
|
89
93
|
if target.is_a?(Array)
|
90
94
|
target.each { |child_record| child_record.association(inverse_name).reset }
|
@@ -126,10 +130,6 @@ module IdentityCache
|
|
126
130
|
all_cached_associations.select { |_name, association| association.embedded_recursively? }
|
127
131
|
end
|
128
132
|
|
129
|
-
def all_cached_associations
|
130
|
-
cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
|
131
|
-
end
|
132
|
-
|
133
133
|
def embedded_associations
|
134
134
|
all_cached_associations.select { |_name, association| association.embedded? }
|
135
135
|
end
|
@@ -151,6 +151,26 @@ module IdentityCache
|
|
151
151
|
end
|
152
152
|
end
|
153
153
|
|
154
|
+
no_op_callback = proc {}
|
155
|
+
included do |base|
|
156
|
+
# Make sure there is at least once after_commit callback so that _run_commit_callbacks
|
157
|
+
# is called, which is overridden to do an early after_commit callback
|
158
|
+
base.after_commit(&no_op_callback)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Override the method that is used to call after_commit callbacks so that we can
|
162
|
+
# expire the caches before other after_commit callbacks. This way we can avoid stale
|
163
|
+
# cache reads that happen from the ordering of callbacks. For example, if an after_commit
|
164
|
+
# callback enqueues a background job, then we don't want it to be possible for the
|
165
|
+
# background job to run and load data from the cache before it is invalidated.
|
166
|
+
def _run_commit_callbacks
|
167
|
+
if destroyed? || transaction_changed_attributes.present?
|
168
|
+
expire_cache
|
169
|
+
expire_parent_caches
|
170
|
+
end
|
171
|
+
super
|
172
|
+
end
|
173
|
+
|
154
174
|
# Invalidate the cache data associated with the record.
|
155
175
|
def expire_cache
|
156
176
|
expire_attribute_indexes
|
@@ -165,63 +185,6 @@ module IdentityCache
|
|
165
185
|
|
166
186
|
private
|
167
187
|
|
168
|
-
def fetch_recursively_cached_association(ivar_name, dehydrated_ivar_name, association_name) # :nodoc:
|
169
|
-
assoc = association(association_name)
|
170
|
-
|
171
|
-
if assoc.klass.should_use_cache? && !assoc.loaded?
|
172
|
-
if instance_variable_defined?(ivar_name)
|
173
|
-
instance_variable_get(ivar_name)
|
174
|
-
elsif instance_variable_defined?(dehydrated_ivar_name)
|
175
|
-
associated_records = hydrate_association_target(assoc.klass, instance_variable_get(dehydrated_ivar_name))
|
176
|
-
set_embedded_association(association_name, associated_records)
|
177
|
-
remove_instance_variable(dehydrated_ivar_name)
|
178
|
-
instance_variable_set(ivar_name, associated_records)
|
179
|
-
else
|
180
|
-
assoc.load_target
|
181
|
-
end
|
182
|
-
else
|
183
|
-
assoc.load_target
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
def hydrate_association_target(associated_class, dehydrated_value) # :nodoc:
|
188
|
-
dehydrated_value = IdentityCache.unmap_cached_nil_for(dehydrated_value)
|
189
|
-
if dehydrated_value.is_a?(Array)
|
190
|
-
dehydrated_value.map { |coder| Encoder.decode(coder, associated_class) }
|
191
|
-
else
|
192
|
-
Encoder.decode(dehydrated_value, associated_class)
|
193
|
-
end
|
194
|
-
end
|
195
|
-
|
196
|
-
def set_embedded_association(association_name, association_target) #:nodoc:
|
197
|
-
model = self.class
|
198
|
-
cached_association = model.cached_association(association_name)
|
199
|
-
|
200
|
-
set_inverse_of_cached_association(cached_association, association_target)
|
201
|
-
|
202
|
-
instance_variable_set(cached_association.records_variable_name, association_target)
|
203
|
-
end
|
204
|
-
|
205
|
-
def set_inverse_of_cached_association(cached_association, association_target)
|
206
|
-
return if association_target.nil?
|
207
|
-
associated_class = cached_association.reflection.klass
|
208
|
-
inverse_name = cached_association.inverse_name
|
209
|
-
inverse_cached_association = associated_class.cached_belongs_tos[inverse_name]
|
210
|
-
return unless inverse_cached_association
|
211
|
-
|
212
|
-
if association_target.is_a?(Array)
|
213
|
-
association_target.each do |child_record|
|
214
|
-
child_record.instance_variable_set(
|
215
|
-
inverse_cached_association.records_variable_name, self
|
216
|
-
)
|
217
|
-
end
|
218
|
-
else
|
219
|
-
association_target.instance_variable_set(
|
220
|
-
inverse_cached_association.records_variable_name, self
|
221
|
-
)
|
222
|
-
end
|
223
|
-
end
|
224
|
-
|
225
188
|
def expire_attribute_indexes # :nodoc:
|
226
189
|
cache_indexes.each do |cached_attribute|
|
227
190
|
cached_attribute.expire(self)
|