identity_cache 0.4.1 → 1.1.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 (94) hide show
  1. checksums.yaml +5 -5
  2. data/.github/probots.yml +2 -0
  3. data/.github/workflows/ci.yml +92 -0
  4. data/.gitignore +2 -0
  5. data/.rubocop.yml +5 -0
  6. data/CAVEATS.md +25 -0
  7. data/CHANGELOG.md +73 -19
  8. data/Gemfile +5 -1
  9. data/LICENSE +1 -1
  10. data/README.md +49 -27
  11. data/Rakefile +14 -5
  12. data/dev.yml +12 -16
  13. data/gemfiles/Gemfile.latest-release +8 -0
  14. data/gemfiles/Gemfile.min-supported +7 -0
  15. data/gemfiles/Gemfile.rails-edge +7 -0
  16. data/identity_cache.gemspec +29 -10
  17. data/lib/identity_cache.rb +78 -51
  18. data/lib/identity_cache/belongs_to_caching.rb +12 -40
  19. data/lib/identity_cache/cache_fetcher.rb +6 -5
  20. data/lib/identity_cache/cache_hash.rb +2 -2
  21. data/lib/identity_cache/cache_invalidation.rb +4 -11
  22. data/lib/identity_cache/cache_key_generation.rb +17 -65
  23. data/lib/identity_cache/cache_key_loader.rb +128 -0
  24. data/lib/identity_cache/cached.rb +7 -0
  25. data/lib/identity_cache/cached/association.rb +87 -0
  26. data/lib/identity_cache/cached/attribute.rb +123 -0
  27. data/lib/identity_cache/cached/attribute_by_multi.rb +37 -0
  28. data/lib/identity_cache/cached/attribute_by_one.rb +88 -0
  29. data/lib/identity_cache/cached/belongs_to.rb +100 -0
  30. data/lib/identity_cache/cached/embedded_fetching.rb +41 -0
  31. data/lib/identity_cache/cached/prefetcher.rb +61 -0
  32. data/lib/identity_cache/cached/primary_index.rb +96 -0
  33. data/lib/identity_cache/cached/recursive/association.rb +109 -0
  34. data/lib/identity_cache/cached/recursive/has_many.rb +9 -0
  35. data/lib/identity_cache/cached/recursive/has_one.rb +9 -0
  36. data/lib/identity_cache/cached/reference/association.rb +16 -0
  37. data/lib/identity_cache/cached/reference/has_many.rb +105 -0
  38. data/lib/identity_cache/cached/reference/has_one.rb +100 -0
  39. data/lib/identity_cache/configuration_dsl.rb +53 -215
  40. data/lib/identity_cache/encoder.rb +95 -0
  41. data/lib/identity_cache/expiry_hook.rb +36 -0
  42. data/lib/identity_cache/fallback_fetcher.rb +2 -1
  43. data/lib/identity_cache/load_strategy/eager.rb +28 -0
  44. data/lib/identity_cache/load_strategy/lazy.rb +71 -0
  45. data/lib/identity_cache/load_strategy/load_request.rb +20 -0
  46. data/lib/identity_cache/load_strategy/multi_load_request.rb +27 -0
  47. data/lib/identity_cache/mem_cache_store_cas.rb +53 -0
  48. data/lib/identity_cache/memoized_cache_proxy.rb +137 -58
  49. data/lib/identity_cache/parent_model_expiration.rb +46 -11
  50. data/lib/identity_cache/query_api.rb +102 -408
  51. data/lib/identity_cache/railtie.rb +8 -0
  52. data/lib/identity_cache/record_not_found.rb +6 -0
  53. data/lib/identity_cache/should_use_cache.rb +1 -0
  54. data/lib/identity_cache/version.rb +3 -2
  55. data/lib/identity_cache/with_primary_index.rb +136 -0
  56. data/lib/identity_cache/without_primary_index.rb +24 -3
  57. data/performance/cache_runner.rb +25 -73
  58. data/performance/cpu.rb +4 -3
  59. data/performance/externals.rb +4 -3
  60. data/performance/profile.rb +6 -5
  61. data/railgun.yml +16 -0
  62. metadata +60 -73
  63. data/.travis.yml +0 -30
  64. data/Gemfile.rails42 +0 -6
  65. data/Gemfile.rails50 +0 -6
  66. data/test/attribute_cache_test.rb +0 -110
  67. data/test/cache_fetch_includes_test.rb +0 -46
  68. data/test/cache_hash_test.rb +0 -14
  69. data/test/cache_invalidation_test.rb +0 -139
  70. data/test/deeply_nested_associated_record_test.rb +0 -19
  71. data/test/denormalized_has_many_test.rb +0 -211
  72. data/test/denormalized_has_one_test.rb +0 -160
  73. data/test/fetch_multi_test.rb +0 -308
  74. data/test/fetch_test.rb +0 -258
  75. data/test/fixtures/serialized_record.mysql2 +0 -0
  76. data/test/fixtures/serialized_record.postgresql +0 -0
  77. data/test/helpers/active_record_objects.rb +0 -106
  78. data/test/helpers/database_connection.rb +0 -72
  79. data/test/helpers/serialization_format.rb +0 -42
  80. data/test/helpers/update_serialization_format.rb +0 -24
  81. data/test/identity_cache_test.rb +0 -29
  82. data/test/index_cache_test.rb +0 -161
  83. data/test/memoized_attributes_test.rb +0 -49
  84. data/test/memoized_cache_proxy_test.rb +0 -107
  85. data/test/normalized_belongs_to_test.rb +0 -107
  86. data/test/normalized_has_many_test.rb +0 -231
  87. data/test/normalized_has_one_test.rb +0 -9
  88. data/test/prefetch_associations_test.rb +0 -364
  89. data/test/readonly_test.rb +0 -109
  90. data/test/recursive_denormalized_has_many_test.rb +0 -131
  91. data/test/save_test.rb +0 -82
  92. data/test/schema_change_test.rb +0 -112
  93. data/test/serialization_format_change_test.rb +0 -16
  94. data/test/test_helper.rb +0 -140
