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.
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