ngmoco-cache-money 0.2.9

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