ngmoco-cache-money 0.2.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,233 @@
1
+ # require 'memcache'
2
+ # require 'memcached'
3
+
4
+ #typically MemCache/Memcached can be used via the following in rails/init.rb:
5
+ #$memcache = MemCache.new(memcache_config[:servers].gsub(' ', '').split(','), memcache_config)
6
+ #$memcache = Memcached::Rails.new(memcache_config[:servers].gsub(' ', '').split(','), memcache_config)
7
+
8
+ #this wrapper lets both work.
9
+
10
+ ####### they have MemCache installed (don't need the wrapper)
11
+ if defined? MemCache
12
+
13
+ Rails.logger.info("cache-money: MemCache installed")
14
+ #TODO add logging?
15
+ class MemcachedWrapper < ::MemCache
16
+ end
17
+
18
+ ########## they have Memcached installed (do need the wrapper)
19
+ elsif defined? Memcached
20
+ Rails.logger.info("cache-money: Memcached installed")
21
+
22
+ class Memcached
23
+ alias :get_multi :get #:nodoc:
24
+ end
25
+
26
+ class MemcachedWrapper < ::Memcached
27
+ DEFAULTS = { :servers => '127.0.0.1:11211' }
28
+
29
+ attr_reader :logger
30
+
31
+ # See Memcached#new for details.
32
+ def initialize(*args)
33
+ opts = DEFAULTS.merge(args.last.is_a?(Hash) ? args.pop : {})
34
+
35
+ if opts.respond_to?(:symbolize_keys!)
36
+ opts.symbolize_keys!
37
+ else
38
+ opts = symbolize_keys(opts)
39
+ end
40
+
41
+ servers = Array(
42
+ args.any? ? args.unshift : opts.delete(:servers)
43
+ ).flatten.compact
44
+
45
+ opts[:prefix_key] ||= "#{opts[:namespace]}:"
46
+
47
+ @logger = opts[:logger]
48
+ @debug = opts[:debug]
49
+
50
+ super(servers, opts)
51
+ end
52
+
53
+ def symbolize_keys(opts)
54
+ # Destructively convert all keys to symbols.
55
+ if opts.kind_of?(Hash) && !opts.kind_of?(HashWithIndifferentAccess)
56
+ opts.keys.each do |key|
57
+ unless key.is_a?(Symbol)
58
+ opts[key.to_sym] = opts[key]
59
+ opts.delete(key)
60
+ end
61
+ end
62
+ end
63
+ opts
64
+ end
65
+
66
+ def namespace
67
+ options[:prefix_key]
68
+ end
69
+
70
+ # Wraps Memcached::Rails#add to return a text string - for cache money
71
+ def add(key, value, ttl=@default_ttl, raw=false)
72
+ super(key, value, ttl, !raw)
73
+ stored
74
+ rescue Memcached::NotStored
75
+ not_stored
76
+ rescue Memcached::Error
77
+ log_error($!)
78
+ log_error($!) if logger
79
+ not_stored
80
+ end
81
+
82
+ def replace(key, value, ttl = @default_ttl, raw = false)
83
+ super(key, value, ttl, !raw)
84
+ stored
85
+ rescue Memcached::NotStored
86
+ not_stored
87
+ rescue Memcached::Error
88
+ log_error($!) if logger
89
+ not_stored
90
+ end
91
+
92
+ # Wraps Memcached#get so that it doesn't raise. This has the side-effect of preventing you from
93
+ # storing <tt>nil</tt> values.
94
+ def get(key, raw=false)
95
+ logger.debug("Memcached get: #{key.inspect}") if logger && @debug
96
+ value = super(key, !raw)
97
+ logger.debug("Memcached hit: #{key.inspect}") if logger && @debug
98
+ value
99
+ rescue Memcached::NotFound
100
+ logger.debug("Memcached miss: #{key.inspect}") if logger && @debug
101
+ nil
102
+ rescue TypeError
103
+ log_error($!) if logger
104
+ delete(key)
105
+ logger.debug("Memcached deleted: #{key.inspect}") if logger && @debug
106
+ nil
107
+ rescue Memcached::Error
108
+ log_error($!) if logger
109
+ nil
110
+ end
111
+
112
+ def fetch(key, expiry = 0, raw = false)
113
+ value = get(key, !raw)
114
+
115
+ if value.nil? && block_given?
116
+ value = yield
117
+ add(key, value, expiry, !raw)
118
+ end
119
+
120
+ value
121
+ end
122
+
123
+ # Wraps Memcached#cas so that it doesn't raise. Doesn't set anything if no value is present.
124
+ def cas(key, ttl=@default_ttl, raw=false, &block)
125
+ super(key, ttl, !raw, &block)
126
+ stored
127
+ rescue Memcached::NotFound
128
+ rescue TypeError
129
+ log_error($!) if logger
130
+ delete(key)
131
+ logger.debug("Memcached deleted: #{key.inspect}") if logger && @debug
132
+ rescue Memcached::Error
133
+ if $!.is_a?(Memcached::ClientError)
134
+ raise $!
135
+ end
136
+ log_error($!) if logger
137
+ end
138
+
139
+ def get_multi(*keys)
140
+ keys.flatten!
141
+ super(keys, true)
142
+ rescue TypeError
143
+ log_error($!) if logger
144
+ keys.each { |key| delete(key) }
145
+ logger.debug("Memcached deleted: #{keys.inspect}") if logger && @debug
146
+ {}
147
+ rescue Memcached::Error
148
+ log_error($!) if logger
149
+ {}
150
+ end
151
+
152
+ def set(key, value, ttl=@default_ttl, raw=false)
153
+ super(key, value, ttl, !raw)
154
+ stored
155
+ rescue Memcached::Error
156
+ log_error($!) if logger
157
+ not_stored
158
+ end
159
+
160
+ def append(key, value)
161
+ super(key, value)
162
+ stored
163
+ rescue Memcached::NotStored
164
+ not_stored
165
+ rescue Memcached::Error
166
+ log_error($!) if logger
167
+ end
168
+
169
+ def prepend(key, value)
170
+ super(key, value)
171
+ stored
172
+ rescue Memcached::NotStored
173
+ not_stored
174
+ rescue Memcached::Error
175
+ log_error($!) if logger
176
+ end
177
+
178
+ def delete(key)
179
+ super(key)
180
+ deleted
181
+ rescue Memcached::NotFound
182
+ not_found
183
+ rescue Memcached::Error
184
+ log_error($!) if logger
185
+ end
186
+
187
+ def incr(*args)
188
+ super
189
+ rescue Memcached::NotFound
190
+ rescue Memcached::Error
191
+ log_error($!) if logger
192
+ end
193
+
194
+ def decr(*args)
195
+ super
196
+ rescue Memcached::NotFound
197
+ rescue Memcached::Error
198
+ log_error($!) if logger
199
+ end
200
+
201
+ alias :reset :quit
202
+ alias :close :quit #nodoc
203
+ alias :flush_all :flush
204
+ alias :compare_and_swap :cas
205
+ alias :"[]" :get
206
+ alias :"[]=" :set
207
+
208
+ private
209
+
210
+ def stored
211
+ "STORED\r\n"
212
+ end
213
+
214
+ def deleted
215
+ "DELETED\r\n"
216
+ end
217
+
218
+ def not_stored
219
+ "NOT_STORED\r\n"
220
+ end
221
+
222
+ def not_found
223
+ "NOT_FOUND\r\n"
224
+ end
225
+
226
+ def log_error(err)
227
+ logger.error("#{err}: \n\t#{err.backtrace.join("\n\t")}") if logger
228
+ end
229
+
230
+ end
231
+ else
232
+ Rails.logger.warn 'unable to determine memcache implementation'
233
+ end #include the wraper
@@ -0,0 +1,39 @@
1
+ yml = YAML.load(IO.read(File.join(RAILS_ROOT, "config", "memcached.yml")))
2
+ memcache_config = yml[RAILS_ENV]
3
+ memcache_config.symbolize_keys! if memcache_config.respond_to?(:symbolize_keys!)
4
+
5
+ if defined?(DISABLE_CACHE_MONEY) || memcache_config.nil? || memcache_config[:cache_money] != true
6
+ Rails.logger.info 'cache-money disabled'
7
+ class ActiveRecord::Base
8
+ def self.index(*args)
9
+ end
10
+ end
11
+ else
12
+ Rails.logger.info 'cache-money enabled'
13
+ require 'cache_money'
14
+
15
+ memcache_config[:logger] = Rails.logger
16
+ $memcache = MemcachedWrapper.new(memcache_config[:servers].gsub(' ', '').split(','), memcache_config)
17
+
18
+ #ActionController::Base.cache_store = :cache_money_mem_cache_store
19
+ ActionController::Base.session_options[:cache] = $memcache if memcache_config[:sessions]
20
+ #silence_warnings {
21
+ # Object.const_set "RAILS_CACHE", ActiveSupport::Cache.lookup_store(:cache_money_mem_cache_store)
22
+ #}
23
+
24
+ $local = Cash::Local.new($memcache)
25
+ $lock = Cash::Lock.new($memcache)
26
+ $cache = Cash::Transactional.new($local, $lock)
27
+
28
+ class ActiveRecord::Base
29
+ is_cached(:repository => $cache)
30
+
31
+ def <=>(other)
32
+ if self.id == other.id then
33
+ 0
34
+ else
35
+ self.id < other.id ? -1 : 1
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,174 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ module Cash
4
+ describe Accessor do
5
+ describe '#fetch' do
6
+ describe '#fetch("...")' do
7
+ describe 'when there is a cache miss' do
8
+ it 'returns nil' do
9
+ Story.fetch("yabba").should be_nil
10
+ end
11
+ end
12
+
13
+ describe 'when there is a cache hit' do
14
+ it 'returns the value of the cache' do
15
+ Story.set("yabba", "dabba")
16
+ Story.fetch("yabba").should == "dabba"
17
+ end
18
+ end
19
+ end
20
+
21
+ describe '#fetch([...])', :shared => true do
22
+ describe '#fetch([])' do
23
+ it 'returns the empty hash' do
24
+ Story.fetch([]).should == {}
25
+ end
26
+ end
27
+
28
+ describe 'when there is a total cache miss' do
29
+ it 'yields the keys to the block' do
30
+ Story.fetch(["yabba", "dabba"]) { |*missing_ids| ["doo", "doo"] }.should == {
31
+ "Story:1/yabba" => "doo",
32
+ "Story:1/dabba" => "doo"
33
+ }
34
+ end
35
+ end
36
+
37
+ describe 'when there is a partial cache miss' do
38
+ it 'yields just the missing ids to the block' do
39
+ Story.set("yabba", "dabba")
40
+ Story.fetch(["yabba", "dabba"]) { |*missing_ids| "doo" }.should == {
41
+ "Story:1/yabba" => "dabba",
42
+ "Story:1/dabba" => "doo"
43
+ }
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ describe '#get' do
50
+ describe '#get("...")' do
51
+ describe 'when there is a cache miss' do
52
+ it 'returns the value of the block' do
53
+ Story.get("yabba") { "dabba" }.should == "dabba"
54
+ end
55
+
56
+ it 'adds to the cache' do
57
+ Story.get("yabba") { "dabba" }
58
+ Story.get("yabba").should == "dabba"
59
+ end
60
+ end
61
+
62
+ describe 'when there is a cache hit' do
63
+ before do
64
+ Story.set("yabba", "dabba")
65
+ end
66
+
67
+ it 'returns the value of the cache' do
68
+ Story.get("yabba") { "doo" }.should == "dabba"
69
+ end
70
+
71
+ it 'does nothing to the cache' do
72
+ Story.get("yabba") { "doo" }
73
+ Story.get("yabba").should == "dabba"
74
+ end
75
+ end
76
+ end
77
+
78
+ describe '#get([...])' do
79
+ it_should_behave_like "#fetch([...])"
80
+ end
81
+ end
82
+
83
+ describe '#incr' do
84
+ describe 'when there is a cache hit' do
85
+ before do
86
+ Story.set("count", 0)
87
+ end
88
+
89
+ it 'increments the value of the cache' do
90
+ Story.incr("count", 2)
91
+ Story.get("count", :raw => true).should =~ /2/
92
+ end
93
+
94
+ it 'returns the new cache value' do
95
+ Story.incr("count", 2).should == 2
96
+ end
97
+ end
98
+
99
+ describe 'when there is a cache miss' do
100
+ it 'initializes the value of the cache to the value of the block' do
101
+ Story.incr("count", 1) { 5 }
102
+ Story.get("count", :raw => true).should =~ /5/
103
+ end
104
+
105
+ it 'returns the new cache value' do
106
+ Story.incr("count", 1) { 2 }.should == 2
107
+ end
108
+ end
109
+ end
110
+
111
+ describe '#add' do
112
+ describe 'when the value already exists' do
113
+ describe 'when a block is given' do
114
+ it 'yields to the block' do
115
+ Story.set("count", 1)
116
+ Story.add("count", 1) { "yield me" }.should == "yield me"
117
+ end
118
+ end
119
+
120
+ describe 'when no block is given' do
121
+ it 'does not error' do
122
+ Story.set("count", 1)
123
+ lambda { Story.add("count", 1) }.should_not raise_error
124
+ end
125
+ end
126
+ end
127
+
128
+ describe 'when the value does not already exist' do
129
+ it 'adds the key to the cache' do
130
+ Story.add("count", 1)
131
+ Story.get("count").should == 1
132
+ end
133
+ end
134
+ end
135
+
136
+ describe '#decr' do
137
+ describe 'when there is a cache hit' do
138
+ before do
139
+ Story.incr("count", 1) { 10 }
140
+ end
141
+
142
+ it 'decrements the value of the cache' do
143
+ Story.decr("count", 2)
144
+ Story.get("count", :raw => true).should =~ /8/
145
+ end
146
+
147
+ it 'returns the new cache value' do
148
+ Story.decr("count", 2).should == 8
149
+ end
150
+ end
151
+
152
+ describe 'when there is a cache miss' do
153
+ it 'initializes the value of the cache to the value of the block' do
154
+ Story.decr("count", 1) { 5 }
155
+ Story.get("count", :raw => true).should =~ /5/
156
+ end
157
+
158
+ it 'returns the new cache value' do
159
+ Story.decr("count", 1) { 2 }.should == 2
160
+ end
161
+ end
162
+ end
163
+
164
+ describe '#cache_key' do
165
+ it 'uses the version number' do
166
+ Story.version 1
167
+ Story.cache_key("foo").should == "Story:1/foo"
168
+
169
+ Story.version 2
170
+ Story.cache_key("foo").should == "Story:2/foo"
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,211 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ module Cash
4
+ describe Finders do
5
+ describe 'when the cache is populated' do
6
+ describe "#find" do
7
+ describe '#find(id...)' do
8
+ describe '#find(id)' do
9
+ it "returns an active record" do
10
+ story = Story.create!(:title => 'a story')
11
+ Story.find(story.id).should == story
12
+ end
13
+ end
14
+
15
+ describe 'when the object is destroyed' do
16
+ describe '#find(id)' do
17
+ it "raises an error" do
18
+ story = Story.create!(:title => "I am delicious")
19
+ story.destroy
20
+ lambda { Story.find(story.id) }.should raise_error(ActiveRecord::RecordNotFound)
21
+ end
22
+ end
23
+ end
24
+
25
+ describe '#find(id1, id2, ...)' do
26
+ it "returns an array" do
27
+ story1, story2 = Story.create!, Story.create!
28
+ Story.find(story1.id, story2.id).should == [story1, story2]
29
+ end
30
+
31
+ describe "#find(id, nil)" do
32
+ it "ignores the nils" do
33
+ story = Story.create!
34
+ Story.find(story.id, nil).should == story
35
+ end
36
+ end
37
+ end
38
+
39
+ describe 'when given nonexistent ids' do
40
+ describe 'when given one nonexistent id' do
41
+ it 'raises an error' do
42
+ lambda { Story.find(1) }.should raise_error(ActiveRecord::RecordNotFound)
43
+ end
44
+ end
45
+
46
+ describe 'when given multiple nonexistent ids' do
47
+ it "raises an error" do
48
+ lambda { Story.find(1, 2, 3) }.should raise_error(ActiveRecord::RecordNotFound)
49
+ end
50
+ end
51
+
52
+
53
+ describe '#find(nil)' do
54
+ it 'raises an error' do
55
+ lambda { Story.find(nil) }.should raise_error(ActiveRecord::RecordNotFound)
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ describe '#find(object)' do
62
+ it "coerces arguments to integers" do
63
+ story = Story.create!
64
+ Story.find(story.id.to_s).should == story
65
+ end
66
+ end
67
+
68
+ describe '#find([...])' do
69
+ describe 'when given an array with valid ids' do
70
+ it "#finds the object with that id" do
71
+ story = Story.create!
72
+ Story.find([story.id]).should == [story]
73
+ end
74
+ end
75
+
76
+ describe '#find([])' do
77
+ it 'returns the empty array' do
78
+ Story.find([]).should == []
79
+ end
80
+ end
81
+
82
+ describe 'when given nonexistent ids' do
83
+ it 'raises an error' do
84
+ lambda { Story.find([1, 2, 3]) }.should raise_error(ActiveRecord::RecordNotFound)
85
+ end
86
+ end
87
+
88
+ describe 'when given limits and offsets' do
89
+ describe '#find([1, 2, ...], :limit => ..., :offset => ...)' do
90
+ it "returns the correct slice of objects" do
91
+ character1 = Character.create!(:name => "Sam", :story_id => 1)
92
+ character2 = Character.create!(:name => "Sam", :story_id => 1)
93
+ character3 = Character.create!(:name => "Sam", :story_id => 1)
94
+ Character.find(
95
+ [character1.id, character2.id, character3.id],
96
+ :conditions => { :name => "Sam", :story_id => 1 }, :limit => 2
97
+ ).should == [character1, character2]
98
+ end
99
+ end
100
+
101
+ describe '#find([1], :limit => 0)' do
102
+ it "raises an error" do
103
+ character = Character.create!(:name => "Sam", :story_id => 1)
104
+ lambda do
105
+ Character.find([character.id], :conditions => { :name => "Sam", :story_id => 1 }, :limit => 0)
106
+ end.should raise_error(ActiveRecord::RecordNotFound)
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ describe '#find(:first, ...)' do
113
+ describe '#find(:first, ..., :offset => ...)' do
114
+ it "#finds the object in the correct order" do
115
+ story1 = Story.create!(:title => 'title1')
116
+ story2 = Story.create!(:title => story1.title)
117
+ Story.find(:first, :conditions => { :title => story1.title }, :offset => 1).should == story2
118
+ end
119
+ end
120
+
121
+ describe '#find(:first, :conditions => [])' do
122
+ it 'finds the object in the correct order' do
123
+ story = Story.create!
124
+ Story.find(:first, :conditions => []).should == story
125
+ end
126
+ end
127
+
128
+ describe "#find(:first, :conditions => '...')" do
129
+ it "coerces ruby values to the appropriate database values" do
130
+ story1 = Story.create! :title => 'a story', :published => true
131
+ story2 = Story.create! :title => 'another story', :published => false
132
+ Story.find(:first, :conditions => 'published = 0').should == story2
133
+ end
134
+ end
135
+ end
136
+ end
137
+
138
+ describe '#find_by_attr' do
139
+ describe '#find_by_attr(nil)' do
140
+ it 'returns nil' do
141
+ Story.find_by_id(nil).should == nil
142
+ end
143
+ end
144
+
145
+ describe 'when given non-existent ids' do
146
+ it 'returns nil' do
147
+ Story.find_by_id(-1).should == nil
148
+ end
149
+ end
150
+ end
151
+
152
+ describe '#find_all_by_attr' do
153
+ describe 'when given non-existent ids' do
154
+ it "does not raise an error" do
155
+ lambda { Story.find_all_by_id([-1, -2, -3]) }.should_not raise_error
156
+ end
157
+ end
158
+ end
159
+ end
160
+
161
+ describe 'when the cache is partially populated' do
162
+ describe '#find(:all, :conditions => ...)' do
163
+ it "returns the correct records" do
164
+ story1 = Story.create!(:title => title = 'once upon a time...')
165
+ $memcache.flush_all
166
+ story2 = Story.create!(:title => title)
167
+ Story.find(:all, :conditions => { :title => story1.title }).should == [story1, story2]
168
+ end
169
+ end
170
+
171
+ describe '#find(id1, id2, ...)' do
172
+ it "returns the correct records" do
173
+ story1 = Story.create!(:title => 'story 1')
174
+ $memcache.flush_all
175
+ story2 = Story.create!(:title => 'story 2')
176
+ Story.find(story1.id, story2.id).should == [story1, story2]
177
+ end
178
+ end
179
+ end
180
+
181
+ describe 'when the cache is not populated' do
182
+ describe '#find(id)' do
183
+ it "returns the correct records" do
184
+ story = Story.create!(:title => 'a story')
185
+ $memcache.flush_all
186
+ Story.find(story.id).should == story
187
+ end
188
+
189
+ it "handles after_find on model" do
190
+ class AfterFindStory < Story
191
+ def after_find
192
+ self.title
193
+ end
194
+ end
195
+ lambda do
196
+ AfterFindStory.create!(:title => 'a story')
197
+ end.should_not raise_error(ActiveRecord::MissingAttributeError)
198
+ end
199
+ end
200
+
201
+ describe '#find(id1, id2, ...)' do
202
+ it "handles finds with multiple ids correctly" do
203
+ story1 = Story.create!
204
+ story2 = Story.create!
205
+ $memcache.flush_all
206
+ Story.find(story1.id, story2.id).should == [story1, story2]
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end