benny_cache 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,21 @@
1
+ module BennyCache
2
+ class Config
3
+ @@_store = nil
4
+
5
+ def self.store
6
+ return @@_store if @@_store
7
+
8
+ if const_defined?('Rails') && Rails.cache
9
+ @@_store = Rails.cache
10
+ else
11
+ @@_store = BennyCache::Cache.new
12
+ end
13
+
14
+ @@_store
15
+ end
16
+
17
+ def self.store=(store)
18
+ @@_store = store
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,264 @@
1
+ require 'digest/sha1'
2
+
3
+ module BennyCache
4
+ module Model
5
+
6
+ def self.included(base)
7
+ base.instance_eval {
8
+ include BennyCache::Base
9
+ }
10
+
11
+ base.extend BennyCache::Model::ClassMethods
12
+
13
+ unless base.class_variable_defined? :@@BENNY_MODEL_INDEXES
14
+ base.class_variable_set(:@@BENNY_MODEL_INDEXES, [])
15
+ end
16
+
17
+ unless base.class_variable_defined? :@@BENNY_DATA_INDEXES
18
+ base.class_variable_set(:@@BENNY_DATA_INDEXES, [])
19
+ end
20
+
21
+ unless base.class_variable_defined? :@@BENNY_METHOD_INDEXES
22
+ base.class_variable_set(:@@BENNY_METHOD_INDEXES, [])
23
+ end
24
+
25
+ if base.respond_to? :after_save
26
+ base.after_save :benny_model_cache_delete
27
+ end
28
+
29
+ if base.respond_to? :after_destroy
30
+ base.after_destroy :benny_model_cache_delete
31
+ end
32
+
33
+ def benny_model_cache_delete
34
+ ns = self.class.get_benny_model_ns
35
+ key = "#{ns}/#{self.id}"
36
+
37
+ BennyCache::Config.store.delete(key)
38
+ self.class.class_variable_get(:@@BENNY_MODEL_INDEXES).each do |idx|
39
+
40
+ if idx.is_a?(Symbol)
41
+ key = "#{ns}/#{idx}/" + idx.to_s.gsub(/(\w+)/) { self.send($1) }
42
+ elsif idx.is_a?(String)
43
+ key = "#{ns}/" + idx.to_s.gsub(/:(\w+)/) { "#{self.send($1) }" }
44
+ end
45
+
46
+ BennyCache::Config.store.delete(key)
47
+ end
48
+ end
49
+
50
+ def benny_data_cache(data_index, &block)
51
+ full_index = self.class.benny_data_cache_full_index(self.id, data_index)
52
+ BennyCache::Config.store.fetch(full_index, &block)
53
+ end
54
+ end
55
+
56
+ module ClassMethods
57
+
58
+ ##
59
+ # Clear the a data cache from a given model
60
+ def benny_data_cache_delete(model_id, data_index)
61
+ full_index = self.benny_data_cache_full_index(model_id, data_index)
62
+ BennyCache::Config.store.delete(full_index)
63
+ end
64
+
65
+ # Clear the a method cache from a given model
66
+ def benny_method_cache_delete(model_id, data_index)
67
+ full_index = self.benny_method_cache_full_index(model_id, data_index)
68
+
69
+ keys = BennyCache::Config.store.fetch(full_index)
70
+
71
+ unless (keys.nil? || keys.empty?)
72
+ keys.each do |key|
73
+ BennyCache::Config.store.delete(key)
74
+ end
75
+ end
76
+ BennyCache::Config.store.delete(full_index)
77
+
78
+ end
79
+
80
+ def benny_data_cache_full_index(model_id, data_index) # :nodoc:
81
+ raise "undefined cache data key '#{data_index}'" unless self.class_variable_get(:@@BENNY_DATA_INDEXES).include?(data_index.to_s)
82
+ ns = self.get_benny_model_ns
83
+ full_index = "#{ns}/#{model_id}/data/#{data_index.to_s}"
84
+ end
85
+
86
+ def benny_method_cache_full_index(model_id, method_index) # :nodoc:
87
+ raise "undefined cache method key '#{method_index}'" unless self.class_variable_get(:@@BENNY_METHOD_INDEXES).include?(method_index.to_s)
88
+ ns = self.get_benny_model_ns
89
+ full_index = "#{ns}/#{model_id}/method/#{method_index.to_s}"
90
+ end
91
+
92
+ # For each benny cached method, we store an array cached results. Each result
93
+ # is unique to the parameters used during the method call.
94
+ # Meaning: foo.bar(:baz) and foo.bar(:bin) are stored as two separate
95
+ # results, with their own key in the benny cache. Additionally,
96
+ # these various keys associated with foo.bar are stored as an
97
+ # separate array in their own cache entry.
98
+ # When it's time to clear the all the foo.bar cached results, we will
99
+ # have an array of keys to reference.
100
+ def benny_method_store_method_args_index(base_method_index, args_method_index)
101
+ method_sig_ary = BennyCache::Config.store.fetch(base_method_index) { [] }
102
+ unless method_sig_ary.include?(args_method_index)
103
+ method_sig_ary.push(args_method_index)
104
+ BennyCache::Config.store.write(base_method_index, method_sig_ary)
105
+ end
106
+ end
107
+
108
+ def benny_method_store_method_args_indexes_delete(model_id, method_name)
109
+ base_method_index = self.benny_method_cache_full_index(model_id, method_name)
110
+
111
+ method_sig_ary = BennyCache::Config.store.fetch(base_method_index) { [] }
112
+ method_sig_ary.each do |args_method_index|
113
+ BennyCache::Config.store.clear(args_method_index)
114
+ end
115
+ BennyCache::Config.store.clear(base_method_index)
116
+ end
117
+
118
+ ##
119
+ # Declares one or more caching indexes for instances of this class.
120
+ # You do not have to declare an :id index, but if you will be referencing or loading
121
+ # models by other indexes, declare them here.
122
+ #
123
+ # Explicit declarations are needed so BennyCache knows which cache keys to clear
124
+ # on a relevant change.
125
+ #
126
+ # Valid options are symbols of other methods, or for multiple-field indexes, an array
127
+ # of :symbols
128
+ # class Agent
129
+ # benny_model_index :user_id
130
+ # # internally works like Agent.where(:user_id => user_id ).first when referenced
131
+ # end
132
+ #
133
+ # or
134
+ #
135
+ # class Location
136
+ # benny_model_index [:x, :y]
137
+ # # internally works like Locaion.where(:x => x, :y => y ).first when referenced
138
+ # end
139
+ #
140
+ # You can include many indexes in the declaration:
141
+ #
142
+ # class Foo
143
+ # benny_model_index :bar, :baz, [:zip, :zap]
144
+ # end
145
+
146
+ def benny_model_index(*options)
147
+ index_keys = options.map do |idx|
148
+ if idx.is_a?(Array)
149
+ idx.map{ |jdx| "#{jdx.to_s}/:#{jdx.to_s}"}.join("/")
150
+ else
151
+ "#{idx.to_s}/:#{idx.to_s}"
152
+ end
153
+ end
154
+ self.class_variable_get(:@@BENNY_MODEL_INDEXES).push(*index_keys)
155
+ end
156
+
157
+ def benny_data_index(*options)
158
+ self.class_variable_get(:@@BENNY_DATA_INDEXES).push(*(options.map(&:to_s)))
159
+ end
160
+
161
+ def benny_method_args_sig(*options)
162
+ sig = ''
163
+ options.each { |arg|
164
+ if arg.respond_to?(:id)
165
+ sig += "#{arg.class}/##{arg.id}"
166
+ elsif arg.is_a?(Array)
167
+ arg.each do |val|
168
+ sig += benny_method_args_sig(*val)
169
+ end
170
+ elsif arg.is_a?(Hash)
171
+ arg.keys.sort.each do |key|
172
+ sig += benny_method_args_sig(key) + benny_method_args_sig(*(arg[key]))
173
+ end
174
+ else
175
+ sig += Digest::SHA1.hexdigest(Marshal.dump(arg))
176
+ end
177
+ }
178
+ sig
179
+ end
180
+
181
+ def benny_method_index(*options)
182
+ self.class_variable_get(:@@BENNY_METHOD_INDEXES).push(*(options.map(&:to_s)))
183
+
184
+ options.each do |method_name|
185
+
186
+ define_method "#{method_name}_with_benny_cache" do |*method_opts|
187
+ @_benny_method_local_cache ||= {}
188
+ #puts "benny cache method: #{method_name}_with_benny_cache #{method_opts.inspect}"
189
+
190
+ model_id = self.id
191
+ base_method_index = self.class.benny_method_cache_full_index(model_id, method_name)
192
+
193
+ sig = self.class.benny_method_args_sig(*method_opts)
194
+
195
+ args_method_index = "#{base_method_index}/args/#{sig}"
196
+
197
+ self.class.benny_method_store_method_args_index(base_method_index, args_method_index)
198
+
199
+ return @_benny_method_local_cache[args_method_index] if @_benny_method_local_cache[args_method_index]
200
+
201
+ return_data = BennyCache::Config.store.fetch(args_method_index) {
202
+ self.send("#{method_name}_without_benny_cache", *method_opts)
203
+ }
204
+
205
+ @_benny_method_local_cache[args_method_index] = return_data
206
+ return_data
207
+
208
+ end
209
+ alias_method "#{method_name}_without_benny_cache", method_name
210
+ alias_method method_name, "#{method_name}_with_benny_cache"
211
+ alias_method "#{method_name}!", "#{method_name}_without_benny_cache"
212
+
213
+ end
214
+
215
+ end
216
+
217
+ ##
218
+ # Retrieves a model from the cache. If the model is no in the cache, BennyCache will load it from the database
219
+ # and store in the cache.
220
+ #
221
+ # agent = Agent.benny_model_cache(1)
222
+ #
223
+ # If the agent with id of 1 is not in the cache, it will make an ActiveRecord call to popuplate the cache,
224
+ # and return the model, like so:
225
+ # Agent.find(1)
226
+ #
227
+ # If you have declared separate data indexes, you can pass a hash and and BennyCache will use
228
+ # ActiveRelation#where to populate the hash
229
+ #
230
+ # Agent.benny_model_cache(:user_id => 999)
231
+ #
232
+ # To populate cache, BennyCache will call
233
+ #
234
+ # Agent.where(:user_id => 999)
235
+ #
236
+ def benny_model_cache(options)
237
+ ns = self.get_benny_model_ns
238
+
239
+ if options.is_a?(Hash)
240
+ key_format = []
241
+ key = []
242
+ options.keys.sort.each do |k|
243
+ key_format << "#{k.to_s}/:#{k.to_s}"
244
+ key << "#{k.to_s}/#{options[k].to_s}"
245
+ end
246
+
247
+ key = key.join('/')
248
+
249
+ key_format = key_format.join('/')
250
+
251
+ raise "undefined cache key format #{ns}/#{key_format}" unless self.class_variable_get(:@@BENNY_MODEL_INDEXES).include?(key_format)
252
+
253
+ BennyCache::Config.store.fetch("#{ns}/#{key}") {
254
+ self.where(options).first
255
+ }
256
+ else # should be a number/id
257
+ BennyCache::Config.store.fetch("#{ns}/#{options}") {
258
+ self.find(options)
259
+ }
260
+ end
261
+ end
262
+ end
263
+ end
264
+ end
@@ -0,0 +1,58 @@
1
+ module BennyCache
2
+ module Related
3
+ def self.included(base) #:nodoc:
4
+ base.send :include, BennyCache::Base
5
+
6
+ base.extend BennyCache::ClassMethods
7
+ unless(base.class_variable_defined? :@@benny_related_indexes)
8
+ base.class_variable_set(:@@benny_related_indexes, [])
9
+ end
10
+
11
+ unless(base.class_variable_defined? :@@benny_related_methods)
12
+ base.class_variable_set(:@@benny_related_methods, [])
13
+ end
14
+
15
+ if base.respond_to?(:after_save)
16
+ base.after_save :benny_cache_clear_related
17
+ end
18
+
19
+ if base.respond_to?(:after_destroy)
20
+ base.after_destroy :benny_cache_clear_related
21
+ end
22
+ end
23
+
24
+ def benny_cache_clear_related
25
+ self.class.class_variable_get(:@@benny_related_indexes).each do |key|
26
+ local_field, klass, data_cache = key.split('/')
27
+ local_field = local_field[1, local_field.length]
28
+ const = benny_constantize(klass)
29
+ id = self.send(local_field)
30
+ const.benny_data_cache_delete(id, data_cache) if id
31
+ end
32
+
33
+ self.class.class_variable_get(:@@benny_related_methods).each do |key|
34
+ local_field, klass, method_cache = key.split('/')
35
+ local_field = local_field[1, local_field.length]
36
+ const = benny_constantize(klass)
37
+ id = self.send(local_field)
38
+ const.benny_method_cache_delete(id, method_cache) if id
39
+ end
40
+
41
+ end
42
+ end
43
+
44
+ module ClassMethods
45
+
46
+ def benny_related_index(*options)
47
+ index_keys = options.map {|idx| idx.is_a?(Array) ? idx.map{ |jdx| "#{jdx.to_s}/:#{jdx.to_s}"}.join("/") : idx }
48
+ self.class_variable_get(:@@benny_related_indexes).push(*index_keys)
49
+ end
50
+
51
+ def benny_related_method(*options)
52
+ index_keys = options.map {|idx| idx.is_a?(Array) ? idx.map{ |jdx| "#{jdx.to_s}/:#{jdx.to_s}"}.join("/") : idx }
53
+ self.class_variable_get(:@@benny_related_methods).push(*index_keys)
54
+ end
55
+
56
+
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ module BennyCache
2
+ VERSION = "0.0.1"
3
+ end
@@ -0,0 +1,59 @@
1
+ require_relative "../spec_helper"
2
+ describe BennyCache::Cache do
3
+ it "is a class" do
4
+ BennyCache::Cache
5
+ end
6
+
7
+ it "can be instantiated" do
8
+ c = BennyCache::Cache.new
9
+ c.should be_true
10
+ end
11
+
12
+ describe do
13
+ before(:each) do
14
+ @c = BennyCache::Cache.new
15
+ @key = "foo"
16
+ @val = "bar"
17
+ end
18
+
19
+ methods = %w(fetch read write delete)
20
+ methods.each do |m|
21
+ it "should respond to #{m}" do
22
+ @c.respond_to?(m).should be_true
23
+ end
24
+ end
25
+
26
+ it "should write and read keyed data" do
27
+ @c.write(@key, @val)
28
+ @c.read(@key).should == @val
29
+ end
30
+
31
+ it "can delete keys" do
32
+ @c.write(@key, @val)
33
+ @c.read(@key).should == @val
34
+ @c.delete(@key)
35
+ @c.read(@key).should be_nil
36
+ end
37
+
38
+ it "#fetch can set a nil val with a block" do
39
+ @c.read(@key).should be_nil
40
+ set_val = @c.fetch(@key) {
41
+ @val
42
+ }
43
+ set_val.should == @val
44
+ @c.read(@key).should == @val
45
+ end
46
+
47
+ it "#clear should remove all cached data" do
48
+ key2 = "baz"
49
+ val2 = "bin"
50
+ @c.write(@key, @val)
51
+ @c.write(key2, val2)
52
+ @c.read(@key).should == @val
53
+ @c.read(key2).should == val2
54
+ @c.clear
55
+ @c.read(@key).should be_nil
56
+ @c.read(key2).should be_nil
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,15 @@
1
+ require_relative "../spec_helper"
2
+
3
+ require 'mocha_standalone'
4
+
5
+ describe BennyCache::Config do
6
+ it "should exist" do
7
+ BennyCache::Config.should be_true
8
+ end
9
+
10
+ it "should default to a BennyCache::Cache" do
11
+ BennyCache::Config.store = nil
12
+ BennyCache::Config.store.class.should == BennyCache::Cache
13
+ end
14
+
15
+ end
@@ -0,0 +1,85 @@
1
+ require_relative "../spec_helper"
2
+
3
+ require 'mocha_standalone'
4
+
5
+ describe BennyCache::Model do
6
+
7
+
8
+ describe "method caching" do
9
+ before(:each) do
10
+ @model = ModelCacheFake.new
11
+ @model.id = 1
12
+ @model.other_id = 123
13
+ @model.x = 12
14
+ @model.y = 36
15
+ @store = BennyCache::Cache.new
16
+ BennyCache::Config.store=@store
17
+ end
18
+
19
+ it "should call the original once method if uncached" do
20
+ @model.expects(:method_to_cache_without_benny_cache).with(:foo).returns(:stuff) # only once!
21
+ @model.expects(:method_to_cache_without_benny_cache).with(:bar).returns(:other_stuff)
22
+
23
+ rv = @model.method_to_cache :foo
24
+ rv.should == :stuff
25
+ rv = @model.method_to_cache :foo # should hit cache
26
+ rv.should == :stuff
27
+ rv = @model.method_to_cache :bar
28
+ rv.should == :other_stuff
29
+
30
+ model_base_index = "Benny/ModelCacheFake/1/method/method_to_cache"
31
+ rv = BennyCache::Config.store.read(model_base_index)
32
+ rv.class.should == Array
33
+ rv.size.should == 2
34
+
35
+ rv[0].should =~ /Benny\/ModelCacheFake\/1\/method\/method_to_cache\/args\/\w+$/
36
+ rv[1].should =~ /Benny\/ModelCacheFake\/1\/method\/method_to_cache\/args\/\w+$/
37
+
38
+ rv[0].should_not == rv[1]
39
+
40
+ puts rv.inspect
41
+ end
42
+
43
+ it "should method cache result data structures locally" do
44
+ data = @model.method_to_cache_with_base_data
45
+ data.should == [:a, :b, :c]
46
+ data.push :d
47
+
48
+ @model.method_to_cache_with_base_data.should == [:a, :b, :c, :d]
49
+
50
+ end
51
+
52
+
53
+ it "should be able to delete the cached method data" do
54
+ @model.expects(:method_to_cache_without_benny_cache).with(:foo).returns(:stuff) # only once!
55
+
56
+ rv = @model.method_to_cache :foo
57
+ rv.should == :stuff
58
+ rv = @model.method_to_cache :foo # should hit cache
59
+ rv.should == :stuff
60
+ model_base_index = "Benny/ModelCacheFake/1/method/method_to_cache"
61
+ rv = BennyCache::Config.store.read(model_base_index)
62
+ rv.size.should == 1
63
+
64
+ ModelCacheFake.benny_method_store_method_args_indexes_delete(@model.id, :method_to_cache)
65
+
66
+ model_base_index = "Benny/ModelCacheFake/1/method/method_to_cache"
67
+ rv = BennyCache::Config.store.read(model_base_index)
68
+ rv.should be_nil
69
+ end
70
+
71
+ it "should use the same cache index for reorder hash params" do
72
+ @model.expects(:method_to_cache_without_benny_cache).returns(:stuff) # only once!
73
+
74
+ rv = @model.method_to_cache :foo => :bar, :baz => :bin
75
+ rv.should == :stuff
76
+ rv = @model.method_to_cache :baz => :bin, :foo => :bar # should use same key
77
+ rv.should == :stuff
78
+ model_base_index = "Benny/ModelCacheFake/1/method/method_to_cache"
79
+ rv = BennyCache::Config.store.read(model_base_index)
80
+ rv.size.should == 1
81
+ end
82
+
83
+ end
84
+
85
+ end