identity_cache 0.0.2 → 0.0.3

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