identity_cache 0.4.1 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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