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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: ba4de54fb34aecd2a44702ea1bb950c718cdc715
4
+ data.tar.gz: 752b53a7d7399f5273fd7083152c4b28ee5cbdeb
5
+ SHA512:
6
+ metadata.gz: ffa0486c327789a3d975ea768ffe63ed73878f4916aa54b24fa06c1771ece940b4e8bd67a2b8838e224d996a9088b2bd6696237209040608b30c10ed28bb0438
7
+ data.tar.gz: 903fc321ed4092d797d230302cfb8a09bfb3a4f47e58f0e67b605391ca0ea6dbc5916dd801ed06ee58e65ffae8793e6f25d8f7e94a875d2dd35d1fa427e62825
data/.travis.yml CHANGED
@@ -1,6 +1,7 @@
1
1
  language: ruby
2
2
  rvm:
3
3
  - 1.9.3
4
+ - 2.0.0
4
5
 
5
6
  services:
6
7
  - memcache
data/CHANGELOG CHANGED
@@ -1,3 +1,10 @@
1
+ 0.0.3
2
+ * Fix: memoization for multi hits actually work
3
+ * Fix: quotes SELECT projection elements on cache misses
4
+ * Add CPU performance benchmark
5
+ * Fix: table names are not hardcoded anymore
6
+ * Logger now differentiates memoized vs non memoized hits
7
+
1
8
  0.0.2
2
9
  * Fix: Existent embedded entries will no longer raise when ActiveModel::MissingAttributeError when accessing a newly created attribute.
3
10
  * Fix: Do not marshal raw AcriveRecord associations
data/README.md CHANGED
@@ -16,6 +16,8 @@ gem 'identity_cache'
16
16
  And then execute:
17
17
 
18
18
  $ bundle
19
+
20
+
19
21
 
20
22
  Add the following to your environment/production.rb:
21
23
 
@@ -23,6 +25,12 @@ Add the following to your environment/production.rb:
23
25
  config.identity_cache_store = :mem_cache_store, Memcached::Rails.new(:servers => ["mem1.server.com"])