@@ -1,30 +1,62 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  module ParentModelExpiration # :nodoc:
3
4
  extend ActiveSupport::Concern
5
+ include ArTransactionChanges
4
6
 
5
- included do |base|
6
- base.class_attribute :parent_expiration_entries
7
- base.parent_expiration_entries = Hash.new{ |hash, key| hash[key] = [] }
7
+ class << self
8
+ def add_parent_expiry_hook(cached_association)
9
+ name = cached_association.reflection.class_name.demodulize
10
+ lazy_hooks[name] << ExpiryHook.new(cached_association)
11
+ end
12
+
13
+ def install_all_pending_parent_expiry_hooks
14
+ until lazy_hooks.empty?
15
+ lazy_hooks.keys.each do |name|
16
+ if (hooks = lazy_hooks.delete(name))
17
+ hooks.each(&:install)
18
+ end
19
+ end
20
+ end
21
+ end
22
+
23
+ def install_pending_parent_expiry_hooks(model)
24
+ return if lazy_hooks.empty?
25
+ name = model.name.demodulize
26
+ if (hooks = lazy_hooks.delete(name))
27
+ hooks.each(&:install)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def lazy_hooks
34
+ @lazy_hooks ||= Hash.new { |hash, key| hash[key] = [] }
35
+ end
36
+ end
37
+
38
+ included do
39
+ class_attribute(:parent_expiration_entries)
40
+ self.parent_expiration_entries = Hash.new { |hash, key| hash[key] = [] }
8
41
  end
9
42
 
10
43
  def expire_parent_caches
11
- parents_to_expire = {}
44
+ parents_to_expire = Set.new
12
45
  add_parents_to_cache_expiry_set(parents_to_expire)
13
- parents_to_expire.each_value do |parent|
14
- parent.send(:expire_primary_index)
46
+ parents_to_expire.each do |parent|
47
+ parent.expire_primary_index if parent.class.primary_cache_index_enabled
15
48
  end
16
49
  end
17
50
 
18
51
  def add_parents_to_cache_expiry_set(parents_to_expire)
52
+ ParentModelExpiration.install_pending_parent_expiry_hooks(cached_model)
19
53
  self.class.parent_expiration_entries.each do |association_name, cached_associations|
20
54
  parents_to_expire_on_changes(parents_to_expire, association_name, cached_associations)
21
55
  end
22
56
  end
23
57
 
24
58
  def add_record_to_cache_expiry_set(parents_to_expire, record)
