factorylabs-cache-money 0.2.5

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,42 @@
1
+ module Cash
2
+ class Transactional
3
+ attr_reader :memcache
4
+
5
+ def initialize(memcache, lock)
6
+ @memcache, @cache = [memcache, memcache]
7
+ @lock = lock
8
+ end
9
+
10
+ def transaction
11
+ exception_was_raised = false
12
+ begin_transaction
13
+ result = yield
14
+ rescue Object => e
15
+ exception_was_raised = true
16
+ raise
17
+ ensure
18
+ begin
19
+ @cache.flush unless exception_was_raised
20
+ ensure
21
+ end_transaction
22
+ end
23
+ end
24
+
25
+ def method_missing(method, *args, &block)
26
+ @cache.send(method, *args, &block)
27
+ end
28
+
29
+ def respond_to?(method)
30
+ @cache.respond_to?(method)
31
+ end
32
+
33
+ private
34
+ def begin_transaction
35
+ @cache = Buffered.push(@cache, @lock)
36
+ end
37
+
38
+ def end_transaction
39
+ @cache = @cache.pop
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,9 @@
1
+ class Array
2
+ alias_method :count, :size
3
+
4
+ def to_hash
5
+ keys_and_values_without_nils = reject { |key, value| value.nil? }
6
+ shallow_flattened_keys_and_values_without_nils = keys_and_values_without_nils.inject([]) { |result, pair| result += pair }
7
+ Hash[*shallow_flattened_keys_and_values_without_nils]
8
+ end
9
+ end
@@ -0,0 +1,72 @@
1
+ module Cash
2
+ module WriteThrough
3
+ DEFAULT_TTL = 12.hours
4
+
5
+ def self.included(active_record_class)
6
+ active_record_class.class_eval do
7
+ include InstanceMethods
8
+ extend ClassMethods
9
+ end
10
+ end
11
+
12
+ module InstanceMethods
13
+ def self.included(active_record_class)
14
+ active_record_class.class_eval do
15
+ after_create :add_to_caches
16
+ after_update :update_caches
17
+ after_destroy :remove_from_caches
18
+ end
19
+ end
20
+
21
+ def add_to_caches
22
+ InstanceMethods.unfold(self.class, :add_to_caches, self)
23
+ end
24
+
25
+ def update_caches
26
+ InstanceMethods.unfold(self.class, :update_caches, self)
27
+ end
28
+
29
+ def remove_from_caches
30
+ return if new_record?
31
+ InstanceMethods.unfold(self.class, :remove_from_caches, self)
32
+ end
33
+
34
+ def expire_caches
35
+ InstanceMethods.unfold(self.class, :expire_caches, self)
36
+ end
37
+
38
+ def shallow_clone
39
+ clone = self.class.new
40
+ clone.instance_variable_set("@attributes", instance_variable_get(:@attributes))
41
+ clone.instance_variable_set("@new_record", new_record?)
42
+ clone
43
+ end
44
+
45
+ private
46
+ def self.unfold(klass, operation, object)
47
+ while klass < ActiveRecord::Base && klass.ancestors.include?(WriteThrough)
48
+ klass.send(operation, object)
49
+ klass = klass.superclass
50
+ end
51
+ end
52
+ end
53
+
54
+ module ClassMethods
55
+ def add_to_caches(object)
56
+ indices.each { |index| index.add(object) }
57
+ end
58
+
59
+ def update_caches(object)
60
+ indices.each { |index| index.update(object) }
61
+ end
62
+
63
+ def remove_from_caches(object)
64
+ indices.each { |index| index.remove(object) }
65
+ end
66
+
67
+ def expire_caches(object)
68
+ indices.each { |index| index.delete(object) }
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,167 @@
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
+
20
+ describe "when the key size is very large" do
21
+ it "should hash the key to fit" do
22
+ k = "a" * 1024
23
+ Story.set(k, "foo")
24
+ Story.fetch(k).should == "foo"
25
+ end
26
+ end
27
+ end
28
+
29
+ describe '#fetch([...])', :shared => true do
30
+ describe 'when there is a total cache miss' do
31
+ it 'yields the keys to the block' do
32
+ Story.fetch(["yabba", "dabba"]) { |*missing_ids| ["doo", "doo"] }.should == {
33
+ "Story:1/yabba" => "doo",
34
+ "Story:1/dabba" => "doo"
35
+ }
36
+ end
37
+ end
38
+
39
+ describe 'when there is a partial cache miss' do
40
+ it 'yields just the missing ids to the block' do
41
+ Story.set("yabba", "dabba")
42
+ Story.fetch(["yabba", "dabba"]) { |*missing_ids| "doo" }.should == {
43
+ "Story:1/yabba" => "dabba",
44
+ "Story:1/dabba" => "doo"
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
50
+
51
+ describe '#get' do
52
+ describe '#get("...")' do
53
+ describe 'when there is a cache miss' do
54
+ it 'returns the value of the block' do
55
+ Story.get("yabba") { "dabba" }.should == "dabba"
56
+ end
57
+
58
+ it 'adds to the cache' do
59
+ Story.get("yabba") { "dabba" }
60
+ Story.get("yabba").should == "dabba"
61
+ end
62
+ end
63
+
64
+ describe 'when there is a cache hit' do
65
+ before do
66
+ Story.set("yabba", "dabba")
67
+ end
68
+
69
+ it 'returns the value of the cache' do
70
+ Story.get("yabba") { "doo" }.should == "dabba"
71
+ end
72
+
73
+ it 'does nothing to the cache' do
74
+ Story.get("yabba") { "doo" }
75
+ Story.get("yabba").should == "dabba"
76
+ end
77
+ end
78
+ end
79
+
80
+ describe '#get([...])' do
81
+ it_should_behave_like "#fetch([...])"
82
+ end
83
+ end
84
+
85
+ describe '#incr' do
86
+ describe 'when there is a cache hit' do
87
+ before do
88
+ Story.set("count", 0)
89
+ end
90
+
91
+ it 'increments the value of the cache' do
92
+ Story.incr("count", 2)
93
+ Story.get("count", :raw => true).should =~ /2/
94
+ end
95
+
96
+ it 'returns the new cache value' do
97
+ Story.incr("count", 2).should == 2
98
+ end
99
+ end
100
+
101
+ describe 'when there is a cache miss' do
102
+ it 'initializes the value of the cache to the value of the block' do
103
+ Story.incr("count", 1) { 5 }
104
+ Story.get("count", :raw => true).should =~ /5/
105
+ end
106
+
107
+ it 'returns the new cache value' do
108
+ Story.incr("count", 1) { 2 }.should == 2
109
+ end
110
+ end
111
+ end
112
+
113
+ describe '#add' do
114
+ describe 'when the value already exists' do
115
+ it 'yields to the block' do
116
+ Story.set("count", 1)
117
+ Story.add("count", 1) { "yield me" }.should == "yield me"
118
+ end
119
+ end
120
+
121
+ describe 'when the value does not already exist' do
122
+ it 'adds the key to the cache' do
123
+ Story.add("count", 1)
124
+ Story.get("count").should == 1
125
+ end
126
+ end
127
+ end
128
+
129
+ describe '#decr' do
130
+ describe 'when there is a cache hit' do
131
+ before do
132
+ Story.incr("count", 1) { 10 }
133
+ end
134
+
135
+ it 'decrements the value of the cache' do
136
+ Story.decr("count", 2)
137
+ Story.get("count", :raw => true).should =~ /8/
138
+ end
139
+
140
+ it 'returns the new cache value' do
141
+ Story.decr("count", 2).should == 8
142
+ end
143
+ end
144
+
145
+ describe 'when there is a cache miss' do
146
+ it 'initializes the value of the cache to the value of the block' do
147
+ Story.decr("count", 1) { 5 }
148
+ Story.get("count", :raw => true).should =~ /5/
149
+ end
150
+
151
+ it 'returns the new cache value' do
152
+ Story.decr("count", 1) { 2 }.should == 2
153
+ end
154
+ end
155
+ end
156
+
157
+ describe '#cache_key' do
158
+ it 'uses the version number' do
159
+ Story.version 1
160
+ Story.cache_key("foo").should == "Story:1/foo"
161
+
162
+ Story.version 2
163
+ Story.cache_key("foo").should == "Story:2/foo"
164
+ end
165
+ end
166
+ end
167
+ end
@@ -0,0 +1,221 @@
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
+
38
+ describe "#find(id,id,id..)" do
39
+ it "accepts a larger than normal set of ids" do
40
+ st = []
41
+ 1.upto(250) do |i|
42
+ st << Story.create!
43
+ end
44
+ ids = st.collect(&:id)
45
+ Story.find(ids).should == st
46
+ end
47
+ end
48
+ end
49
+
50
+ describe 'when given nonexistent ids' do
51
+ describe 'when given one nonexistent id' do
52
+ it 'raises an error' do
53
+ lambda { Story.find(1) }.should raise_error(ActiveRecord::RecordNotFound)
54
+ end
55
+ end
56
+
57
+ describe 'when given multiple nonexistent ids' do
58
+ it "raises an error" do
59
+ lambda { Story.find(1, 2, 3) }.should raise_error(ActiveRecord::RecordNotFound)
60
+ end
61
+ end
62
+
63
+
64
+ describe '#find(nil)' do
65
+ it 'raises an error' do
66
+ lambda { Story.find(nil) }.should raise_error(ActiveRecord::RecordNotFound)
67
+ end
68
+ end
69
+ end
70
+ end
71
+
72
+ describe '#find(object)' do
73
+ it "coerces arguments to integers" do
74
+ story = Story.create!
75
+ Story.find(story.id.to_s).should == story
76
+ end
77
+ end
78
+
79
+ describe '#find([...])' do
80
+ describe 'when given an array with valid ids' do
81
+ it "#finds the object with that id" do
82
+ story = Story.create!
83
+ Story.find([story.id]).should == [story]
84
+ end
85
+ end
86
+
87
+ describe "when given a large array of ids" do
88
+ it "#finds all objects with those ids" do
89
+ st = []
90
+ 1.upto(250) do |i|
91
+ st << Story.create!
92
+ end
93
+ ids = st.collect(&:id)
94
+ Story.find(:all, :conditions => { :id => ids }).should == st
95
+ end
96
+ end
97
+
98
+ describe '#find([])' do
99
+ it 'returns the empty array' do
100
+ Story.find([]).should == []
101
+ end
102
+ end
103
+
104
+ describe 'when given nonexistent ids' do
105
+ it 'raises an error' do
106
+ lambda { Story.find([1, 2, 3]) }.should raise_error(ActiveRecord::RecordNotFound)
107
+ end
108
+ end
109
+
110
+ describe 'when given limits and offsets' do
111
+ describe '#find([1, 2, ...], :limit => ..., :offset => ...)' do
112
+ it "returns the correct slice of objects" do
113
+ character1 = Character.create!(:name => "Sam", :story_id => 1)
114
+ character2 = Character.create!(:name => "Sam", :story_id => 1)
115
+ character3 = Character.create!(:name => "Sam", :story_id => 1)
116
+ Character.find(
117
+ [character1.id, character2.id, character3.id],
118
+ :conditions => { :name => "Sam", :story_id => 1 }, :limit => 2
119
+ ).should == [character1, character2]
120
+ end
121
+ end
122
+
123
+ describe '#find([1], :limit => 0)' do
124
+ it "raises an error" do
125
+ character = Character.create!(:name => "Sam", :story_id => 1)
126
+ lambda do
127
+ Character.find([character.id], :conditions => { :name => "Sam", :story_id => 1 }, :limit => 0)
128
+ end.should raise_error(ActiveRecord::RecordNotFound)
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ describe '#find(:first, ..., :offset => ...)' do
135
+ it "#finds the object in the correct order" do
136
+ story1 = Story.create!(:title => 'title1')
137
+ story2 = Story.create!(:title => story1.title)
138
+ Story.find(:first, :conditions => { :title => story1.title }, :offset => 1).should == story2
139
+ end
140
+ end
141
+
142
+ describe '#find(:first, :conditions => [])' do
143
+ it 'works' do
144
+ story = Story.create!
145
+ Story.find(:first, :conditions => []).should == story
146
+ end
147
+ end
148
+
149
+ describe "#find(:first, :conditions => '...')" do
150
+ it "uses the active record instance to typecast values extracted from the conditions" do
151
+ story1 = Story.create! :title => 'a story', :published => true
152
+ story2 = Story.create! :title => 'another story', :published => false
153
+ Story.get('published/false').should == [story2.id]
154
+ Story.find(:first, :conditions => 'published = 0').should == story2
155
+ end
156
+ end
157
+ end
158
+
159
+ describe '#find_by_attr' do
160
+ describe '#find_by_attr(nil)' do
161
+ it 'returns nil' do
162
+ Story.find_by_id(nil).should == nil
163
+ end
164
+ end
165
+
166
+ describe 'when given non-existent ids' do
167
+ it 'returns nil' do
168
+ Story.find_by_id(-1).should == nil
169
+ end
170
+ end
171
+ end
172
+
173
+ describe '#find_all_by_attr' do
174
+ describe 'when given non-existent ids' do
175
+ it "does not raise an error" do
176
+ lambda { Story.find_all_by_id([-1, -2, -3]) }.should_not raise_error
177
+ end
178
+ end
179
+ end
180
+ end
181
+
182
+ describe 'when the cache is partially populated' do
183
+ describe '#find(:all, :conditions => ...)' do
184
+ it "returns the correct records" do
185
+ story1 = Story.create!(:title => title = 'once upon a time...')
186
+ $memcache.flush_all
187
+ story2 = Story.create!(:title => title)
188
+ Story.find(:all, :conditions => { :title => story1.title }).should == [story1, story2]
189
+ end
190
+ end
191
+
192
+ describe '#find(id1, id2, ...)' do
193
+ it "returns the correct records" do
194
+ story1 = Story.create!(:title => 'story 1')
195
+ $memcache.flush_all
196
+ story2 = Story.create!(:title => 'story 2')
197
+ Story.find(story1.id, story2.id).should == [story1, story2]
198
+ end
199
+ end
200
+ end
201
+
202
+ describe 'when the cache is not populated' do
203
+ describe '#find(id)' do
204
+ it "returns the correct records" do
205
+ story = Story.create!(:title => 'a story')
206
+ $memcache.flush_all
207
+ Story.find(story.id).should == story
208
+ end
209
+ end
210
+
211
+ describe '#find(id1, id2, ...)' do
212
+ it "handles finds with multiple ids correctly" do
213
+ story1 = Story.create!
214
+ story2 = Story.create!
215
+ $memcache.flush_all
216
+ Story.find(story1.id, story2.id).should == [story1, story2]
217
+ end
218
+ end
219
+ end
220
+ end
221
+ end
@@ -0,0 +1,67 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ module Cash
4
+ describe Finders do
5
+ describe 'Calculations' do
6
+ describe 'when the cache is populated' do
7
+ before do
8
+ @stories = [Story.create!(:title => @title = 'asdf'), Story.create!(:title => @title)]
9
+ end
10
+
11
+ describe '#count(:all, :conditions => ...)' do
12
+ it "does not use the database" do
13
+ Story.count(:all, :conditions => { :title => @title }).should == @stories.size
14
+ end
15
+ end
16
+
17
+ describe '#count(:column, :conditions => ...)' do
18
+ it "uses the database, not the cache" do
19
+ mock(Story).get.never
20
+ Story.count(:title, :conditions => { :title => @title }).should == @stories.size
21
+ end
22
+ end
23
+
24
+ describe '#count(:all, :distinct => ..., :select => ...)' do
25
+ it 'uses the database, not the cache' do
26
+ mock(Story).get.never
27
+ Story.count(:all, :distinct => true, :select => :title, :conditions => { :title => @title }).should == @stories.collect(&:title).uniq.size
28
+ end
29
+ end
30
+
31
+ describe 'association proxies' do
32
+ describe '#count(:all, :conditions => ...)' do
33
+ it 'does not use the database' do
34
+ story = Story.create!
35
+ characters = [story.characters.create!(:name => name = 'name'), story.characters.create!(:name => name)]
36
+ mock(Story.connection).execute.never
37
+ story.characters.count(:all, :conditions => { :name => name }).should == characters.size
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ describe 'when the cache is not populated' do
44
+ describe '#count(:all, ...)' do
45
+ describe '#count(:all)' do
46
+ it 'uses the database, not the cache' do
47
+ mock(Story).get.never
48
+ Story.count
49
+ end
50
+ end
51
+
52
+ describe '#count(:all, :conditions => ...)' do
53
+ before do
54
+ Story.create!(:title => @title = 'title')
55
+ $memcache.flush_all
56
+ end
57
+
58
+ it "populates the count correctly" do
59
+ Story.count(:all, :conditions => { :title => @title }).should == 1
60
+ Story.fetch("title/#{@title}/count", :raw => true).should =~ /\s*1\s*/
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end