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 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