25
- key = record.primary_cache_index_key
26
- unless parents_to_expire[key]
27
- parents_to_expire[key] = record
59
+ if parents_to_expire.add?(record)
28
60
  record.add_parents_to_cache_expiry_set(parents_to_expire)
29
61
  end
30
62
  end
@@ -52,11 +84,12 @@ module IdentityCache
52
84
  end
53
85
 
54
86
  cached_associations.each do |parent_class, only_on_foreign_key_change|
55
- if new_parent && new_parent.is_a?(parent_class) && should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
87
+ if new_parent&.is_a?(parent_class) &&
88
+ should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
56
89
  add_record_to_cache_expiry_set(parents_to_expire, new_parent)
57
90
  end
58
91
 
59
- if old_parent && old_parent.is_a?(parent_class)
92
+ if old_parent&.is_a?(parent_class)
60
93
  add_record_to_cache_expiry_set(parents_to_expire, old_parent)
61
94
  end
62
95
  end
@@ -70,4 +103,6 @@ module IdentityCache
70
103
  end
71
104
  end
72
105
  end
106
+
107
+ private_constant :ParentModelExpiration
73
108
  end
@@ -1,114 +1,30 @@
1
+ # frozen_string_literal: true
1
2
  module IdentityCache
2
3
  module QueryAPI
3
4
  extend ActiveSupport::Concern
4
5
 
5
- included do |base|
6
- base.after_commit :expire_cache
7
- end
8
-
9
6
  module ClassMethods
10
- # Similar to ActiveRecord::Base#exists? will return true if the id can be
11
- # found in the cache or in the DB.
12
- def exists_with_identity_cache?(id)
13
- raise NotImplementedError, "exists_with_identity_cache? needs the primary index enabled" unless primary_cache_index_enabled
14
- !!fetch_by_id(id)
15
- end
16
-
17
- # Default fetcher added to the model on inclusion, it behaves like
18
- # ActiveRecord::Base.where(id: id).first
19
- def fetch_by_id(id, options={})
20
- ensure_base_model
21
- raise_if_scoped
22
- raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
23
- return unless id
24
- record = if should_use_cache?
25
- require_if_necessary do
26
- object = nil
27
- coder = IdentityCache.fetch(rails_cache_key(id)){ coder_from_record(object = resolve_cache_miss(id)) }
28
- object ||= record_from_coder(coder)
29
- if object && object.id.to_s != id.to_s
30
- IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]}"
31
- end
32
- object
33
- end
34
- else
35
- resolve_cache_miss(id)
36
- end
37
- prefetch_associations(options[:includes], [record]) if record && options[:includes]
38
- record
39
- end
40
-
41
- # Default fetcher added to the model on inclusion, it behaves like
42
- # ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
43
- # if id is not in the cache or the db.
44
- def fetch(id, options={})
45
- fetch_by_id(id, options) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.name} with ID=#{id}")
7
+ # Prefetches cached associations on a collection of records
8
+ def prefetch_associations(includes, records)
9
+ Cached::Prefetcher.prefetch(self, includes, records)
46
10
  end
47
11
 
48
- # Default fetcher added to the model on inclusion, if behaves like
49
- # ActiveRecord::Base.find_all_by_id
50
- def fetch_multi(*ids)
51
- ensure_base_model
52
- raise_if_scoped
53
- raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
54
- options = ids.extract_options!
55
- ids.flatten!(1)
56
- records = if should_use_cache?
57
- require_if_necessary do
58
- cache_keys = ids.map {|id| rails_cache_key(id) }
59
- key_to_id_map = Hash[ cache_keys.zip(ids) ]
60
- key_to_record_map = {}
61
-
62
- coders_by_key = IdentityCache.fetch_multi(cache_keys) do |unresolved_keys|
63
- ids = unresolved_keys.map {|key| key_to_id_map[key] }
64
- records = find_batch(ids)
65
- key_to_record_map = records.compact.index_by{ |record| rails_cache_key(record.id) }
66
- records.map {|record| coder_from_record(record) }
67
- end
68
-
69
- cache_keys.map{ |key| key_to_record_map[key] || record_from_coder(coders_by_key[key]) }
70
- end
71
- else
72
- find_batch(ids)
73
- end
74
- records.compact!
75
- prefetch_associations(options[:includes], records) if options[:includes]
76
- records
12
+ # @api private
13
+ def cached_association(name) # :nodoc:
14
+ cached_has_manys[name] || cached_has_ones[name] || cached_belongs_tos.fetch(name)
77
15
  end
