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
@@ -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)
|