24
26
  ```
25
27
 
28
+ Add an initializer with this code:
29
+
30
+ ```ruby
31
+ IdentityCache.cache_backend = ActiveSupport::Cache.lookup_store(*Rails.configuration.identity_cache_store)
32
+ ```
33
+
26
34
  ## Usage
27
35
 
28
36
  ### Basic Usage
data/Rakefile CHANGED
@@ -14,3 +14,22 @@ Rake::TestTask.new(:test) do |t|
14
14
  t.pattern = 'test/**/*_test.rb'
15
15
  t.verbose = true
16
16
  end
17
+
18
+ namespace :benchmark do
19
+ desc "Run the identity cache CPU benchmark"
20
+ task :cpu do
21
+ ruby "./performance/cpu.rb"
22
+ end
23
+
24
+ task :externals do
25
+ ruby "./performance/externals.rb"
26
+ end
27
+ end
28
+
29
+ namespace :profile do
30
+ desc "Profile IDC code"
31
+ task :run do
32
+ ruby "./performance/profile.rb"
33
+ end
34
+ end
35
+
@@ -22,7 +22,8 @@ Gem::Specification.new do |gem|
22
22
  gem.add_dependency('cityhash', '0.6.0')
23
23
  gem.add_development_dependency('memcache-client')
24
24
  gem.add_development_dependency('rake')
25
- gem.add_development_dependency('mocha')
25
+ gem.add_development_dependency('mocha', '0.14.0')
26
26
  gem.add_development_dependency('mysql2')
27
27
  gem.add_development_dependency('debugger')
28
+ gem.add_development_dependency('ruby-prof')
28
29
  end
@@ -1,21 +1,21 @@
1
1
  module IdentityCache
2
2
  module BelongsToCaching
3
+ extend ActiveSupport::Concern
3
4
 
4
- def self.included(base)
5
- base.send(:extend, ClassMethods)
5
+ included do |base|
6
6
  base.class_attribute :cached_belongs_tos
7
+ base.cached_belongs_tos = {}
7
8
  end
8
9
 
9
10
  module ClassMethods
10
11
  def cache_belongs_to(association, options = {})
11
- self.cached_belongs_tos ||= {}
12
12
  self.cached_belongs_tos[association] = options
13
13
 
14
14
  options[:embed] ||= false
15
- options[:cached_accessor_name] ||= "fetch_#{association}"
16
- options[:foreign_key] ||= reflect_on_association(association).foreign_key
17
- options[:associated_class] ||= reflect_on_association(association).class_name
18
-
15
+ options[:cached_accessor_name] ||= "fetch_#{association}"
16
+ options[:foreign_key] ||= reflect_on_association(association).foreign_key
17
+ options[:association_class] ||= reflect_on_association(association).klass
18
+ options[:prepopulate_method_name] ||= "prepopulate_fetched_#{association}"
19
19
  if options[:embed]
20
20
  raise NotImplementedError
21
21
  else
@@ -27,11 +27,15 @@ module IdentityCache
27
27
  self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
28
28
  def #{options[:cached_accessor_name]}
29
29
  if IdentityCache.should_cache? && #{options[:foreign_key]}.present? && !association(:#{association}).loaded?
30
- self.#{association} = #{options[:associated_class]}.fetch_by_id(#{options[:foreign_key]})
30
+ self.#{association} = #{options[:association_class]}.fetch_by_id(#{options[:foreign_key]})
31
31
  else
32
32
  #{association}
33
33
  end
34
34
  end
35
+
36
+ def #{options[:prepopulate_method_name]}(record)
37
+ self.#{association} = record
38
+ end
35
39
  CODE
36
40
  end
37
41
  end
@@ -0,0 +1,58 @@
1
+ module IdentityCache
2
+ module CacheKeyGeneration
3
+ extend ActiveSupport::Concern
4
+
5
+ module ClassMethods
6
+ def rails_cache_key(id)
7
+ rails_cache_key_prefix + id.to_s
8
+ end
9
+
10
+ def rails_cache_key_prefix
11
+ @rails_cache_key_prefix ||= begin
12
+ "IDC:blob:#{base_class.name}:#{IdentityCache.denormalized_schema_hash(self)}:"
13
+ end
14
+ end
15
+
16
+ def rails_cache_index_key_for_fields_and_values(fields, values)
17
+ "IDC:index:#{base_class.name}:#{rails_cache_string_for_fields_and_values(fields, values)}"
18
+ end
19
+
20
+ def rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
21
+ "IDC:attribute:#{base_class.name}:#{attribute}:#{rails_cache_string_for_fields_and_values(fields, values)}"
22
+ end
23
+
24
+ def rails_cache_string_for_fields_and_values(fields, values)
25
+ "#{fields.join('/')}:#{IdentityCache.memcache_hash(values.join('/'))}"
26
+ end
27
+ end
28
+
29
+ def primary_cache_index_key # :nodoc:
30
+ self.class.rails_cache_key(id)
31
+ end
32
+
33
+ def secondary_cache_index_key_for_current_values(fields) # :nodoc:
34
+ self.class.rails_cache_index_key_for_fields_and_values(fields, fields.collect {|field| self.send(field)})
35
+ end
36
+
37
+ def secondary_cache_index_key_for_previous_values(fields) # :nodoc:
38
+ self.class.rails_cache_index_key_for_fields_and_values(fields, old_values_for_fields(fields))
39
+ end
40
+
41
+ def attribute_cache_key_for_attribute_and_previous_values(attribute, fields) # :nodoc:
42
+ self.class.rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, old_values_for_fields(fields))
43
+ end
44
+
45
+ def old_values_for_fields(fields) # :nodoc:
46
+ fields.map do |field|
47
+ field_string = field.to_s
48
+ if destroyed? && transaction_changed_attributes.has_key?(field_string)
49
+ transaction_changed_attributes[field_string]
50
+ elsif persisted? && transaction_changed_attributes.has_key?(field_string)
51
+ transaction_changed_attributes[field_string]
52
+ else
53
+ self.send(field)
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,301 @@
1
+ module IdentityCache
2
+ module ConfigurationDSL
3
+ extend ActiveSupport::Concern
4
+
5
+ included do |base|
6
+ base.class_attribute :cache_indexes
7
+ base.class_attribute :cache_attributes
8
+ base.class_attribute :cached_has_manys
9
+ base.class_attribute :cached_has_ones
10
+ base.class_attribute :primary_cache_index_enabled
11
+
12
+ base.cached_has_manys = {}
13
+ base.cached_has_ones = {}
14
+ base.cache_attributes = []
15
+ base.cache_indexes = []
16
+ base.primary_cache_index_enabled = true
17
+
18
+ base.private_class_method :build_normalized_has_many_cache, :build_denormalized_association_cache,
19
+ :add_parent_expiry_hook, :identity_cache_multiple_value_dynamic_fetcher,
20
+ :identity_cache_single_value_dynamic_fetcher, :identity_cache_sql_conditions
21
+ end
22
+
23
+ module ClassMethods
24
+ # Declares a new index in the cache for the class where IdentityCache was
25
+ # included.
26
+ #
27
+ # IdentityCache will add a fetch_by_field1_and_field2_and_...field for every
28
+ # index.
29
+ #
30
+ # == Example:
31
+ #
32
+ # class Product
33
+ # include IdentityCache
34
+ # cache_index :name, :vendor
35
+ # end
36
+ #
37
+ # Will add Product.fetch_by_name_and_vendor
38
+ #
39
+ # == Parameters
40
+ #
41
+ # +fields+ Array of symbols or strings representing the fields in the index
42
+ #
43
+ # == Options
44
+ # * unique: if the index would only have unique values
45
+ #
46
+ def cache_index(*fields)
47
+ raise NotImplementedError, "Cache indexes need an enabled primary index" unless primary_cache_index_enabled
48
+ options = fields.extract_options!
49
+ self.cache_indexes.push fields
50
+
51
+ field_list = fields.join("_and_")
52
+ arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
53
+
54
+ if options[:unique]
55
+ self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
56
+ def fetch_by_#{field_list}(#{arg_list})
57
+ identity_cache_single_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}])
58
+ end
59
+
60
+ # exception throwing variant
61
+ def fetch_by_#{field_list}!(#{arg_list})
62
+ fetch_by_#{field_list}(#{arg_list}) or raise ActiveRecord::RecordNotFound
63
+ end
64
+ CODE
65
+ else
66
+ self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
67
+ def fetch_by_#{field_list}(#{arg_list})
68
+ identity_cache_multiple_value_dynamic_fetcher(#{fields.inspect}, [#{arg_list}])
69
+ end
70
+ CODE
71
+ end
72
+ end
73
+
74
+
75
+ # Will cache an association to the class including IdentityCache.
76
+ # The embed option, if set, will make IdentityCache keep the association
77
+ # values in the same cache entry as the parent.
78
+ #
79
+ # Embedded associations are more effective in offloading database work,
80
+ # however they will increase the size of the cache entries and make the
81
+ # whole entry expire when any of the embedded members change.
82
+ #
83
+ # == Example:
84
+ # class Product
85
+ # cached_has_many :options, :embed => false
86
+ # cached_has_many :orders
87
+ # cached_has_many :buyers, :inverse_name => 'line_item'
88
+ # end
89
+ #
90
+ # == Parameters
91
+ # +association+ Name of the association being cached as a symbol
92
+ #
93
+ # == Options
94
+ #
95
+ # * embed: If set will cause IdentityCache to keep the values for this
96
+ # association in the same cache entry as the parent, instead of its own.
97
+ # * inverse_name: The name of the parent in the association if the name is
98
+ # not the lowercase pluralization of the parent object's class
99
+ def cache_has_many(association, options = {})
100
+ options[:embed] ||= false
101
+ options[:inverse_name] ||= self.name.underscore.to_sym
102
+ raise InverseAssociationError unless self.reflect_on_association(association)
103
+ self.cached_has_manys[association] = options
104
+
105
+ if options[:embed]
106
+ build_denormalized_association_cache(association, options)
107
+ else
108
+ build_normalized_has_many_cache(association, options)
109
+ end
110
+ end
111
+
112
+ # Will cache an association to the class including IdentityCache.
113
+ # The embed option if set will make IdentityCache keep the association
114
+ # values in the same cache entry as the parent.
115
+ #
116
+ # Embedded associations are more effective in offloading database work,
117
+ # however they will increase the size of the cache entries and make the
118
+ # whole entry expire with the change of any of the embedded members
119
+ #
120
+ # == Example:
121
+ # class Product
122
+ # cached_has_one :store, :embed => false
123
+ # cached_has_one :vendor
124
+ # end
125
+ #
126
+ # == Parameters
127
+ # +association+ Symbol with the name of the association being cached
128
+ #
129
+ # == Options
130
+ #
131
+ # * embed: If set will cause IdentityCache to keep the values for this
132
+ # association in the same cache entry as the parent, instead of its own.
133
+ # * inverse_name: The name of the parent in the association ( only
134
+ # necessary if the name is not the lowercase pluralization of the
135
+ # parent object's class)
136
+ def cache_has_one(association, options = {})
137
+ options[:embed] ||= true
138
+ options[:inverse_name] ||= self.name.underscore.to_sym
139
+ raise InverseAssociationError unless self.reflect_on_association(association)
140
+ self.cached_has_ones[association] = options
141
+
142
+ if options[:embed]
143
+ build_denormalized_association_cache(association, options)
144
+ else
145
+ raise NotImplementedError
146
+ end
147
+ end
148
+
149
+ # Will cache a single attribute on its own blob, it will add a
150
+ # fetch_attribute_by_id (or the value of the by option).
151
+ #
152
+ # == Example:
153
+ # class Product
154
+ # cache_attribute :quantity, :by => :name
155
+ # cache_attribute :quantity :by => [:name, :vendor]
156
+ # end
157
+ #
158
+ # == Parameters
159
+ # +attribute+ Symbol with the name of the attribute being cached
160
+ #
161
+ # == Options
162
+ #
163
+ # * by: Other attribute or attributes in the model to keep values indexed. Default is :id
164
+ def cache_attribute(attribute, options = {})
165
+ options[:by] ||= :id
166
+ fields = Array(options[:by])
167
+
168
+ self.cache_attributes.push [attribute, fields]
169
+
170
+ field_list = fields.join("_and_")
171
+ arg_list = (0...fields.size).collect { |i| "arg#{i}" }.join(',')
172
+
173
+ self.instance_eval(ruby = <<-CODE, __FILE__, __LINE__)
174
+ def fetch_#{attribute}_by_#{field_list}(#{arg_list})
175
+ attribute_dynamic_fetcher(#{attribute.inspect}, #{fields.inspect}, [#{arg_list}])
176
+ end
177
+ CODE
178
+ end
179
+
180
+ def disable_primary_cache_index
181
+ raise NotImplementedError, "Secondary indexes rely on the primary index to function. You must either remove the secondary indexes or don't disable the primary" if self.cache_indexes.size > 0
182
+ self.primary_cache_index_enabled = false
183
+ end
184
+
185
+ def identity_cache_single_value_dynamic_fetcher(fields, values) # :nodoc:
186
+ sql_on_miss = "SELECT `id` FROM `#{table_name}` WHERE #{identity_cache_sql_conditions(fields, values)} LIMIT 1"
187
+ cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
188
+ id = IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
189
+ unless id.nil?
190
+ record = fetch_by_id(id.to_i)
191
+ IdentityCache.cache.delete(cache_key) unless record
192
+ end
193
+
194
+ record
195
+ end
196
+
197
+ def identity_cache_multiple_value_dynamic_fetcher(fields, values) # :nodoc
198
+ sql_on_miss = "SELECT `id` FROM `#{table_name}` WHERE #{identity_cache_sql_conditions(fields, values)}"
199
+ cache_key = rails_cache_index_key_for_fields_and_values(fields, values)
200
+ ids = IdentityCache.fetch(cache_key) { connection.select_values(sql_on_miss) }
201
+
202
+ ids.empty? ? [] : fetch_multi(*ids)
203
+ end
204
+
205
+ def build_denormalized_association_cache(association, options) #:nodoc:
206
+ options[:association_class] ||= reflect_on_association(association).klass
207
+ options[:cached_accessor_name] ||= "fetch_#{association}"
208
+ options[:records_variable_name] ||= "cached_#{association}"
209
+ options[:population_method_name] ||= "populate_#{association}_cache"
210
+
211
+
212
+ unless instance_methods.include?(options[:cached_accessor_name].to_sym)
213
+ self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
214
+ def #{options[:cached_accessor_name]}
215
+ fetch_denormalized_cached_association('#{options[:records_variable_name]}', :#{association})
216
+ end
217
+
218
+ def #{options[:population_method_name]}
219
+ populate_denormalized_cached_association('#{options[:records_variable_name]}', :#{association})
220
+ end
221
+ CODE
222
+
223
+ add_parent_expiry_hook(options.merge(:only_on_foreign_key_change => false))
224
+ end
225
+ end
226
+
227
+ def build_normalized_has_many_cache(association, options) #:nodoc:
228
+ singular_association = association.to_s.singularize
229
+ options[:association_class] ||= reflect_on_association(association).klass
230
+ options[:cached_accessor_name] ||= "fetch_#{association}"
231
+ options[:ids_name] ||= "#{singular_association}_ids"
232
+ options[:cached_ids_name] ||= "fetch_#{options[:ids_name]}"
233
+ options[:ids_variable_name] ||= "cached_#{options[:ids_name]}"
234
+ options[:records_variable_name] ||= "cached_#{association}"
235
+ options[:population_method_name] ||= "populate_#{association}_cache"
236
+ options[:prepopulate_method_name] ||= "prepopulate_fetched_#{association}"
237
+
238
+ self.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
239
+ attr_reader :#{options[:ids_variable_name]}
240
+
241
+ def #{options[:cached_ids_name]}
242
+ #{options[:population_method_name]} unless @#{options[:ids_variable_name]}
243
+ @#{options[:ids_variable_name]}
244
+ end
245
+
246
+ def #{options[:population_method_name]}
247
+ @#{options[:ids_variable_name]} = #{options[:ids_name]}
248
+ association_cache.delete(:#{association})
249
+ end
250
+
251
+ def #{options[:cached_accessor_name]}
252
+ if IdentityCache.should_cache? || #{association}.loaded?
253
+ #{options[:population_method_name]} unless @#{options[:ids_variable_name]} || @#{options[:records_variable_name]}
254
+ @#{options[:records_variable_name]} ||= #{options[:association_class]}.fetch_multi(*@#{options[:ids_variable_name]})
255
+ else
256
+ #{association}
257
+ end
258
+ end
259
+
260
+ def #{options[:prepopulate_method_name]}(records)
261
+ @#{options[:records_variable_name]} = records
262
+ end
263
+ CODE
264
+
265
+ add_parent_expiry_hook(options.merge(:only_on_foreign_key_change => true))
266
+ end
267
+
268
+ def attribute_dynamic_fetcher(attribute, fields, values) #:nodoc:
269
+ cache_key = rails_cache_key_for_attribute_and_fields_and_values(attribute, fields, values)
270
+ sql_on_miss = "SELECT `#{attribute}` FROM `#{table_name}` WHERE #{identity_cache_sql_conditions(fields, values)} LIMIT 1"
271
+
272
+ IdentityCache.fetch(cache_key) { connection.select_value(sql_on_miss) }
273
+ end
274
+
275
+ def add_parent_expiry_hook(options)
276
+ child_class = options[:association_class]
277
+ child_association = child_class.reflect_on_association(options[:inverse_name])
278
+ raise InverseAssociationError unless child_association
279
+ foreign_key = child_association.association_foreign_key
280
+ parent_class ||= self.name
281
+ new_parent = options[:inverse_name]
282
+
283
+ child_class.send(:include, ArTransactionChanges) unless child_class.include?(ArTransactionChanges)
284
+ child_class.send(:include, ParentModelExpiration) unless child_class.include?(ParentModelExpiration)
285
+
286
+ child_class.class_eval(ruby = <<-CODE, __FILE__, __LINE__)
287
+ after_commit :expire_parent_cache
288
+ after_touch :expire_parent_cache
289
+
290
+ def expire_parent_cache
291
+ expire_parent_cache_on_changes(:#{options[:inverse_name]}, '#{foreign_key}', #{parent_class}, #{options[:only_on_foreign_key_change]})
292
+ end
293
+ CODE
294
+ end
295
+
296
+ def identity_cache_sql_conditions(fields, values)
297
+ fields.each_with_index.collect { |f, i| "`#{f}` = #{quote_value(values[i])}" }.join(" AND ")
298
+ end
299
+ end
300
+ end
301
+ end
@@ -0,0 +1,118 @@
1
+ require 'monitor'
2
+
3
+ module IdentityCache
4
+ class MemoizedCacheProxy
5
+ attr_writer :memcache
6
+
7
+ def initialize(memcache = nil)
8
+ @memcache = memcache || Rails.cache
9
+ @key_value_maps = Hash.new {|h, k| h[k] = {} }
10
+ end
11
+
12
+ def memoized_key_values
13
+ @key_value_maps[Thread.current]
14
+ end
15
+
16
+ def with_memoization(&block)
17
+ Thread.current[:memoizing_idc] = true
18
+ yield
19
+ ensure
20
+ clear_memoization
21
+ Thread.current[:memoizing_idc] = false
22
+ end
23
+
24
+ def write(key, value)
25
+ memoized_key_values[key] = value if memoizing?
26
+ @memcache.write(key, value)
27
+ end
28
+
29
+ def read(key)
30
+ used_memcached = true
31
+
32
+ result = if memoizing?
33
+ used_memcached = false
34
+ mkv = memoized_key_values
35
+
36
+ mkv.fetch(key) do
37
+ used_memcached = true
38
+ mkv[key] = @memcache.read(key)
39
+ end
40
+
41
+ else
42
+ @memcache.read(key)
43
+ end
44
+
45
+ if result
46
+ IdentityCache.logger.debug { "[IdentityCache] #{ used_memcached ? '(memcache)' : '(memoized)' } cache hit for #{key}" }
47
+ else
48
+ IdentityCache.logger.debug { "[IdentityCache] cache miss for #{key}" }
49
+ end
50
+
51
+ result
52
+ end
53
+
54
+ def delete(key)
55
+ memoized_key_values.delete(key) if memoizing?
56
+ @memcache.delete(key)
57
+ end
58
+
59
+ def read_multi(*keys)
60
+
61
+ if IdentityCache.logger.debug?
62
+ memoized_keys , memcache_keys = [], []
63
+ end
64
+
65
+ result = if memoizing?
66
+ hash = {}
67
+ mkv = memoized_key_values
68
+
69
+ missing_keys = keys.reject do |key|
70
+ if mkv.has_key?(key)
71
+ memoized_keys << key if IdentityCache.logger.debug?
72
+ hit = mkv[key]
73
+ hash[key] = hit unless hit.nil?
74
+ true
75
+ end
76
+ end
77
+
78
+ hits = missing_keys.empty? ? {} : @memcache.read_multi(*missing_keys)
79
+
80
+ missing_keys.each do |key|
81
+ hit = hits[key]
82
+ mkv[key] = hit
83
+ hash[key] = hit unless hit.nil?
84
+ end
85
+ hash
86
+ else
87
+ @memcache.read_multi(*keys)
88
+ end
89
+
90
+ if IdentityCache.logger.debug?
91
+
92
+ result.each do |k, v|
93
+ memcache_keys << k if !v.nil? && !memoized_keys.include?(k)
94
+ end
95
+
96
+ memoized_keys.each{ |k| IdentityCache.logger.debug "[IdentityCache] (memoized) cache hit for #{k} (multi)" }
97
+ memcache_keys.each{ |k| IdentityCache.logger.debug "[IdentityCache] (memcache) cache hit for #{k} (multi)" }
98
+ end
99
+
100
+ result
101
+ end
102
+
103
+ def clear
104
+ clear_memoization
105
+ @memcache.clear
106
+ end
107
+
108
+ private
109
+
110
+ def clear_memoization
111
+ @key_value_maps.delete(Thread.current)
112
+ end
113
+
114
+ def memoizing?
115
+ Thread.current[:memoizing_idc]
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,34 @@
1
+ module IdentityCache
2
+ module ParentModelExpiration # :nodoc:
3
+ def expire_parent_cache_on_changes(parent_name, foreign_key, parent_class, only_on_foreign_key_change)
4
+ new_parent = send(parent_name)
5
+
6
+ if new_parent && new_parent.respond_to?(:expire_primary_index, true)
7
+ if should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
8
+ new_parent.expire_primary_index
9
+ new_parent.expire_parent_cache if new_parent.respond_to?(:expire_parent_cache)
10
+ end
11
+ end
12
+
13
+ if transaction_changed_attributes[foreign_key].present?
14
+ begin
15
+ old_parent = parent_class.find(transaction_changed_attributes[foreign_key])
16
+ old_parent.expire_primary_index if old_parent.respond_to?(:expire_primary_index)
17
+ old_parent.expire_parent_cache if old_parent.respond_to?(:expire_parent_cache)
18
+ rescue ActiveRecord::RecordNotFound => e
19
+ # suppress errors finding the old parent if its been destroyed since it will have expired itself in that case
20
+ end
21
+ end
22
+
23
+ true
24
+ end
25
+
26
+ def should_expire_identity_cache_parent?(foreign_key, only_on_foreign_key_change)
27
+ if only_on_foreign_key_change
28
+ destroyed? || was_new_record? || transaction_changed_attributes[foreign_key].present?
29
+ else
30
+ true
31
+ end
32
+ end
33
+ end
34
+ end