78
16
 
79
- def prefetch_associations(associations, records)
80
- records = records.to_a
81
- return if records.empty?
82
-
83
- case associations
84
- when nil
85
- # do nothing
86
- when Symbol
87
- prefetch_one_association(associations, records)
88
- when Array
89
- associations.each do |association|
90
- prefetch_associations(association, records)
91
- end
92
- when Hash
93
- associations.each do |association, sub_associations|
94
- next_level_records = prefetch_one_association(association, records)
95
-
96
- if sub_associations.present?
97
- associated_class = reflect_on_association(association).klass
98
- associated_class.prefetch_associations(sub_associations, next_level_records)
99
- end
100
- end
101
- else
102
- raise TypeError, "Invalid associations class #{associations.class}"
103
- end
104
- nil
17
+ # @api private
18
+ def all_cached_associations # :nodoc:
19
+ cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
105
20
  end
106
21
 
107
22
  private
108
23
 
109
24
  def raise_if_scoped
110
25
  if current_scope
111
- raise UnsupportedScopeError, "IdentityCache doesn't support rails scopes"
26
+ IdentityCache.logger.error("#{name} has scope: #{current_scope.to_sql} (#{current_scope.values.keys})")
27
+ raise UnsupportedScopeError, "IdentityCache doesn't support rails scopes (#{name})"
112
28
  end
113
29
  end
114
30
 
@@ -116,171 +32,107 @@ module IdentityCache
116
32
  association_reflection = reflect_on_association(association_name)
117
33
  scope = association_reflection.scope
118
34
  if scope && !association_reflection.klass.all.instance_exec(&scope).joins_values.empty?
119
- raise UnsupportedAssociationError, "caching association #{self}.#{association_name} scoped with a join isn't supported"
35
+ raise UnsupportedAssociationError, <<~MSG.squish
36
+ caching association #{self}.#{association_name}
37
+ scoped with a join isn't supported
38
+ MSG
120
39
  end
121
40
  end
122
41
 
123
- def record_from_coder(coder) #:nodoc:
124
- if coder
125
- klass = coder[:class]
126
- record = klass.instantiate(coder[:attributes].dup)
42
+ def preload_id_embedded_association(records, cached_association)
43
+ reflection = cached_association.reflection
44
+ child_model = reflection.klass
45
+ scope = child_model.all
46
+ scope = scope.where(reflection.type => base_class.name) if reflection.type
47
+ scope = scope.instance_exec(nil, &reflection.scope) if reflection.scope
127
48
 
128
- coder[:associations].each {|name, value| set_embedded_association(record, name, value) } if coder.has_key?(:associations)
129
- coder[:association_ids].each {|name, ids| record.instance_variable_set(:"@#{record.class.cached_has_manys[name][:ids_variable_name]}", ids) } if coder.has_key?(:association_ids)
130
- record.readonly! if IdentityCache.fetch_read_only_records
131
- record
49
+ pairs = scope.where(reflection.foreign_key => records.map(&:id)).pluck(
50
+ reflection.foreign_key, reflection.association_primary_key
51
+ )
52
+ ids_by_parent = Hash.new { |hash, key| hash[key] = [] }
53
+ pairs.each do |parent_id, child_id|
54
+ ids_by_parent[parent_id] << child_id
132
55
  end
133
- end
134
-
135
- def set_inverse_of_cached_has_many(record, association_reflection, child_records)
136
- associated_class = association_reflection.klass
137
- return unless associated_class < IdentityCache
138
-
139
- inverse_name = record.class.cached_has_manys.fetch(association_reflection.name).fetch(:inverse_name)
140
- inverse_cached_association = associated_class.cached_belongs_tos[inverse_name]
141
- return unless inverse_cached_association
142
-
143
- prepopulate_method_name = inverse_cached_association.fetch(:prepopulate_method_name)
144
- child_records.each { |child_record| child_record.send(prepopulate_method_name, record) }
145
- end
146
-
147
- def set_embedded_association(record, association_name, coder_or_array) #:nodoc:
148
- value = if IdentityCache.unmap_cached_nil_for(coder_or_array).nil?
149
- nil
150
- elsif (reflection = record.class.reflect_on_association(association_name)).collection?
151
- associated_records = coder_or_array.map {|e| record_from_coder(e) }
152
-
153
- set_inverse_of_cached_has_many(record, reflection, associated_records)
154
56
 
