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.
- 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
data/lib/identity_cache.rb
CHANGED
@@ -1,12 +1,23 @@
|
|
1
|
-
require "identity_cache/version"
|
2
1
|
require 'cityhash'
|
3
2
|
require 'ar_transaction_changes'
|
4
|
-
require
|
5
|
-
require
|
3
|
+
require "identity_cache/version"
|
4
|
+
require 'identity_cache/memoized_cache_proxy'
|
5
|
+
require 'identity_cache/belongs_to_caching'
|
6
|
+
require 'identity_cache/cache_key_generation'
|
7
|
+
require 'identity_cache/configuration_dsl'
|
8
|
+
require 'identity_cache/parent_model_expiration'
|
9
|
+
require 'identity_cache/query_api'
|
6
10
|
|
7
11
|
module IdentityCache
|
8
12
|
CACHED_NIL = :idc_cached_nil
|
9
13
|
|
14
|
+
class AlreadyIncludedError < StandardError; end
|
15
|
+
class InverseAssociationError < StandardError
|
16
|
+
def initialize
|
17
|
+
super "Inverse name for association could not be determined. Please use the :inverse_name option to specify the inverse association name for this cache."
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
10
21
|
class << self
|
11
22
|
|
12
23
|
attr_accessor :logger, :readonly
|
@@ -46,18 +57,13 @@ module IdentityCache
|
|
46
57
|
|
47
58
|
if result.nil?
|
48
59
|
if block_given?
|
49
|
-
|
50
|
-
result = yield
|
51
|
-
end
|
60
|
+
result = yield
|
52
61
|
result = map_cached_nil_for(result)
|
53
62
|
|
54
63
|
if should_cache?
|
55
64
|
cache.write(key, result)
|
56
65
|
end
|
57
66
|
end
|
58
|
-
logger.debug "[IdentityCache] cache miss for #{key}"
|
59
|
-
else
|
60
|
-
logger.debug "[IdentityCache] cache hit for #{key}"
|
61
67
|
end
|
62
68
|
|
63
69
|
unmap_cached_nil_for(result)
|
@@ -82,29 +88,25 @@ module IdentityCache
|
|
82
88
|
result = {}
|
83
89
|
result = cache.read_multi(*keys) if should_cache?
|
84
90
|
|
85
|
-
|
91
|
+
hit_keys = result.select {|key, value| value.present? }.keys
|
92
|
+
missed_keys = keys - hit_keys
|
86
93
|
|
87
94
|
if missed_keys.size > 0
|
88
95
|
if block_given?
|
89
96
|
replacement_results = nil
|
90
|
-
|
91
|
-
replacement_results = yield missed_keys
|
92
|
-
end
|
97
|
+
replacement_results = yield missed_keys
|
93
98
|
missed_keys.zip(replacement_results) do |(key, replacement_result)|
|
94
99
|
if should_cache?
|
95
100
|
replacement_result = map_cached_nil_for(replacement_result )
|
96
101
|
cache.write(key, replacement_result)
|
97
|
-
logger.debug "[IdentityCache] cache miss for #{key} (multi)"
|
102
|
+
logger.debug { "[IdentityCache] cache miss for #{key} (multi)" }
|
98
103
|
end
|
99
104
|
result[key] = replacement_result
|
100
105
|
end
|
101
106
|
end
|
102
|
-
else
|
103
|
-
result.keys.each do |key|
|
104
|
-
logger.debug "[IdentityCache] cache hit for #{key} (multi)"
|
105
|
-
end
|
106
107
|
end
|
107
108
|
|
109
|
+
|
108
110
|
result.keys.each do |key|
|
109
111
|
result[key] = unmap_cached_nil_for(result[key])
|
110
112
|
end
|
@@ -113,630 +115,32 @@ module IdentityCache
|
|
113
115
|
end
|
114
116
|
|
115
117
|
def schema_to_string(columns)
|
116
|
-
columns.sort_by(&:name).map
|
118
|
+
columns.sort_by(&:name).map{|c| "#{c.name}:#{c.type}"}.join(',')
|
119
|
+
end
|
120
|
+
|
121
|
+
def denormalized_schema_hash(klass)
|
122
|
+
schema_string = schema_to_string(klass.columns)
|
123
|
+
if klass.respond_to?(:all_cached_associations_needing_population) && !(embeded_associations = klass.all_cached_associations_needing_population).empty?
|
124
|
+
embedded_schema = embeded_associations.map do |name, options|
|
125
|
+
"#{name}:(#{denormalized_schema_hash(options[:association_class])})"
|
126
|
+
end.sort.join(',')
|
127
|
+
schema_string << "," << embedded_schema
|
128
|
+
end
|
129
|
+
IdentityCache.memcache_hash(schema_string)
|
117
130
|
end
|
118
131
|
|
119
132
|
def included(base) #:nodoc:
|
120
133
|
raise AlreadyIncludedError if base.respond_to? :cache_indexes
|
121
134
|
|
122
|
-
unless ActiveRecord::Base.connection.respond_to?(:with_master)
|
123
|
-
ActiveRecord::Base.connection.class.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
124
|
-
def with_master
|
125
|
-
yield
|
126
|
-
end
|
127
|
-
CODE
|
128
|
-
end
|
129
|
-
|
130
135
|
base.send(:include, ArTransactionChanges) unless base.include?(ArTransactionChanges)
|
131
136
|
base.send(:include, IdentityCache::BelongsToCaching)
|
132
|
-
base.
|
133
|
-
base.
|
134
|
-
base.
|
135
|
-
base.class_attribute :cache_attributes
|
136
|
-
base.class_attribute :cached_has_manys
|
137
|
-
base.class_attribute :cached_has_ones
|
138
|
-
base.class_attribute :embedded_schema_hashes
|
139
|
-
base.send(:extend, ClassMethods)
|
140
|
-
|
141
|
-
base.cached_has_manys = {}
|
142
|
-
base.cached_has_ones = {}
|
143
|
-
base.embedded_schema_hashes = {}
|
144
|
-
base.cache_attributes = []
|
145
|
-
base.cache_indexes = []
|
146
|
-
|
147
|
-
base.private_class_method :require_if_necessary, :build_normalized_has_many_cache, :build_denormalized_association_cache, :add_parent_expiry_hook,
|
148
|
-
:identity_cache_multiple_value_dynamic_fetcher, :identity_cache_single_value_dynamic_fetcher
|
149
|
-
|
150
|
-
|
151
|
-
base.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
152
|
-
private :expire_cache, :was_new_record?, :fetch_denormalized_cached_association, :populate_denormalized_cached_association
|
153
|
-
CODE
|
137
|
+
base.send(:include, IdentityCache::CacheKeyGeneration)
|
138
|
+
base.send(:include, IdentityCache::ConfigurationDSL)
|
139
|
+
base.send(:include, IdentityCache::QueryAPI)
|
154
140
|
end
|
155
141
|
|
156
142
|
def memcache_hash(key) #:nodoc:
|
157
143
|
CityHash.hash64(key)
|
158
144
|
end
|
159
145
|
end
|
160
|
-
|
161
|
-
module ClassMethods
|
162
|
-
|
163
|
-
# Declares a new index in the cache for the class where IdentityCache was
|
164
|
-
# included.
|
165
|
-
#
|
166
|
-
# IdentityCache will add a fetch_by_field1_and_field2_and_...field for every
|
167
|
-
# index.
|
168
|
-
#
|
169
|
-
# == Example:
|
170
|
-
#
|
171
|
-
# class Product
|
172
|
-
# include IdentityCache
|
173
|
-
# cache_index :name, :vendor
|
174
|
-
# end
|
175
|
-
#
|
176
|
-
# Will add Product.fetch_by_name_and_vendor
|
177
|
-
#
|
178
|
-
# == Parameters
|
179
|
-
#
|
180
|
-
# +fields+ Array of symbols or strings representing the fields in the index
|
181
|
-
#
|
182
|
-
# == Options
|
183
|
-
# * unique: if the index would only have unique values
|
184
|
-
#
|
185
|
-
def cache_index(*fields)
|
186
|
-
options = fields.extract_options!
|
187
|
-
self.cache_indexes.push fields
|
188
|
-
|
189
|
-
field_list = fields.join("_and_")
|
190
|
-
arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
|
191
|
-
where_list = fields.each_with_index.collect { |f, i| "#{f} = \#{quote_value(arg#{i})}" }.join(" AND ")
|
192
|
-
|
193
|
-
if options[:unique]
|
194
|
-
self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
195
|
-
def fetch_by_#{field_list}(#{arg_list})
|
196
|
-
sql = "SELECT id FROM #{table_name} WHERE #{where_list} LIMIT 1"
|
197
|
-
identity_cache_single_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}], sql)
|
198
|
-
end
|
199
|
-
|
200
|
-
# exception throwing variant
|
201
|
-
def fetch_by_#{field_list}!(#{arg_list})
|
202
|
-
fetch_by_#{field_list}(#{arg_list}) or raise ActiveRecord::RecordNotFound
|
203
|
-
end
|
204
|
-
CODE
|
205
|
-
else
|
206
|
-
self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
207
|
-
def fetch_by_#{field_list}(#{arg_list})
|
208
|
-
sql = "SELECT id FROM #{table_name} WHERE #{where_list}"
|
209
|
-
identity_cache_multiple_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}], sql)
|
210
|
-
end
|
211
|
-
CODE
|
212
|
-
end
|
213
|
-
end
|
214
|
-
|
215
|
-
def identity_cache_single_value_dynamic_fetcher(fields, values, sql_on_miss) # :nodoc:
|
216
|
-
cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
|
217
|
-
id = IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
|
218
|
-
unless id.nil?
|
219
|
-
record = fetch_by_id(id.to_i)
|
220
|
-
IdentityCache.cache.delete(cache_key) unless record
|
221
|
-
end
|
222
|
-
|
223
|
-
record
|
224
|
-
end
|
225
|
-
|
226
|
-
def identity_cache_multiple_value_dynamic_fetcher(fields, values, sql_on_miss) # :nodoc:
|
227
|
-
cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
|
228
|
-
ids = IdentityCache.fetch(cache_key) { connection.select_values(sql_on_miss) }
|
229
|
-
|
230
|
-
ids.empty? ? [] : fetch_multi(*ids)
|
231
|
-
end
|
232
|
-
|
233
|
-
|
234
|
-
# Will cache an association to the class including IdentityCache.
|
235
|
-
# The embed option, if set, will make IdentityCache keep the association
|
236
|
-
# values in the same cache entry as the parent.
|
237
|
-
#
|
238
|
-
# Embedded associations are more effective in offloading database work,
|
239
|
-
# however they will increase the size of the cache entries and make the
|
240
|
-
# whole entry expire when any of the embedded members change.
|
241
|
-
#
|
242
|
-
# == Example:
|
243
|
-
# class Product
|
244
|
-
# cached_has_many :options, :embed => false
|
245
|
-
# cached_has_many :orders
|
246
|
-
# cached_has_many :buyers, :inverse_name => 'line_item'
|
247
|
-
# end
|
248
|
-
#
|
249
|
-
# == Parameters
|
250
|
-
# +association+ Name of the association being cached as a symbol
|
251
|
-
#
|
252
|
-
# == Options
|
253
|
-
#
|
254
|
-
# * embed: If set will cause IdentityCache to keep the values for this
|
255
|
-
# association in the same cache entry as the parent, instead of its own.
|
256
|
-
# * inverse_name: The name of the parent in the association if the name is
|
257
|
-
# not the lowercase pluralization of the parent object's class
|
258
|
-
def cache_has_many(association, options = {})
|
259
|
-
options[:embed] ||= false
|
260
|
-
options[:inverse_name] ||= self.name.underscore.to_sym
|
261
|
-
raise InverseAssociationError unless self.reflect_on_association(association)
|
262
|
-
self.cached_has_manys[association] = options
|
263
|
-
|
264
|
-
if options[:embed]
|
265
|
-
build_denormalized_association_cache(association, options)
|
266
|
-
else
|
267
|
-
build_normalized_has_many_cache(association, options)
|
268
|
-
end
|
269
|
-
end
|
270
|
-
|
271
|
-
# Will cache an association to the class including IdentityCache.
|
272
|
-
# The embed option if set will make IdentityCache keep the association
|
273
|
-
# values in the same cache entry as the parent.
|
274
|
-
#
|
275
|
-
# Embedded associations are more effective in offloading database work,
|
276
|
-
# however they will increase the size of the cache entries and make the
|
277
|
-
# whole entry expire with the change of any of the embedded members
|
278
|
-
#
|
279
|
-
# == Example:
|
280
|
-
# class Product
|
281
|
-
# cached_has_one :store, :embed => false
|
282
|
-
# cached_has_one :vendor
|
283
|
-
# end
|
284
|
-
#
|
285
|
-
# == Parameters
|
286
|
-
# +association+ Symbol with the name of the association being cached
|
287
|
-
#
|
288
|
-
# == Options
|
289
|
-
#
|
290
|
-
# * embed: If set will cause IdentityCache to keep the values for this
|
291
|
-
# association in the same cache entry as the parent, instead of its own.
|
292
|
-
# * inverse_name: The name of the parent in the association ( only
|
293
|
-
# necessary if the name is not the lowercase pluralization of the
|
294
|
-
# parent object's class)
|
295
|
-
def cache_has_one(association, options = {})
|
296
|
-
options[:embed] ||= true
|
297
|
-
options[:inverse_name] ||= self.name.underscore.to_sym
|
298
|
-
raise InverseAssociationError unless self.reflect_on_association(association)
|
299
|
-
self.cached_has_ones[association] = options
|
300
|
-
|
301
|
-
build_denormalized_association_cache(association, options)
|
302
|
-
end
|
303
|
-
|
304
|
-
def build_denormalized_association_cache(association, options) #:nodoc:
|
305
|
-
options[:cached_accessor_name] ||= "fetch_#{association}"
|
306
|
-
options[:cache_variable_name] ||= "cached_#{association}"
|
307
|
-
options[:population_method_name] ||= "populate_#{association}_cache"
|
308
|
-
|
309
|
-
|
310
|
-
unless instance_methods.include?(options[:cached_accessor_name].to_sym)
|
311
|
-
self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
312
|
-
def #{options[:cached_accessor_name]}
|
313
|
-
fetch_denormalized_cached_association('#{options[:cache_variable_name]}', :#{association})
|
314
|
-
end
|
315
|
-
|
316
|
-
def #{options[:population_method_name]}
|
317
|
-
populate_denormalized_cached_association('#{options[:cache_variable_name]}', :#{association})
|
318
|
-
end
|
319
|
-
CODE
|
320
|
-
|
321
|
-
association_class = reflect_on_association(association).klass
|
322
|
-
add_parent_expiry_hook(association_class, options.merge(:only_on_foreign_key_change => false))
|
323
|
-
end
|
324
|
-
end
|
325
|
-
|
326
|
-
def build_normalized_has_many_cache(association, options) #:nodoc:
|
327
|
-
singular_association = association.to_s.singularize
|
328
|
-
association_class = reflect_on_association(association).klass
|
329
|
-
options[:cached_accessor_name] ||= "fetch_#{association}"
|
330
|
-
options[:ids_name] ||= "#{singular_association}_ids"
|
331
|
-
options[:ids_cache_name] ||= "cached_#{options[:ids_name]}"
|
332
|
-
options[:population_method_name] ||= "populate_#{association}_cache"
|
333
|
-
|
334
|
-
self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
335
|
-
attr_reader :#{options[:ids_cache_name]}
|
336
|
-
|
337
|
-
def #{options[:population_method_name]}
|
338
|
-
@#{options[:ids_cache_name]} = #{options[:ids_name]}
|
339
|
-
end
|
340
|
-
|
341
|
-
def #{options[:cached_accessor_name]}
|
342
|
-
if IdentityCache.should_cache? || #{association}.loaded?
|
343
|
-
populate_#{association}_cache unless @#{options[:ids_cache_name]}
|
344
|
-
@cached_#{association} ||= #{association_class}.fetch_multi(*@#{options[:ids_cache_name]})
|
345
|
-
else
|
346
|
-
#{association}
|
347
|
-
end
|
348
|
-
end
|
349
|
-
CODE
|
350
|
-
|
351
|
-
add_parent_expiry_hook(association_class, options.merge(:only_on_foreign_key_change => true))
|
352
|
-
end
|
353
|
-
|
354
|
-
|
355
|
-
# Will cache a single attribute on its own blob, it will add a
|
356
|
-
# fetch_attribute_by_id (or the value of the by option).
|
357
|
-
#
|
358
|
-
# == Example:
|
359
|
-
# class Product
|
360
|
-
# cache_attribute :quantity, :by => :name
|
361
|
-
# cache_attribute :quantity :by => [:name, :vendor]
|
362
|
-
# end
|
363
|
-
#
|
364
|
-
# == Parameters
|
365
|
-
# +attribute+ Symbol with the name of the attribute being cached
|
366
|
-
#
|
367
|
-
# == Options
|
368
|
-
#
|
369
|
-
# * by: Other attribute or attributes in the model to keep values indexed. Default is :id
|
370
|
-
def cache_attribute(attribute, options = {})
|
371
|
-
options[:by] ||= :id
|
372
|
-
fields = Array(options[:by])
|
373
|
-
|
374
|
-
self.cache_attributes.push [attribute, fields]
|
375
|
-
|
376
|
-
field_list = fields.join("_and_")
|
377
|
-
arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
|
378
|
-
where_list = fields.each_with_index.collect { |f, i| "#{f} = \#{quote_value(arg#{i})}" }.join(" AND ")
|
379
|
-
|
380
|
-
self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
381
|
-
def fetch_#{attribute}_by_#{field_list}(#{arg_list})
|
382
|
-
sql = "SELECT #{attribute} FROM #{table_name} WHERE #{where_list} LIMIT 1"
|
383
|
-
attribute_dynamic_fetcher(#{attribute.inspect}, #{fields.inspect}, [#{arg_list}], sql)
|
384
|
-
end
|
385
|
-
CODE
|
386
|
-
end
|
387
|
-
|
388
|
-
def attribute_dynamic_fetcher(attribute, fields, values, sql_on_miss) #:nodoc:
|
389
|
-
cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
|
390
|
-
IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
|
391
|
-
end
|
392
|
-
|
393
|
-
# Similar to ActiveRecord::Base#exists? will return true if the id can be
|
394
|
-
# found in the cache.
|
395
|
-
def exists_with_identity_cache?(id)
|
396
|
-
!!fetch_by_id(id)
|
397
|
-
end
|
398
|
-
|
399
|
-
# Default fetcher added to the model on inclusion, it behaves like
|
400
|
-
# ActiveRecord::Base.find_by_id
|
401
|
-
def fetch_by_id(id)
|
402
|
-
if IdentityCache.should_cache?
|
403
|
-
|
404
|
-
require_if_necessary do
|
405
|
-
object = IdentityCache.fetch(rails_cache_key(id)){ resolve_cache_miss(id) }
|
406
|
-
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
|
407
|
-
object
|
408
|
-
end
|
409
|
-
|
410
|
-
else
|
411
|
-
self.find_by_id(id)
|
412
|
-
end
|
413
|
-
end
|
414
|
-
|
415
|
-
# Default fetcher added to the model on inclusion, it behaves like
|
416
|
-
# ActiveRecord::Base.find, will raise ActiveRecord::RecordNotFound exception
|
417
|
-
# if id is not in the cache or the db.
|
418
|
-
def fetch(id)
|
419
|
-
fetch_by_id(id) or raise(ActiveRecord::RecordNotFound, "Couldn't find #{self.class.name} with ID=#{id}")
|
420
|
-
end
|
421
|
-
|
422
|
-
|
423
|
-
# Default fetcher added to the model on inclusion, if behaves like
|
424
|
-
# ActiveRecord::Base.find_all_by_id
|
425
|
-
def fetch_multi(*ids)
|
426
|
-
options = ids.extract_options!
|
427
|
-
if IdentityCache.should_cache?
|
428
|
-
|
429
|
-
require_if_necessary do
|
430
|
-
cache_keys = ids.map {|id| rails_cache_key(id) }
|
431
|
-
key_to_id_map = Hash[ cache_keys.zip(ids) ]
|
432
|
-
|
433
|
-
objects_by_key = IdentityCache.fetch_multi(*key_to_id_map.keys) do |unresolved_keys|
|
434
|
-
ids = unresolved_keys.map {|key| key_to_id_map[key] }
|
435
|
-
records = find_batch(ids, options)
|
436
|
-
records.compact.each(&:populate_association_caches)
|
437
|
-
records
|
438
|
-
end
|
439
|
-
|
440
|
-
cache_keys.map {|key| objects_by_key[key] }.compact
|
441
|
-
end
|
442
|
-
|
443
|
-
else
|
444
|
-
find_batch(ids, options)
|
445
|
-
end
|
446
|
-
end
|
447
|
-
|
448
|
-
def require_if_necessary #:nodoc:
|
449
|
-
# mem_cache_store returns raw value if unmarshal fails
|
450
|
-
rval = yield
|
451
|
-
case rval
|
452
|
-
when String
|
453
|
-
rval = Marshal.load(rval)
|
454
|
-
when Array
|
455
|
-
rval.map!{ |v| v.kind_of?(String) ? Marshal.load(v) : v }
|
456
|
-
end
|
457
|
-
rval
|
458
|
-
rescue ArgumentError => e
|
459
|
-
if e.message =~ /undefined [\w\/]+ (\w+)/
|
460
|
-
ok = Kernel.const_get($1) rescue nil
|
461
|
-
retry if ok
|
462
|
-
end
|
463
|
-
raise
|
464
|
-
end
|
465
|
-
|
466
|
-
module ParentModelExpiration # :nodoc:
|
467
|
-
def expire_parent_cache_on_changes(parent_name, foreign_key, parent_class, options = {})
|
468
|
-
new_parent = send(parent_name)
|
469
|
-
|
470
|
-
if new_parent && new_parent.respond_to?(:expire_primary_index, true)
|
471
|
-
if should_expire_identity_cache_parent?(foreign_key, options[:only_on_foreign_key_change])
|
472
|
-
new_parent.expire_primary_index
|
473
|
-
new_parent.expire_parent_cache if new_parent.respond_to?(:expire_parent_cache)
|
474
|
-
end
|
475
|
-
end
|
476
|
-
|
477
|
-
if transaction_changed_attributes[foreign_key].present?
|
478
|
-
begin
|
479
|
-
old_parent = parent_class.find(transaction_changed_attributes[foreign_key])
|
480
|
-
old_parent.expire_primary_index if old_parent.respond_to?(:expire_primary_index)
|
481
|
-
old_parent.expire_parent_cache if old_parent.respond_to?(:expire_parent_cache)
|
482
|
-
rescue ActiveRecord::RecordNotFound => e
|
483
|
-
# suppress errors finding the old parent if its been destroyed since it will have expired itself in that case
|
484
|
-
end
|
485
|
-
end
|
486
|
-
|
487
|
-
true
|
488
|
-
end
|
489
|
-
|
490
|
-
def should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
|
491
|
-
if only_on_foreign_key_change
|
492
|
-
destroyed? || was_new_record? || transaction_changed_attributes[foreign_key].present?
|
493
|
-
else
|
494
|
-
true
|
495
|
-
end
|
496
|
-
end
|
497
|
-
end
|
498
|
-
|
499
|
-
def add_parent_expiry_hook(child_class, options = {})
|
500
|
-
child_association = child_class.reflect_on_association(options[:inverse_name])
|
501
|
-
raise InverseAssociationError unless child_association
|
502
|
-
foreign_key = child_association.association_foreign_key
|
503
|
-
parent_class ||= self.name
|
504
|
-
new_parent = options[:inverse_name]
|
505
|
-
|
506
|
-
child_class.send(:include, ArTransactionChanges) unless child_class.include?(ArTransactionChanges)
|
507
|
-
child_class.send(:include, ParentModelExpiration) unless child_class.include?(ParentModelExpiration)
|
508
|
-
|
509
|
-
child_class.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
|
510
|
-
after_commit :expire_parent_cache
|
511
|
-
after_touch :expire_parent_cache
|
512
|
-
|
513
|
-
def expire_parent_cache
|
514
|
-
expire_parent_cache_on_changes(:#{options[:inverse_name]}, '#{foreign_key}', #{parent_class}, #{options.inspect})
|
515
|
-
end
|
516
|
-
CODE
|
517
|
-
end
|
518
|
-
|
519
|
-
def resolve_cache_miss(id)
|
520
|
-
self.find_by_id(id, :include => cache_fetch_includes).tap do |object|
|
521
|
-
object.try(:populate_association_caches)
|
522
|
-
end
|
523
|
-
end
|
524
|
-
|
525
|
-
def all_cached_associations
|
526
|
-
(cached_has_manys || {}).merge(cached_has_ones || {}).merge(cached_belongs_tos || {})
|
527
|
-
end
|
528
|
-
|
529
|
-
def all_cached_associations_needing_population
|
530
|
-
all_cached_associations.select do |cached_association, options|
|
531
|
-
options[:population_method_name].present? # non-embedded belongs_to associations don't need population
|
532
|
-
end
|
533
|
-
end
|
534
|
-
|
535
|
-
def cache_fetch_includes(additions = {})
|
536
|
-
additions = hashify_includes_structure(additions)
|
537
|
-
embedded_associations = all_cached_associations.select { |name, options| options[:embed] }
|
538
|
-
|
539
|
-
associations_for_identity_cache = embedded_associations.map do |child_association, options|
|
540
|
-
child_class = reflect_on_association(child_association).try(:klass)
|
541
|
-
|
542
|
-
child_includes = additions.delete(child_association)
|
543
|
-
|
544
|
-
if child_class.respond_to?(:cache_fetch_includes)
|
545
|
-
child_includes = child_class.cache_fetch_includes(child_includes)
|
546
|
-
end
|
547
|
-
|
548
|
-
if child_includes.blank?
|
549
|
-
child_association
|
550
|
-
else
|
551
|
-
{ child_association => child_includes }
|
552
|
-
end
|
553
|
-
end
|
554
|
-
|
555
|
-
associations_for_identity_cache.push(additions) if additions.keys.size > 0
|
556
|
-
associations_for_identity_cache.compact
|
557
|
-
end
|
558
|
-
|
559
|
-
def find_batch(ids, options = {})
|
560
|
-
@id_column ||= columns.detect {|c| c.name == "id"}
|
561
|
-
ids = ids.map{ |id| @id_column.type_cast(id) }
|
562
|
-
records = where('id IN (?)', ids).includes(cache_fetch_includes(options[:includes])).all
|
563
|
-
records_by_id = records.index_by(&:id)
|
564
|
-
records = ids.map{ |id| records_by_id[id] }
|
565
|
-
mismatching_ids = records.compact.map(&:id) - ids
|
566
|
-
IdentityCache.logger.error "[IDC id mismatch] fetch_batch_requested=#{ids.inspect} fetch_batch_got=#{mismatchig_ids.inspect} mismatching ids " unless mismatching_ids.empty?
|
567
|
-
records
|
568
|
-
end
|
569
|
-
|
570
|
-
def rails_cache_key(id)
|
571
|
-
rails_cache_key_prefix + id.to_s
|
572
|
-
end
|
573
|
-
|
574
|
-
|
575
|
-
def rails_cache_key_prefix
|
576
|
-
@rails_cache_key_prefix ||= begin
|
577
|
-
"IDC:blob:#{base_class.name}:#{IdentityCache.memcache_hash(IdentityCache.schema_to_string(columns))}:"
|
578
|
-
end
|
579
|
-
end
|
580
|
-
|
581
|
-
def rails_cache_index_key_for_fields_and_values(fields, values)
|
582
|
-
"IDC:index:#{base_class.name}:#{rails_cache_string_for_fields_and_values(fields, values)}"
|
583
|
-
end
|
584
|
-
|
585
|
-
def rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
|
586
|
-
"IDC:attribute:#{base_class.name}:#{attribute}:#{rails_cache_string_for_fields_and_values(fields, values)}"
|
587
|
-
end
|
588
|
-
|
589
|
-
def rails_cache_string_for_fields_and_values(fields, values)
|
590
|
-
"#{fields.join('/')}:#{IdentityCache.memcache_hash(values.join('/'))}"
|
591
|
-
end
|
592
|
-
|
593
|
-
private
|
594
|
-
|
595
|
-
def hashify_includes_structure(structure)
|
596
|
-
case structure
|
597
|
-
when nil
|
598
|
-
{}
|
599
|
-
when Symbol
|
600
|
-
{structure => []}
|
601
|
-
when Hash
|
602
|
-
structure.clone
|
603
|
-
when Array
|
604
|
-
structure.each_with_object({}) do |member, hash|
|
605
|
-
case member
|
606
|
-
when Hash
|
607
|
-
hash.merge(hash)
|
608
|
-
when Symbol
|
609
|
-
hash[member] = []
|
610
|
-
end
|
611
|
-
end
|
612
|
-
end
|
613
|
-
end
|
614
|
-
end
|
615
|
-
|
616
|
-
def populate_association_caches # :nodoc:
|
617
|
-
self.class.all_cached_associations_needing_population.each do |cached_association, options|
|
618
|
-
send(options[:population_method_name])
|
619
|
-
reflection = options[:embed] && self.class.reflect_on_association(cached_association)
|
620
|
-
if reflection && reflection.klass.respond_to?(:cached_has_manys)
|
621
|
-
child_objects = Array.wrap(send(options[:cached_accessor_name]))
|
622
|
-
child_objects.each(&:populate_association_caches)
|
623
|
-
end
|
624
|
-
end
|
625
|
-
self.clear_association_cache if self.respond_to?(:clear_association_cache)
|
626
|
-
end
|
627
|
-
|
628
|
-
def fetch_denormalized_cached_association(ivar_name, association_name) # :nodoc:
|
629
|
-
ivar_full_name = :"@#{ivar_name}"
|
630
|
-
if IdentityCache.should_cache?
|
631
|
-
populate_denormalized_cached_association(ivar_name, association_name)
|
632
|
-
IdentityCache.unmap_cached_nil_for(instance_variable_get(ivar_full_name))
|
633
|
-
else
|
634
|
-
send(association_name.to_sym)
|
635
|
-
end
|
636
|
-
end
|
637
|
-
|
638
|
-
def populate_denormalized_cached_association(ivar_name, association_name) # :nodoc:
|
639
|
-
ivar_full_name = :"@#{ivar_name}"
|
640
|
-
schema_hash_ivar = :"@#{ivar_name}_schema_hash"
|
641
|
-
reflection = association(association_name)
|
642
|
-
|
643
|
-
current_schema_hash = self.class.embedded_schema_hashes[association_name] ||= begin
|
644
|
-
IdentityCache.memcache_hash(IdentityCache.schema_to_string(reflection.klass.columns))
|
645
|
-
end
|
646
|
-
|
647
|
-
saved_schema_hash = instance_variable_get(schema_hash_ivar)
|
648
|
-
|
649
|
-
if saved_schema_hash == current_schema_hash
|
650
|
-
value = instance_variable_get(ivar_full_name)
|
651
|
-
return value unless value.nil?
|
652
|
-
end
|
653
|
-
|
654
|
-
reflection.load_target unless reflection.loaded?
|
655
|
-
|
656
|
-
loaded_association = send(association_name)
|
657
|
-
|
658
|
-
instance_variable_set(schema_hash_ivar, current_schema_hash)
|
659
|
-
instance_variable_set(ivar_full_name, IdentityCache.map_cached_nil_for(loaded_association))
|
660
|
-
end
|
661
|
-
|
662
|
-
def primary_cache_index_key # :nodoc:
|
663
|
-
self.class.rails_cache_key(id)
|
664
|
-
end
|
665
|
-
|
666
|
-
def secondary_cache_index_key_for_current_values(fields) # :nodoc:
|
667
|
-
self.class.rails_cache_index_key_for_fields_and_values(fields, fields.collect {|field| self.send(field)})
|
668
|
-
end
|
669
|
-
|
670
|
-
def secondary_cache_index_key_for_previous_values(fields) # :nodoc:
|
671
|
-
self.class.rails_cache_index_key_for_fields_and_values(fields, old_values_for_fields(fields))
|
672
|
-
end
|
673
|
-
|
674
|
-
def attribute_cache_key_for_attribute_and_previous_values(attribute, fields) # :nodoc:
|
675
|
-
self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields))
|
676
|
-
end
|
677
|
-
|
678
|
-
def old_values_for_fields(fields) # :nodoc:
|
679
|
-
fields.map do |field|
|
680
|
-
field_string = field.to_s
|
681
|
-
if destroyed? && transaction_changed_attributes.has_key?(field_string)
|
682
|
-
transaction_changed_attributes[field_string]
|
683
|
-
elsif persisted? && transaction_changed_attributes.has_key?(field_string)
|
684
|
-
transaction_changed_attributes[field_string]
|
685
|
-
else
|
686
|
-
self.send(field)
|
687
|
-
end
|
688
|
-
end
|
689
|
-
end
|
690
|
-
|
691
|
-
def expire_primary_index # :nodoc:
|
692
|
-
extra_keys = if respond_to? :updated_at
|
693
|
-
old_updated_at = old_values_for_fields([:updated_at]).first
|
694
|
-
"expiring_last_updated_at=#{old_updated_at}"
|
695
|
-
else
|
696
|
-
""
|
697
|
-
end
|
698
|
-
IdentityCache.logger.debug "[IdentityCache] expiring=#{self.class.name} expiring_id=#{id} #{extra_keys}"
|
699
|
-
|
700
|
-
IdentityCache.cache.delete(primary_cache_index_key)
|
701
|
-
end
|
702
|
-
|
703
|
-
def expire_secondary_indexes # :nodoc:
|
704
|
-
cache_indexes.try(:each) do |fields|
|
705
|
-
if self.destroyed?
|
706
|
-
IdentityCache.cache.delete(secondary_cache_index_key_for_previous_values(fields))
|
707
|
-
else
|
708
|
-
new_cache_index_key = secondary_cache_index_key_for_current_values(fields)
|
709
|
-
IdentityCache.cache.delete(new_cache_index_key)
|
710
|
-
|
711
|
-
if !was_new_record?
|
712
|
-
old_cache_index_key = secondary_cache_index_key_for_previous_values(fields)
|
713
|
-
IdentityCache.cache.delete(old_cache_index_key) unless old_cache_index_key == new_cache_index_key
|
714
|
-
end
|
715
|
-
end
|
716
|
-
end
|
717
|
-
end
|
718
|
-
|
719
|
-
def expire_attribute_indexes # :nodoc:
|
720
|
-
cache_attributes.try(:each) do |(attribute, fields)|
|
721
|
-
IdentityCache.cache.delete(attribute_cache_key_for_attribute_and_previous_values(attribute, fields)) unless was_new_record?
|
722
|
-
end
|
723
|
-
end
|
724
|
-
|
725
|
-
def expire_cache # :nodoc:
|
726
|
-
expire_primary_index
|
727
|
-
expire_secondary_indexes
|
728
|
-
expire_attribute_indexes
|
729
|
-
true
|
730
|
-
end
|
731
|
-
|
732
|
-
def was_new_record? # :nodoc:
|
733
|
-
!destroyed? && transaction_changed_attributes.has_key?('id') && transaction_changed_attributes['id'].nil?
|
734
|
-
end
|
735
|
-
|
736
|
-
class AlreadyIncludedError < StandardError; end
|
737
|
-
class InverseAssociationError < StandardError
|
738
|
-
def initialize
|
739
|
-
super "Inverse name for association could not be determined. Please use the :inverse_name option to specify the inverse association name for this cache."
|
740
|
-
end
|
741
|
-
end
|
742
146
|
end
|