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
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
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
|
+
|
data/identity_cache.gemspec
CHANGED
@@ -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
|
-
|
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]
|
16
|
-
options[:foreign_key]
|
17
|
-
options[:
|
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[:
|
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
|