155
- unless IdentityCache.never_set_inverse_association
156
- association = reflection.association_class.new(record, reflection)
157
- association.target = associated_records
158
- association.target.each {|e| association.set_inverse_instance(e) }
57
+ records.each do |parent|
58
+ child_ids = ids_by_parent[parent.id]
59
+ case cached_association
60
+ when Cached::Reference::HasMany
61
+ parent.instance_variable_set(cached_association.ids_variable_name, child_ids)
62
+ when Cached::Reference::HasOne
63
+ parent.instance_variable_set(cached_association.id_variable_name, child_ids.first)
159
64
  end
160
-
161
- associated_records
162
- else
163
- record_from_coder(coder_or_array)
164
- end
165
- variable_name = record.class.send(:recursively_embedded_associations)[association_name][:records_variable_name]
166
- record.instance_variable_set(:"@#{variable_name}", value)
167
- end
168
-
169
- def get_embedded_association(record, association, options) #:nodoc:
170
- embedded_variable = record.public_send(options.fetch(:cached_accessor_name))
171
- if embedded_variable.respond_to?(:to_ary)
172
- embedded_variable.map {|e| coder_from_record(e) }
173
- else
174
- coder_from_record(embedded_variable)
175
65
  end
176
66
  end
177
67
 
178
- def coder_from_record(record) #:nodoc:
179
- unless record.nil?
180
- coder = {
181
- attributes: record.attributes_before_type_cast.dup,
182
- class: record.class,
183
- }
184
- add_cached_associations_to_coder(record, coder)
185
- coder
68
+ def setup_embedded_associations_on_miss(records,
69
+ readonly: IdentityCache.fetch_read_only_records && should_use_cache?)
70
+ return if records.empty?
71
+ records.each(&:readonly!) if readonly
72
+ each_id_embedded_association do |cached_association|
73
+ preload_id_embedded_association(records, cached_association)
186
74
  end
187
- end
75
+ recursively_embedded_associations.each_value do |cached_association|
76
+ association_reflection = cached_association.reflection
77
+ association_name = association_reflection.name
188
78
 
189
- def add_cached_associations_to_coder(record, coder)
190
- klass = record.class
191
- if klass.include?(IdentityCache)
192
- if (recursively_embedded_associations = klass.send(:recursively_embedded_associations)).present?
193
- coder[:associations] = recursively_embedded_associations.each_with_object({}) do |(name, options), hash|
194
- hash[name] = IdentityCache.map_cached_nil_for(get_embedded_association(record, name, options))
195
- end
196
- end
197
- if (cached_has_manys = klass.cached_has_manys).present?
198
- coder[:association_ids] = cached_has_manys.each_with_object({}) do |(name, options), hash|
199
- hash[name] = record.instance_variable_get(:"@#{options[:ids_variable_name]}") unless options[:embed] == true
79
+ # Move the loaded records to the cached association instance variable so they
80
+ # behave the same way if they were loaded from the cache
81
+ records.each do |record|
82
+ association = record.association(association_name)
83
+ target = association.target
84
+ target = readonly_copy(target) if readonly
85
+ cached_association.set_with_inverse(record, target)
86
+ association.reset
87
+ # reset inverse associations
88
+ next unless target && association_reflection.has_inverse?
89
+ inverse_name = association_reflection.inverse_of.name
90
+ if target.is_a?(Array)
91
+ target.each { |child_record| child_record.association(inverse_name).reset }
92
+ else
93
+ target.association(inverse_name).reset
200
94
  end
201
95
  end
202
- end
203
- end
204
96
 
205
- def require_if_necessary #:nodoc:
206
- # mem_cache_store returns raw value if unmarshal fails
207
- rval = yield
208
- case rval
209
- when String
210
- rval = Marshal.load(rval)
211
- when Array
212
- rval.map!{ |v| v.kind_of?(String) ? Marshal.load(v) : v }
97
+ child_model = association_reflection.klass
98
+ child_records = records.flat_map(&cached_association.cached_accessor_name).compact
99
+ child_model.send(:setup_embedded_associations_on_miss, child_records, readonly: readonly)
213
100
  end
