benny_cache 0.0.1

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