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