214
- rval
215
- rescue ArgumentError => e
216
- if e.message =~ /undefined [\w\/]+ (\w+)/
217
- ok = Kernel.const_get($1) rescue nil
218
- retry if ok
219
- end
220
- raise
221
101
  end
222
102
 
223
- def resolve_cache_miss(id)
224
- record = self.includes(cache_fetch_includes).reorder(nil).where(primary_key => id).first
225
- if record
226
- preload_id_embedded_associations([record])
227
- record.readonly! if IdentityCache.fetch_read_only_records && should_use_cache?
228
- end
103
+ def readonly_record_copy(record)
104
+ record = record.clone
105
+ record.readonly!
229
106
  record
230
107
  end
231
108
 
232
- def preload_id_embedded_associations(records)
233
- return if records.empty?
234
- each_id_embedded_association do |options|
235
- reflection = options.fetch(:association_reflection)
236
- child_model = reflection.klass
237
- scope = child_model.all
238
- scope = scope.instance_exec(nil, &reflection.scope) if reflection.scope
239
-
240
- pairs = scope.where(reflection.foreign_key => records.map(&:id)).pluck(reflection.foreign_key, reflection.active_record_primary_key)
241
- ids_by_parent = Hash.new{ |hash, key| hash[key] = [] }
242
- pairs.each do |parent_id, child_id|
243
- ids_by_parent[parent_id] << child_id
244
- end
245
-
246
- records.each do |parent|
247
- child_ids = ids_by_parent[parent.id]
248
- parent.instance_variable_set(:"@#{options.fetch(:ids_variable_name)}", child_ids)
249
- end
250
- end
251
- recursively_embedded_associations.each_value do |options|
252
- child_model = options.fetch(:association_reflection).klass
253
- if child_model.include?(IdentityCache)
254
- child_records = records.flat_map(&options.fetch(:cached_accessor_name).to_sym).compact
255
- child_model.send(:preload_id_embedded_associations, child_records)
256
- end
109
+ def readonly_copy(record_or_records)
110
+ if record_or_records.is_a?(Array)
111
+ record_or_records.map { |record| readonly_record_copy(record) }
112
+ elsif record_or_records
113
+ readonly_record_copy(record_or_records)
257
114
  end
258
115
  end
259
116
 
260
117
  def each_id_embedded_association
261
- cached_has_manys.each_value do |options|
262
- yield options if options.fetch(:embed) == :ids
118
+ cached_has_manys.each_value do |association|
119
+ yield association if association.embedded_by_reference?
263
120
  end
264
- end
265
-
266
- def recursively_embedded_associations
267
- all_cached_associations.select do |cached_association, options|
268
- options[:embed] == true
121
+ cached_has_ones.each_value do |association|
122
+ yield association if association.embedded_by_reference?
269
123
  end
270
124
  end
271
125
 
272
- def all_cached_associations
273
- cached_has_manys.merge(cached_has_ones).merge(cached_belongs_tos)
126
+ def recursively_embedded_associations
127
+ all_cached_associations.select { |_name, association| association.embedded_recursively? }
274
128
  end
275
129
 
276
130
  def embedded_associations
277
- all_cached_associations.select do |cached_association, options|
278
- options[:embed]
279
- end
131
+ all_cached_associations.select { |_name, association| association.embedded? }
280
132
  end
281
133
 
282
134
  def cache_fetch_includes
283
- associations_for_identity_cache = recursively_embedded_associations.map do |child_association, options|
135
+ associations_for_identity_cache = recursively_embedded_associations.map do |child_association, _options|
284
136
  child_class = reflect_on_association(child_association).try(:klass)
285
137
 
286
138
  child_includes = child_class.send(:cache_fetch_includes)
@@ -294,203 +146,45 @@ module IdentityCache
294
146
 
295
147
  associations_for_identity_cache.compact
296
148
  end
