identity_cache 0.0.2 → 0.0.3

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.
@@ -0,0 +1,312 @@
1
+ module IdentityCache
2
+ module QueryAPI
3
+ extend ActiveSupport::Concern
4
+
5
+ included do |base|
6
+ base.private_class_method :require_if_necessary
7
+ base.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
8
+ private :expire_cache, :was_new_record?, :fetch_denormalized_cached_association,
9
+ :populate_denormalized_cached_association
10
+ CODE
11
+ base.after_commit :expire_cache
12
+ base.after_touch :expire_cache
13
+ end
14
+
15
+ module ClassMethods
16
+ # Similar to ActiveRecord::Base#exists? will return true if the id can be
17
+ # found in the cache or in the DB.
18
+ def exists_with_identity_cache?(id)
19
+ raise NotImplementedError, "exists_with_identity_cache? needs the primary index enabled" unless primary_cache_index_enabled
20
+ !!fetch_by_id(id)
21
+ end
22
+
23
+ # Default fetcher added to the model on inclusion, it behaves like
24
+ # ActiveRecord::Base.find_by_id
25
+ def fetch_by_id(id)
26
+ raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
27
+ if IdentityCache.should_cache?
28
+
29
+ require_if_necessary do
30
+ object = IdentityCache.fetch(rails_cache_key(id)){ resolve_cache_miss(id) }
31
+ IdentityCache.logger.error "[IDC id mismatch] fetch_by_id_requested=#{id} fetch_by_id_got=#{object.id} for #{object.inspect[(0..100)]} " if object && object.id != id.to_i
32
+ object
33
+ end
34
+
35
+ else
36
+ self.find_by_id(id)
37
+ end
38
+ end
39
+
40
+ # Default fetcher added to the model on inclusion, it behaves like
41
+ # ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
42
+ # if id is not in the cache or the db.
43
+ def fetch(id)
44
+ fetch_by_id(id) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.class.name} with ID=#{id}")
45
+ end
46
+
47
+ # Default fetcher added to the model on inclusion, if behaves like
48
+ # ActiveRecord::Base.find_all_by_id
49
+ def fetch_multi(*ids)
50
+ raise NotImplementedError, "fetching needs the primary index enabled" unless primary_cache_index_enabled
51
+ options = ids.extract_options!
52
+ if IdentityCache.should_cache?
53
+
54
+ require_if_necessary do
55
+ cache_keys = ids.map {|id| rails_cache_key(id) }
56
+ key_to_id_map = Hash[ cache_keys.zip(ids) ]
57
+
58
+ objects_by_key = IdentityCache.fetch_multi(*cache_keys) do |unresolved_keys|
59
+ ids = unresolved_keys.map {|key| key_to_id_map[key] }
60
+ records = find_batch(ids, options)
61
+ records.compact.each(&:populate_association_caches)
62
+ records
63
+ end
64
+
65
+ records = cache_keys.map {|key| objects_by_key[key] }.compact
66
+ prefetch_associations(options[:includes], records) if options[:includes]
67
+
68
+ records
69
+ end
70
+
71
+ else
72
+ find_batch(ids, options)
73
+ end
74
+ end
75
+
76
+ def require_if_necessary #:nodoc:
77
+ # mem_cache_store returns raw value if unmarshal fails
78
+ rval = yield
79
+ case rval
80
+ when String
81
+ rval = Marshal.load(rval)
82
+ when Array
83
+ rval.map!{ |v| v.kind_of?(String) ? Marshal.load(v) : v }
84
+ end
85
+ rval
86
+ rescue ArgumentError => e
87
+ if e.message =~ /undefined [\w\/]+ (\w+)/
88
+ ok = Kernel.const_get($1) rescue nil
89
+ retry if ok
90
+ end
91
+ raise
92
+ end
93
+
94
+ def resolve_cache_miss(id)
95
+ self.find_by_id(id, :include => cache_fetch_includes).tap do |object|
96
+ object.try(:populate_association_caches)
97
+ end
98
+ end
99
+
100
+ def all_cached_associations
101
+ (cached_has_manys || {}).merge(cached_has_ones || {}).merge(cached_belongs_tos || {})
102
+ end
103
+
104
+ def all_cached_associations_needing_population
105
+ all_cached_associations.select do |cached_association, options|
106
+ options[:population_method_name].present? # non-embedded belongs_to associations don't need population
107
+ end
108
+ end
109
+
110
+ def cache_fetch_includes(additions = {})
111
+ additions = hashify_includes_structure(additions)
112
+ embedded_associations = all_cached_associations.select { |name, options| options[:embed] }
113
+
114
+ associations_for_identity_cache = embedded_associations.map do |child_association, options|
115
+ child_class = reflect_on_association(child_association).try(:klass)
116
+
117
+ child_includes = additions.delete(child_association)
118
+
119
+ if child_class.respond_to?(:cache_fetch_includes)
120
+ child_includes = child_class.cache_fetch_includes(child_includes)
121
+ end
122
+
123
+ if child_includes.blank?
124
+ child_association
125
+ else
126
+ { child_association => child_includes }
127
+ end
128
+ end
129
+
130
+ associations_for_identity_cache.push(additions) if additions.keys.size > 0
131
+ associations_for_identity_cache.compact
132
+ end
133
+
134
+ def find_batch(ids, options = {})
135
+ @id_column ||= columns.detect {|c| c.name == "id"}
136
+ ids = ids.map{ |id| @id_column.type_cast(id) }
137
+ records = where('id IN (?)', ids).includes(cache_fetch_includes(options[:includes])).all
138
+ records_by_id = records.index_by(&:id)
139
+ records = ids.map{ |id| records_by_id[id] }
140
+ mismatching_ids = records.compact.map(&:id) - ids
141
+ IdentityCache.logger.error "[IDC id mismatch] fetch_batch_requested=#{ids.inspect} fetch_batch_got=#{mismatchig_ids.inspect} mismatching ids " unless mismatching_ids.empty?
142
+ records
143
+ end
144
+
145
+ def prefetch_associations(associations, records)
146
+ associations = hashify_includes_structure(associations)
147
+
148
+ associations.each do |association, sub_associations|
149
+ case
150
+ when details = cached_has_manys[association]
151
+
152
+ if details[:embed]
153
+ child_records = records.map(&details[:cached_accessor_name].to_sym).flatten
154
+ else
155
+ ids_to_parent_record = records.each_with_object({}) do |record, hash|
156
+ child_ids = record.send(details[:cached_ids_name])
157
+ child_ids.each do |child_id|
158
+ hash[child_id] = record
159
+ end
160
+ end
161
+
162
+ parent_record_to_child_records = Hash.new { |h, k| h[k] = [] }
163
+ child_records = details[:association_class].fetch_multi(*ids_to_parent_record.keys)
164
+ child_records.each do |child_record|
165
+ parent_record = ids_to_parent_record[child_record.id]
166
+ parent_record_to_child_records[parent_record] << child_record
167
+ end
168
+
169
+ parent_record_to_child_records.each do |parent_record, child_records|
170
+ parent_record.send(details[:prepopulate_method_name], child_records)
171
+ end
172
+ end
173
+
174
+ next_level_records = child_records
175
+
176
+ when details = cached_belongs_tos[association]
177
+ if details[:embed]
178
+ raise ArgumentError.new("Embedded belongs_to associations do not support prefetching yet.")
179
+ else
180
+ ids_to_child_record = records.each_with_object({}) do |child_record, hash|
181
+ parent_id = child_record.send(details[:foreign_key])
182
+ hash[parent_id] = child_record if parent_id.present?
183
+ end
184
+ parent_records = details[:association_class].fetch_multi(*ids_to_child_record.keys)
185
+ parent_records.each do |parent_record|
186
+ child_record = ids_to_child_record[parent_record.id]
187
+ child_record.send(details[:prepopulate_method_name], parent_record)
188
+ end
189
+ end
190
+
191
+ next_level_records = parent_records
192
+
193
+ when details = cached_has_ones[association]
194
+ if details[:embed]
195
+ parent_records = records.map(&details[:cached_accessor_name].to_sym)
196
+ else
197
+ raise ArgumentError.new("Non-embedded has_one associations do not support prefetching yet.")
198
+ end
199
+
200
+ next_level_records = parent_records
201
+
202
+ else
203
+ raise ArgumentError.new("Unknown cached association #{association} listed for prefetching")
204
+ end
205
+
206
+ if details && details[:association_class].respond_to?(:prefetch_associations)
207
+ details[:association_class].prefetch_associations(sub_associations, next_level_records)
208
+ end
209
+ end
210
+ end
211
+
212
+ def hashify_includes_structure(structure)
213
+ case structure
214
+ when nil
215
+ {}
216
+ when Symbol
217
+ {structure => []}
218
+ when Hash
219
+ structure.clone
220
+ when Array
221
+ structure.each_with_object({}) do |member, hash|
222
+ case member
223
+ when Hash
224
+ hash.merge!(member)
225
+ when Symbol
226
+ hash[member] = []
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
232
+
233
+ def populate_association_caches # :nodoc:
234
+ self.class.all_cached_associations_needing_population.each do |cached_association, options|
235
+ send(options[:population_method_name])
236
+ reflection = options[:embed] && self.class.reflect_on_association(cached_association)
237
+ if reflection && reflection.klass.respond_to?(:cached_has_manys)
238
+ child_objects = Array.wrap(send(options[:cached_accessor_name]))
239
+ child_objects.each(&:populate_association_caches)
240
+ end
241
+ end
242
+ end
243
+
244
+ def fetch_denormalized_cached_association(ivar_name, association_name) # :nodoc:
245
+ ivar_full_name = :"@#{ivar_name}"
246
+ if IdentityCache.should_cache?
247
+ populate_denormalized_cached_association(ivar_name, association_name)
248
+ IdentityCache.unmap_cached_nil_for(instance_variable_get(ivar_full_name))
249
+ else
250
+ send(association_name.to_sym)
251
+ end
252
+ end
253
+
254
+ def populate_denormalized_cached_association(ivar_name, association_name) # :nodoc:
255
+ ivar_full_name = :"@#{ivar_name}"
256
+
257
+ value = instance_variable_get(ivar_full_name)
258
+ return value unless value.nil?
259
+
260
+ loaded_association = send(association_name)
261
+
262
+ instance_variable_set(ivar_full_name, IdentityCache.map_cached_nil_for(loaded_association))
263
+ end
264
+
265
+ def expire_primary_index # :nodoc:
266
+ return unless self.class.primary_cache_index_enabled
267
+ extra_keys = if respond_to? :updated_at
268
+ old_updated_at = old_values_for_fields([:updated_at]).first
269
+ "expiring_last_updated_at=#{old_updated_at}"
270
+ else
271
+ ""
272
+ end
273
+ IdentityCache.logger.debug { "[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}" }
274
+
275
+ IdentityCache.cache.delete(primary_cache_index_key)
276
+ end
277
+
278
+ def expire_secondary_indexes # :nodoc:
279
+ return unless self.class.primary_cache_index_enabled
280
+ cache_indexes.try(:each) do |fields|
281
+ if self.destroyed?
282
+ IdentityCache.cache.delete(secondary_cache_index_key_for_previous_values(fields))
283
+ else
284
+ new_cache_index_key = secondary_cache_index_key_for_current_values(fields)
285
+ IdentityCache.cache.delete(new_cache_index_key)
286
+
287
+ if !was_new_record?
288
+ old_cache_index_key = secondary_cache_index_key_for_previous_values(fields)
289
+ IdentityCache.cache.delete(old_cache_index_key) unless old_cache_index_key == new_cache_index_key
290
+ end
291
+ end
292
+ end
293
+ end
294
+
295
+ def expire_attribute_indexes # :nodoc:
296
+ cache_attributes.try(:each) do |(attribute, fields)|
297
+ IdentityCache.cache.delete(attribute_cache_key_for_attribute_and_previous_values(attribute, fields)) unless was_new_record?
298
+ end
299
+ end
300
+
301
+ def expire_cache # :nodoc:
302
+ expire_primary_index
303
+ expire_secondary_indexes
304
+ expire_attribute_indexes
305
+ true
306
+ end
307
+
308
+ def was_new_record? # :nodoc:
309
+ !destroyed? && transaction_changed_attributes.has_key?('id') && transaction_changed_attributes['id'].nil?
310
+ end
311
+ end
312
+ end
@@ -1,3 +1,3 @@
1
1
  module IdentityCache
2
- VERSION = "0.0.2"
2
+ VERSION = "0.0.3"
3
3
  end