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.
- checksums.yaml +7 -0
- data/.travis.yml +1 -0
- data/CHANGELOG +7 -0
- data/README.md +8 -0
- data/Rakefile +19 -0
- data/identity_cache.gemspec +2 -1
- data/lib/{belongs_to_caching.rb → identity_cache/belongs_to_caching.rb} +12 -8
- data/lib/identity_cache/cache_key_generation.rb +58 -0
- data/lib/identity_cache/configuration_dsl.rb +301 -0
- data/lib/identity_cache/memoized_cache_proxy.rb +118 -0
- data/lib/identity_cache/parent_model_expiration.rb +34 -0
- data/lib/identity_cache/query_api.rb +312 -0
- data/lib/identity_cache/version.rb +1 -1
- data/lib/identity_cache.rb +35 -631
- data/performance/cache_runner.rb +123 -0
- data/performance/cpu.rb +28 -0
- data/performance/externals.rb +45 -0
- data/performance/profile.rb +26 -0
- data/test/attribute_cache_test.rb +3 -3
- data/test/fetch_multi_test.rb +13 -39
- data/test/fetch_multi_with_batched_associations_test.rb +236 -0
- data/test/fetch_test.rb +1 -1
- data/test/helpers/active_record_objects.rb +43 -0
- data/test/helpers/cache.rb +3 -12
- data/test/helpers/database_connection.rb +2 -1
- data/test/index_cache_test.rb +7 -0
- data/test/memoized_cache_proxy_test.rb +46 -1
- data/test/normalized_has_many_test.rb +13 -0
- data/test/recursive_denormalized_has_many_test.rb +17 -2
- data/test/save_test.rb +2 -2
- data/test/schema_change_test.rb +8 -28
- data/test/test_helper.rb +49 -43
- metadata +76 -76
- data/lib/memoized_cache_proxy.rb +0 -71
@@ -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
|