297
-
298
- def find_batch(ids)
299
- return [] if ids.empty?
300
-
301
- @id_column ||= columns.detect {|c| c.name == primary_key}
302
- ids = ids.map{ |id| connection.type_cast(id, @id_column) }
303
- records = where(primary_key => ids).includes(cache_fetch_includes).to_a
304
- records.each(&:readonly!) if IdentityCache.fetch_read_only_records && should_use_cache?
305
- preload_id_embedded_associations(records)
306
- records_by_id = records.index_by(&:id)
307
- ids.map{ |id| records_by_id[id] }
308
- end
309
-
310
- def fetch_embedded_associations(records)
311
- associations = embedded_associations
312
- return if associations.empty?
313
-
314
- return unless primary_cache_index_enabled
315
-
316
- cached_records_by_id = fetch_multi(records.map(&:id)).index_by(&:id)
317
-
318
- associations.each_value do |options|
319
- records.each do |record|
320
- next unless cached_record = cached_records_by_id[record.id]
321
- if options[:embed] == :ids
322
- cached_association = cached_record.public_send(options.fetch(:cached_ids_name))
323
- record.instance_variable_set(:"@#{options.fetch(:ids_variable_name)}", cached_association)
324
- else
325
- cached_association = cached_record.public_send(options.fetch(:cached_accessor_name))
326
- record.instance_variable_set(:"@#{options.fetch(:records_variable_name)}", cached_association)
327
- end
328
- end
329
- end
330
- end
331
-
332
- def prefetch_embedded_association(records, association, details)
333
- # Make the same assumption as ActiveRecord::Associations::Preloader, which is
334
- # that all the records have the same associations loaded, so we can just check
335
- # the first record to see if an association is loaded.
336
- first_record = records.first
337
- return if first_record.association(association).loaded?
338
- iv_name_key = details[:embed] == true ? :records_variable_name : :ids_variable_name
339
- return if first_record.instance_variable_defined?(:"@#{details[iv_name_key]}")
340
- fetch_embedded_associations(records)
341
- end
342
-
343
- def prefetch_one_association(association, records)
344
- unless records.first.class.should_use_cache?
345
- ActiveRecord::Associations::Preloader.new.preload(records, association)
346
- return
347
- end
348
-
349
- case
350
- when details = cached_has_manys[association]
351
- prefetch_embedded_association(records, association, details)
352
- if details[:embed] == true
353
- child_records = records.flat_map(&details[:cached_accessor_name].to_sym)
354
- else
355
- ids_to_parent_record = records.each_with_object({}) do |record, hash|
356
- child_ids = record.send(details[:cached_ids_name])
357
- child_ids.each do |child_id|
358
- hash[child_id] = record
359
- end
360
- end
361
-
362
- parent_record_to_child_records = Hash.new { |h, k| h[k] = [] }
363
- child_records = details[:association_reflection].klass.fetch_multi(*ids_to_parent_record.keys)
364
- child_records.each do |child_record|
365
- parent_record = ids_to_parent_record[child_record.id]
366
- parent_record_to_child_records[parent_record] << child_record
367
- end
368
-
369
- parent_record_to_child_records.each do |parent, children|
370
- parent.send(details[:prepopulate_method_name], children)
371
- end
372
- end
373
-
374
- next_level_records = child_records
375
-
376
- when details = cached_belongs_tos[association]
377
- if details[:embed] == true
378
- raise ArgumentError.new("Embedded belongs_to associations do not support prefetching yet.")
379
- else
380
- reflection = details[:association_reflection]
381
- if reflection.polymorphic?
382
- raise ArgumentError.new("Polymorphic belongs_to associations do not support prefetching yet.")
383
- end
384
-
385
- cached_iv_name = :"@#{details.fetch(:records_variable_name)}"
386
- ids_to_child_record = records.each_with_object({}) do |child_record, hash|
387
- parent_id = child_record.send(reflection.foreign_key)
388
- if parent_id && !child_record.instance_variable_defined?(cached_iv_name)
389
- hash[parent_id] = child_record
390
- end
391
- end
392
- parent_records = reflection.klass.fetch_multi(ids_to_child_record.keys)
393
- parent_records.each do |parent_record|
394
- child_record = ids_to_child_record[parent_record.id]
395
- child_record.send(details[:prepopulate_method_name], parent_record)
396
- end
397
- end
398
-
399
- next_level_records = parent_records
400
-
401
- when details = cached_has_ones[association]
402
- if details[:embed] == true
403
- prefetch_embedded_association(records, association, details)
404
- parent_records = records.map(&details[:cached_accessor_name].to_sym)
405
- else
406
- raise ArgumentError.new("Non-embedded has_one associations do not support prefetching yet.")
407
- end
408
-
409
- next_level_records = parent_records
410
-
411
- else
412
- raise ArgumentError.new("Unknown cached association #{association} listed for prefetching")
413
- end
414
- next_level_records
415
- end
416
- end
417
-
418
- private
419
-
420
- def fetch_recursively_cached_association(ivar_name, association_name) # :nodoc:
421
- ivar_full_name = :"@#{ivar_name}"
422
- assoc = association(association_name)
423
-
424
- if assoc.klass.should_use_cache?
425
- if instance_variable_defined?(ivar_full_name)
426
- instance_variable_get(ivar_full_name)
427
- else
428
- cached_assoc = assoc.load_target
429
- if IdentityCache.fetch_read_only_records
430
- cached_assoc = readonly_copy(cached_assoc)
431
- end
432
- instance_variable_set(ivar_full_name, cached_assoc)
433
- end
434
- else
435
- assoc.load_target
436
- end
437
149
  end
