nkallen-cache-money 0.2.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,195 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ module Cash
4
+ describe 'Windows' do
5
+ LIMIT, BUFFER = 5, 2
6
+
7
+ before :suite do
8
+ Fable = Class.new(Story)
9
+ Fable.index :title, :limit => LIMIT, :buffer => BUFFER
10
+ end
11
+
12
+ describe '#find(...)' do
13
+ before do
14
+ @fables = []
15
+ 10.times { @fables << Fable.create!(:title => @title = 'title') }
16
+ end
17
+
18
+ describe 'when the cache is populated' do
19
+ describe "#find(:all, :conditions => ...)" do
20
+ it "uses the database, not the cache" do
21
+ mock(Fable).get.never
22
+ Fable.find(:all, :conditions => { :title => @title }).should == @fables
23
+ end
24
+ end
25
+
26
+ describe "#find(:all, :conditions => ..., :limit => ...) and query limit > index limit" do
27
+ it "uses the database, not the cache" do
28
+ mock(Fable).get.never
29
+ Fable.find(:all, :conditions => { :title => @title }, :limit => LIMIT + 1).should == @fables[0, LIMIT + 1]
30
+ end
31
+ end
32
+
33
+ describe "#find(:all, :conditions => ..., :limit => ..., :offset => ...) and query limit + offset > index limit" do
34
+ it "uses the database, not the cache" do
35
+ mock(Fable).get.never
36
+ Fable.find(:all, :conditions => { :title => @title }, :limit => 1, :offset => LIMIT).should == @fables[LIMIT, 1]
37
+ end
38
+ end
39
+
40
+ describe "#find(:all, :conditions => ..., :limit => ...) and query limit <= index limit" do
41
+ it "does not use the database" do
42
+ mock(Fable.connection).execute.never
43
+ Fable.find(:all, :conditions => { :title => @title }, :limit => LIMIT - 1).should == @fables[0, LIMIT - 1]
44
+ end
45
+ end
46
+ end
47
+
48
+ describe 'when the cache is not populated' do
49
+ describe "#find(:all, :conditions => ..., :limit => ...) and query limit <= index limit" do
50
+ describe 'when there are fewer than limit + buffer items' do
51
+ it "populates the cache with all items" do
52
+ Fable.find(:all, :limit => deleted = @fables.size - LIMIT - BUFFER + 1).collect(&:destroy)
53
+ $memcache.flush_all
54
+ Fable.find(:all, :conditions => { :title => @title }, :limit => LIMIT).should == @fables[deleted, LIMIT]
55
+ Fable.get("title/#{@title}").should == @fables[deleted, @fables.size - deleted].collect(&:id)
56
+ end
57
+ end
58
+
59
+ describe 'when there are more than limit + buffer items' do
60
+ it "populates the cache with limit + buffer items" do
61
+ $memcache.flush_all
62
+ Fable.find(:all, :conditions => { :title => @title }, :limit => 5).should == @fables[0, 5]
63
+ Fable.get("title/#{@title}").should == @fables[0, LIMIT + BUFFER].collect(&:id)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
69
+
70
+ describe '#create!' do
71
+ describe 'when the cache is populated' do
72
+ describe 'when the count of records in the database is > limit + buffer items' do
73
+ it 'truncates' do
74
+ fables, title = [], 'title'
75
+ (LIMIT + BUFFER).times { fables << Fable.create!(:title => title) }
76
+ Fable.get("title/#{title}").should == fables.collect(&:id)
77
+ Fable.create!(:title => title)
78
+ Fable.get("title/#{title}").should == fables.collect(&:id)
79
+ end
80
+ end
81
+
82
+ describe 'when the count of records in the database is < limit + buffer items' do
83
+ it 'appends to the list' do
84
+ fables, title = [], 'title'
85
+ (LIMIT + BUFFER - 1).times { fables << Fable.create!(:title => title) }
86
+ Fable.get("title/#{title}").should == fables.collect(&:id)
87
+ fable = Fable.create!(:title => title)
88
+ Fable.get("title/#{title}").should == (fables << fable).collect(&:id)
89
+ end
90
+ end
91
+ end
92
+
93
+ describe 'when the cache is not populated' do
94
+ describe 'when the count of records in the database is > limit + buffer items' do
95
+ it 'truncates the index' do
96
+ fables, title = [], 'title'
97
+ (LIMIT + BUFFER).times { fables << Fable.create!(:title => title) }
98
+ $memcache.flush_all
99
+ Fable.create!(:title => title)
100
+ Fable.get("title/#{title}").should == fables.collect(&:id)
101
+ end
102
+ end
103
+
104
+ describe 'when the count of records in the database is < limit + buffer items' do
105
+ it 'appends to the list' do
106
+ fables, title = [], 'title'
107
+ (LIMIT + BUFFER - 1).times { fables << Fable.create!(:title => title) }
108
+ $memcache.flush_all
109
+ fable = Fable.create!(:title => title)
110
+ Fable.get("title/#{title}").should == (fables << fable).collect(&:id)
111
+ end
112
+ end
113
+ end
114
+ end
115
+
116
+ describe '#destroy' do
117
+ describe 'when the cache is populated' do
118
+ describe 'when the index size is <= limit of items' do
119
+ describe 'when the count of records in the database is <= limit of items' do
120
+ it 'deletes from the list without refreshing from the database' do
121
+ fables, title = [], 'title'
122
+ LIMIT.times { fables << Fable.create!(:title => title) }
123
+ Fable.get("title/#{title}").size.should <= LIMIT
124
+
125
+ mock(Fable.connection).select.never
126
+ fables.shift.destroy
127
+ Fable.get("title/#{title}").should == fables.collect(&:id)
128
+ end
129
+ end
130
+
131
+ describe 'when the count of records in the database is >= limit of items' do
132
+ it 'refreshes the list (from the database)' do
133
+ fables, title = [], 'title'
134
+ (LIMIT + BUFFER + 1).times { fables << Fable.create!(:title => title) }
135
+ BUFFER.times { fables.shift.destroy }
136
+ Fable.get("title/#{title}").size.should == LIMIT
137
+
138
+ fables.shift.destroy
139
+ Fable.get("title/#{title}").should == fables.collect(&:id)
140
+
141
+ end
142
+ end
143
+ end
144
+
145
+ describe 'when the index size is > limit of items' do
146
+ it 'deletes from the list' do
147
+ fables, title = [], 'title'
148
+ (LIMIT + 1).times { fables << Fable.create!(:title => title) }
149
+ Fable.get("title/#{title}").size.should > LIMIT
150
+
151
+ fables.shift.destroy
152
+ Fable.get("title/#{title}").should == fables.collect(&:id)
153
+ end
154
+ end
155
+ end
156
+
157
+ describe 'when the cache is not populated' do
158
+ describe 'when count of records in the database is <= limit of items' do
159
+ it 'deletes from the index' do
160
+ fables, title = [], 'title'
161
+ LIMIT.times { fables << Fable.create!(:title => title) }
162
+ $memcache.flush_all
163
+
164
+ fables.shift.destroy
165
+ Fable.get("title/#{title}").should == fables.collect(&:id)
166
+ end
167
+
168
+ describe 'when the count of records in the database is between limit and limit + buffer items' do
169
+ it 'populates the index' do
170
+ fables, title = [], 'title'
171
+ (LIMIT + BUFFER + 1).times { fables << Fable.create!(:title => title) }
172
+ BUFFER.times { fables.shift.destroy }
173
+ $memcache.flush_all
174
+
175
+ fables.shift.destroy
176
+ Fable.get("title/#{title}").should == fables.collect(&:id)
177
+
178
+ end
179
+ end
180
+
181
+ describe 'when the count of records in the database is > limit + buffer items' do
182
+ it 'populates the index with limit + buffer items' do
183
+ fables, title = [], 'title'
184
+ (LIMIT + BUFFER + 2).times { fables << Fable.create!(:title => title) }
185
+ $memcache.flush_all
186
+
187
+ fables.shift.destroy
188
+ Fable.get("title/#{title}").should == fables[0, LIMIT + BUFFER].collect(&:id)
189
+ end
190
+ end
191
+ end
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,223 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ module Cash
4
+ describe WriteThrough do
5
+ describe 'ClassMethods' do
6
+ describe 'after create' do
7
+ it "inserts all indexed attributes into the cache" do
8
+ story = Story.create!(:title => "I am delicious")
9
+ Story.get("title/#{story.title}").should == [story.id]
10
+ Story.get("id/#{story.id}").should == [story]
11
+ end
12
+
13
+ describe 'multiple objects' do
14
+ it "inserts multiple objects into the same cache key" do
15
+ story1 = Story.create!(:title => "I am delicious")
16
+ story2 = Story.create!(:title => "I am delicious")
17
+ Story.get("title/#{story1.title}").should == [story1.id, story2.id]
18
+ end
19
+
20
+ describe 'when the cache has been cleared after some objects were created' do
21
+ before do
22
+ @story1 = Story.create!(:title => "I am delicious")
23
+ $memcache.flush_all
24
+ @story2 = Story.create!(:title => "I am delicious")
25
+ end
26
+
27
+ it 'inserts legacy objects into the cache' do
28
+ Story.get("title/#{@story1.title}").should == [@story1.id, @story2.id]
29
+ end
30
+
31
+ it 'initializes the count to account for the legacy objects' do
32
+ Story.get("title/#{@story1.title}/count", :raw => true).should =~ /2/
33
+ end
34
+ end
35
+ end
36
+
37
+ it "does not write through the cache on non-indexed attributes" do
38
+ story = Story.create!(:title => "Story 1", :subtitle => "Subtitle")
39
+ Story.get("subtitle/#{story.subtitle}").should == nil
40
+ end
41
+
42
+ it "indexes on combinations of attributes" do
43
+ story = Story.create!(:title => "Sam")
44
+ Story.get("id/#{story.id}/title/#{story.title}").should == [story.id]
45
+ end
46
+
47
+ it "does not cache associations" do
48
+ story = Story.new(:title => 'I am lugubrious')
49
+ story.characters.build(:name => 'How am I holy?')
50
+ story.save!
51
+ Story.get("id/#{story.id}").first.characters.loaded?.should_not be
52
+ end
53
+
54
+ it 'increments the count' do
55
+ story = Story.create!(:title => "Sam")
56
+ Story.get("title/#{story.title}/count", :raw => true).should =~ /1/
57
+ story = Story.create!(:title => "Sam")
58
+ Story.get("title/#{story.title}/count", :raw => true).should =~ /2/
59
+ end
60
+
61
+ describe 'when the value is nil' do
62
+ it "does not write through the cache on indexed attributes" do
63
+ story = Story.create!(:title => nil)
64
+ Story.get("title/").should == nil
65
+ end
66
+ end
67
+ end
68
+
69
+ describe 'after update' do
70
+ it "overwrites the primary cache" do
71
+ story = Story.create!(:title => "I am delicious")
72
+ Story.get(cache_key = "id/#{story.id}").first.title.should == "I am delicious"
73
+ story.update_attributes(:title => "I am fabulous")
74
+ Story.get(cache_key).first.title.should == "I am fabulous"
75
+ end
76
+
77
+ it "populates empty caches" do
78
+ story = Story.create!(:title => "I am delicious")
79
+ $memcache.flush_all
80
+ story.update_attributes(:title => "I am fabulous")
81
+ Story.get("title/#{story.title}").should == [story.id]
82
+ end
83
+
84
+ it "removes from the affected index caches on update" do
85
+ story = Story.create!(:title => "I am delicious")
86
+ Story.get(cache_key = "title/#{story.title}").should == [story.id]
87
+ story.update_attributes(:title => "I am fabulous")
88
+ Story.get(cache_key).should == []
89
+ end
90
+
91
+ it 'increments/decrements the counts of affected indices' do
92
+ story = Story.create!(:title => original_title = "I am delicious")
93
+ story.update_attributes(:title => new_title = "I am fabulous")
94
+ Story.get("title/#{original_title}/count", :raw => true).should =~ /0/
95
+ Story.get("title/#{new_title}/count", :raw => true).should =~ /1/
96
+ end
97
+ end
98
+
99
+ describe 'after destroy' do
100
+ it "removes from the primary cache" do
101
+ story = Story.create!(:title => "I am delicious")
102
+ Story.get(cache_key = "id/#{story.id}").should == [story]
103
+ story.destroy
104
+ Story.get(cache_key).should == []
105
+ end
106
+
107
+ it "removes from the the cache on keys matching the original values of attributes" do
108
+ story = Story.create!(:title => "I am delicious")
109
+ Story.get(cache_key = "title/#{story.title}").should == [story.id]
110
+ story.title = "I am not delicious"
111
+ story.destroy
112
+ Story.get(cache_key).should == []
113
+ end
114
+
115
+ it 'decrements the count' do
116
+ story = Story.create!(:title => "I am delicious")
117
+ story.destroy
118
+ Story.get("title/#{story.title}/count", :raw => true).should =~ /0/
119
+ end
120
+
121
+ describe 'when there are multiple items in the index' do
122
+ it "only removes one item from the affected indices, not all of them" do
123
+ story1 = Story.create!(:title => "I am delicious")
124
+ story2 = Story.create!(:title => "I am delicious")
125
+ Story.get(cache_key = "title/#{story1.title}").should == [story1.id, story2.id]
126
+ story1.destroy
127
+ Story.get(cache_key).should == [story2.id]
128
+ end
129
+ end
130
+
131
+ describe 'when the object is a new record' do
132
+ it 'does nothing' do
133
+ story1 = Story.new
134
+ mock(Story).set.never
135
+ story1.destroy
136
+ end
137
+ end
138
+
139
+ describe 'when the cache is not yet populated' do
140
+ it "populates the cache with data" do
141
+ story1 = Story.create!(:title => "I am delicious")
142
+ story2 = Story.create!(:title => "I am delicious")
143
+ $memcache.flush_all
144
+ Story.get(cache_key = "title/#{story1.title}").should == nil
145
+ story1.destroy
146
+ Story.get(cache_key).should == [story2.id]
147
+ end
148
+ end
149
+
150
+ describe 'when the value is nil' do
151
+ it "does not delete through the cache on indexed attributes when the value is nil" do
152
+ story = Story.create!(:title => nil)
153
+ story.destroy
154
+ Story.get("title/").should == nil
155
+ end
156
+ end
157
+ end
158
+
159
+ describe 'InstanceMethods' do
160
+ describe '#expire_caches' do
161
+ it 'deletes the index' do
162
+ story = Story.create!(:title => "I am delicious")
163
+ Story.get(cache_key = "id/#{story.id}").should == [story]
164
+ story.expire_caches
165
+ Story.get(cache_key).should be_nil
166
+ end
167
+ end
168
+ end
169
+ end
170
+
171
+ describe "Locking" do
172
+ it "acquires and releases locks, in order, for all indices to be written" do
173
+ pending
174
+
175
+ story = Story.create!(:title => original_title = "original title")
176
+ story.title = tentative_title = "tentative title"
177
+ keys = ["id/#{story.id}", "title/#{original_title}", "title/#{story.title}", "id/#{story.id}/title/#{original_title}", "id/#{story.id}/title/#{tentative_title}"]
178
+
179
+ locks_should_be_acquired_and_released_in_order($lock, keys)
180
+ story.save!
181
+ end
182
+
183
+ it "acquires and releases locks on destroy" do
184
+ pending
185
+
186
+ story = Story.create!(:title => "title")
187
+ keys = ["id/#{story.id}", "title/#{story.title}", "id/#{story.id}/title/#{story.title}"]
188
+
189
+ locks_should_be_acquired_and_released_in_order($lock, keys)
190
+ story.destroy
191
+ end
192
+
193
+ def locks_should_be_acquired_and_released_in_order(lock, keys)
194
+ mock = keys.sort!.inject(mock = mock($lock)) do |mock, key|
195
+ mock.acquire_lock.with(Story.cache_key(key)).then
196
+ end
197
+ keys.inject(mock) do |mock, key|
198
+ mock.release_lock.with(Story.cache_key(key)).then
199
+ end
200
+ end
201
+ end
202
+
203
+ describe "Single Table Inheritence" do
204
+ describe 'A subclass' do
205
+ it "writes to indices of all superclasses" do
206
+ oral = Oral.create!(:title => 'title')
207
+ Story.get("title/#{oral.title}").should == [oral.id]
208
+ Epic.get("title/#{oral.title}").should == [oral.id]
209
+ Oral.get("title/#{oral.title}").should == [oral.id]
210
+ end
211
+
212
+ describe 'when one ancestor has its own indices' do
213
+ it "it only populates those indices for that ancestor" do
214
+ oral = Oral.create!(:subtitle => 'subtitle')
215
+ Story.get("subtitle/#{oral.subtitle}").should be_nil
216
+ Epic.get("subtitle/#{oral.subtitle}").should be_nil
217
+ Oral.get("subtitle/#{oral.subtitle}").should == [oral.id]
218
+ end
219
+ end
220
+ end
221
+ end
222
+ end
223
+ end
@@ -0,0 +1,55 @@
1
+ dir = File.dirname(__FILE__)
2
+ $LOAD_PATH.unshift "#{dir}/../lib"
3
+
4
+ require 'rubygems'
5
+ require 'spec'
6
+ require 'pp'
7
+ require 'cash'
8
+ require 'memcache'
9
+ require File.join(dir, '../config/environment')
10
+
11
+ Spec::Runner.configure do |config|
12
+ config.mock_with :rr
13
+ config.before :suite do
14
+ load File.join(dir, "../db/schema.rb")
15
+
16
+ config = YAML.load(IO.read((File.expand_path(File.dirname(__FILE__) + "/../config/memcache.yml"))))['test']
17
+ $memcache = MemCache.new(config)
18
+ $memcache.servers = config['servers']
19
+ $lock = Cash::Lock.new($memcache)
20
+ end
21
+
22
+ config.before :each do
23
+ $memcache.flush_all
24
+ Story.delete_all
25
+ Character.delete_all
26
+ end
27
+
28
+ config.before :suite do
29
+ ActiveRecord::Base.class_eval do
30
+ is_cached :repository => Cash::Transactional.new($memcache, $lock)
31
+ end
32
+
33
+ Character = Class.new(ActiveRecord::Base)
34
+ Story = Class.new(ActiveRecord::Base)
35
+ Story.has_many :characters
36
+
37
+ Story.class_eval do
38
+ index :title
39
+ index [:id, :title]
40
+ end
41
+
42
+ Epic = Class.new(Story)
43
+ Oral = Class.new(Epic)
44
+
45
+ Character.class_eval do
46
+ index [:name, :story_id]
47
+ index [:id, :story_id]
48
+ index [:id, :name, :story_id]
49
+ end
50
+
51
+ Oral.class_eval do
52
+ index :subtitle
53
+ end
54
+ end
55
+ end