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.
Files changed (61) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +76 -9
  3. data/.github/workflows/cla.yml +22 -0
  4. data/.gitignore +1 -0
  5. data/.rubocop.yml +7 -3
  6. data/.spin/bootstrap +7 -0
  7. data/.spin/svc.yml +2 -0
  8. data/CAVEATS.md +25 -0
  9. data/CHANGELOG.md +56 -22
  10. data/Gemfile +15 -5
  11. data/LICENSE +1 -1
  12. data/README.md +27 -8
  13. data/Rakefile +13 -12
  14. data/dev.yml +5 -4
  15. data/gemfiles/Gemfile.latest-release +12 -5
  16. data/gemfiles/Gemfile.min-supported +12 -0
  17. data/gemfiles/Gemfile.rails-edge +9 -5
  18. data/identity_cache.gemspec +15 -24
  19. data/{railgun.yml → isogun.yml} +0 -5
  20. data/lib/identity_cache/belongs_to_caching.rb +1 -0
  21. data/lib/identity_cache/cache_fetcher.rb +241 -16
  22. data/lib/identity_cache/cache_hash.rb +7 -6
  23. data/lib/identity_cache/cache_invalidation.rb +2 -1
  24. data/lib/identity_cache/cache_key_generation.rb +22 -19
  25. data/lib/identity_cache/cache_key_loader.rb +2 -2
  26. data/lib/identity_cache/cached/association.rb +2 -4
  27. data/lib/identity_cache/cached/attribute.rb +3 -3
  28. data/lib/identity_cache/cached/attribute_by_multi.rb +1 -1
  29. data/lib/identity_cache/cached/belongs_to.rb +24 -14
  30. data/lib/identity_cache/cached/embedded_fetching.rb +2 -0
  31. data/lib/identity_cache/cached/prefetcher.rb +12 -2
  32. data/lib/identity_cache/cached/primary_index.rb +3 -3
  33. data/lib/identity_cache/cached/recursive/association.rb +55 -12
  34. data/lib/identity_cache/cached/recursive/has_many.rb +1 -0
  35. data/lib/identity_cache/cached/recursive/has_one.rb +1 -0
  36. data/lib/identity_cache/cached/reference/association.rb +1 -0
  37. data/lib/identity_cache/cached/reference/has_many.rb +3 -2
  38. data/lib/identity_cache/cached/reference/has_one.rb +3 -2
  39. data/lib/identity_cache/cached.rb +1 -0
  40. data/lib/identity_cache/configuration_dsl.rb +1 -0
  41. data/lib/identity_cache/encoder.rb +2 -1
  42. data/lib/identity_cache/expiry_hook.rb +2 -1
  43. data/lib/identity_cache/fallback_fetcher.rb +6 -1
  44. data/lib/identity_cache/mem_cache_store_cas.rb +63 -0
  45. data/lib/identity_cache/memoized_cache_proxy.rb +33 -23
  46. data/lib/identity_cache/parent_model_expiration.rb +6 -3
  47. data/lib/identity_cache/query_api.rb +29 -66
  48. data/lib/identity_cache/railtie.rb +1 -0
  49. data/lib/identity_cache/should_use_cache.rb +1 -0
  50. data/lib/identity_cache/version.rb +2 -1
  51. data/lib/identity_cache/with_primary_index.rb +37 -10
  52. data/lib/identity_cache/without_primary_index.rb +7 -3
  53. data/lib/identity_cache.rb +66 -26
  54. data/performance/cache_runner.rb +12 -51
  55. data/performance/cpu.rb +7 -6
  56. data/performance/externals.rb +6 -5
  57. data/performance/profile.rb +7 -6
  58. metadata +32 -112
  59. data/.github/probots.yml +0 -2
  60. data/.travis.yml +0 -45
  61. 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
- reflection.active_record.class_eval(<<-RUBY, __FILE__, __LINE__ + 1)
15
- def #{cached_accessor_name}
16
- fetch_recursively_cached_association(
17
- :#{records_variable_name},
18
- :#{dehydrated_variable_name},
19
- :#{name}
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.public_send(cached_accessor_name)
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 write(record, records)
32
- record.instance_variable_set(records_variable_name, records)
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 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
@@ -20,8 +21,8 @@ module IdentityCache
20
21
  end
21
22
 
22
23
  def #{cached_accessor_name}
23
- association_klass = association(:#{name}).klass
24
- if association_klass.should_use_cache? && !#{name}.loaded?
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
- association_klass = association(:#{name}).klass
25
- if association_klass.should_use_cache? && !association(:#{name}).loaded?
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 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)
@@ -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
- require 'monitor'
3
- require 'benchmark'
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
- "which is needed for cache consistency")
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('cache_write.identity_cache', memoizing: memoizing) do
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('cache_delete.identity_cache', memoizing: memoizing) do
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('cache_fetch.identity_cache') do |payload|
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) do
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 ? '(cache_backend)' : '(memoized)'} cache hit for #{key}"
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('cache_fetch_multi.identity_cache') do |payload|
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('cache_clear.identity_cache') do
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 do
189
- memoized_keys = keys - memo_miss_keys
190
- cache_hit_keys = memo_miss_keys - cache_miss_keys
191
- missed_keys = cache_miss_keys
192
-
193
- memoized_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (memoized) cache hit for #{k} (multi)") }
194
- cache_hit_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] (backend) cache hit for #{k} (multi)") }
195
- missed_keys.each { |k| IdentityCache.logger.debug("[IdentityCache] cache miss for #{k} (multi)") }
196
- end
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
- should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
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
- record.send(:set_embedded_association, association_name, target)
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)
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  class Railtie < Rails::Railtie
4
5
  initializer "identity_cache.setup" do |app|
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
4
  module ShouldUseCache
4
5
  extend ActiveSupport::Concern
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module IdentityCache
3
- VERSION = "1.0.0"
4
+ VERSION = "1.2.0"
4
5
  CACHE_VERSION = 8
5
6
  end