438
150
 
439
- def expire_primary_index # :nodoc:
440
- return unless self.class.primary_cache_index_enabled
441
-
442
- IdentityCache.logger.debug do
443
- extra_keys =
444
- if respond_to?(:updated_at)
445
- old_updated_at = old_values_for_fields([:updated_at]).first
446
- "expiring_last_updated_at=#{old_updated_at}"
447
- else
448
- ""
449
- end
450
-
451
- "[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}"
452
- end
453
-
454
- IdentityCache.cache.delete(primary_cache_index_key)
151
+ no_op_callback = proc {}
152
+ included do |base|
153
+ # Make sure there is at least once after_commit callback so that _run_commit_callbacks
154
+ # is called, which is overridden to do an early after_commit callback
155
+ base.after_commit(&no_op_callback)
455
156
  end
456
157
 
457
- def expire_attribute_indexes # :nodoc:
458
- cache_indexes.each do |(attribute, fields, unique)|
459
- unless was_new_record?
460
- old_cache_attribute_key = attribute_cache_key_for_attribute_and_previous_values(attribute, fields, unique)
461
- IdentityCache.cache.delete(old_cache_attribute_key)
462
- end
463
- unless destroyed?
464
- new_cache_attribute_key = attribute_cache_key_for_attribute_and_current_values(attribute, fields, unique)
465
- if new_cache_attribute_key != old_cache_attribute_key
466
- IdentityCache.cache.delete(new_cache_attribute_key)
467
- end
468
- end
469
- end
158
+ # Override the method that is used to call after_commit callbacks so that we can
159
+ # expire the caches before other after_commit callbacks. This way we can avoid stale
160
+ # cache reads that happen from the ordering of callbacks. For example, if an after_commit
161
+ # callback enqueues a background job, then we don't want it to be possible for the
162
+ # background job to run and load data from the cache before it is invalidated.
163
+ def _run_commit_callbacks
164
+ if destroyed? || transaction_changed_attributes.present?
165
+ expire_cache
166
+ expire_parent_caches
167
+ end
168
+ super
470
169
  end
471
170
 
472
- def expire_cache # :nodoc:
473
- expire_primary_index
171
+ # Invalidate the cache data associated with the record.
172
+ def expire_cache
474
173
  expire_attribute_indexes
475
174
  true
476
175
  end
477
176
 
177
+ # @api private
478
178
  def was_new_record? # :nodoc:
479
179
  pk = self.class.primary_key
480
- !destroyed? && transaction_changed_attributes.has_key?(pk) && transaction_changed_attributes[pk].nil?
180
+ !destroyed? && transaction_changed_attributes.key?(pk) && transaction_changed_attributes[pk].nil?
481
181
  end
482
182
 
483
- def readonly_record_copy(record)
484
- record = record.clone
485
- record.readonly!
486
- record
487
- end
183
+ private
488
184
 
489
- def readonly_copy(record_or_records)
490
- if record_or_records.is_a?(Array)
491
- record_or_records.map { |record| readonly_record_copy(record) }
492
- elsif record_or_records
493
- readonly_record_copy(record_or_records)
185
+ def expire_attribute_indexes # :nodoc:
186
+ cache_indexes.each do |cached_attribute|
187
+ cached_attribute.expire(self)
494
188
  end
495
189
  end
496
190
  end