viximo-cache-money 0.3.0

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.
Files changed (51) hide show
  1. data/LICENSE +201 -0
  2. data/README +204 -0
  3. data/README.markdown +204 -0
  4. data/TODO +17 -0
  5. data/UNSUPPORTED_FEATURES +13 -0
  6. data/config/environment.rb +8 -0
  7. data/config/memcached.yml +4 -0
  8. data/db/schema.rb +18 -0
  9. data/init.rb +1 -0
  10. data/lib/cache_money.rb +105 -0
  11. data/lib/cash/accessor.rb +83 -0
  12. data/lib/cash/adapter/memcache_client.rb +36 -0
  13. data/lib/cash/adapter/memcached.rb +127 -0
  14. data/lib/cash/adapter/redis.rb +144 -0
  15. data/lib/cash/buffered.rb +137 -0
  16. data/lib/cash/config.rb +78 -0
  17. data/lib/cash/fake.rb +83 -0
  18. data/lib/cash/finders.rb +50 -0
  19. data/lib/cash/index.rb +211 -0
  20. data/lib/cash/local.rb +105 -0
  21. data/lib/cash/lock.rb +63 -0
  22. data/lib/cash/mock.rb +158 -0
  23. data/lib/cash/query/abstract.rb +219 -0
  24. data/lib/cash/query/calculation.rb +45 -0
  25. data/lib/cash/query/primary_key.rb +50 -0
  26. data/lib/cash/query/select.rb +16 -0
  27. data/lib/cash/request.rb +3 -0
  28. data/lib/cash/transactional.rb +43 -0
  29. data/lib/cash/util/array.rb +9 -0
  30. data/lib/cash/util/marshal.rb +19 -0
  31. data/lib/cash/version.rb +3 -0
  32. data/lib/cash/write_through.rb +71 -0
  33. data/lib/mem_cached_session_store.rb +49 -0
  34. data/lib/mem_cached_support_store.rb +143 -0
  35. data/rails/init.rb +1 -0
  36. data/spec/cash/accessor_spec.rb +186 -0
  37. data/spec/cash/active_record_spec.rb +224 -0
  38. data/spec/cash/buffered_spec.rb +9 -0
  39. data/spec/cash/calculations_spec.rb +78 -0
  40. data/spec/cash/finders_spec.rb +455 -0
  41. data/spec/cash/local_buffer_spec.rb +9 -0
  42. data/spec/cash/local_spec.rb +9 -0
  43. data/spec/cash/lock_spec.rb +110 -0
  44. data/spec/cash/marshal_spec.rb +60 -0
  45. data/spec/cash/order_spec.rb +172 -0
  46. data/spec/cash/transactional_spec.rb +602 -0
  47. data/spec/cash/window_spec.rb +195 -0
  48. data/spec/cash/without_caching_spec.rb +32 -0
  49. data/spec/cash/write_through_spec.rb +252 -0
  50. data/spec/spec_helper.rb +87 -0
  51. metadata +300 -0
@@ -0,0 +1,195 @@
1
+ require "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,32 @@
1
+ require 'spec_helper'
2
+
3
+ describe Cash do
4
+ describe 'when disabled' do
5
+ before(:each) do
6
+ Cash.enabled = false
7
+
8
+ mock($memcache).get.never
9
+ mock($memcache).add.never
10
+ mock($memcache).set.never
11
+ end
12
+
13
+ after(:each) do
14
+ Cash.enabled = true
15
+ end
16
+
17
+ it 'creates and looks up objects without using cache' do
18
+ story = Story.create!
19
+ Story.find(story.id).should == story
20
+ end
21
+
22
+ it 'updates objects without using cache' do
23
+ story = Story.create!
24
+ story.title = 'test'
25
+ story.save!
26
+ end
27
+
28
+ it 'should find using indexed condition without using cache' do
29
+ Story.find(:all, :conditions => {:title => 'x'})
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,252 @@
1
+ require "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
+
68
+ it 'should not remember instance variables' do
69
+ story = Story.new(:title => 'story')
70
+ story.instance_eval { @forgetme = "value" }
71
+ story.save!
72
+ Story.find(story.id).instance_variables.should_not include("@forgetme")
73
+ end
74
+ end
75
+
76
+ describe 'after update' do
77
+ it "overwrites the primary cache" do
78
+ story = Story.create!(:title => "I am delicious")
79
+ Story.get(cache_key = "id/#{story.id}").first.title.should == "I am delicious"
80
+ story.update_attributes(:title => "I am fabulous")
81
+ Story.get(cache_key).first.title.should == "I am fabulous"
82
+ end
83
+
84
+ it "populates empty caches" do
85
+ story = Story.create!(:title => "I am delicious")
86
+ $memcache.flush_all
87
+ story.update_attributes(:title => "I am fabulous")
88
+ Story.get("title/#{story.title}").should == [story.id]
89
+ end
90
+
91
+ it "removes from the affected index caches on update" do
92
+ story = Story.create!(:title => "I am delicious")
93
+ Story.get(cache_key = "title/#{story.title}").should == [story.id]
94
+ story.update_attributes(:title => "I am fabulous")
95
+ Story.get(cache_key).should == []
96
+ end
97
+
98
+ it 'increments/decrements the counts of affected indices' do
99
+ story = Story.create!(:title => original_title = "I am delicious")
100
+ story.update_attributes(:title => new_title = "I am fabulous")
101
+ Story.get("title/#{original_title}/count", :raw => true).should =~ /0/
102
+ Story.get("title/#{new_title}/count", :raw => true).should =~ /1/
103
+ end
104
+ end
105
+
106
+ describe 'after destroy' do
107
+ it "removes from the primary cache" do
108
+ story = Story.create!(:title => "I am delicious")
109
+ Story.get(cache_key = "id/#{story.id}").should == [story]
110
+ story.destroy
111
+ Story.get(cache_key).should == []
112
+ end
113
+
114
+ it "removes from the the cache on keys matching the original values of attributes" do
115
+ story = Story.create!(:title => "I am delicious")
116
+ Story.get(cache_key = "title/#{story.title}").should == [story.id]
117
+ story.title = "I am not delicious"
118
+ story.destroy
119
+ Story.get(cache_key).should == []
120
+ end
121
+
122
+ it 'decrements the count' do
123
+ story = Story.create!(:title => "I am delicious")
124
+ story.destroy
125
+ Story.get("title/#{story.title}/count", :raw => true).should =~ /0/
126
+ end
127
+
128
+ describe 'when there are multiple items in the index' do
129
+ it "only removes one item from the affected indices, not all of them" do
130
+ story1 = Story.create!(:title => "I am delicious")
131
+ story2 = Story.create!(:title => "I am delicious")
132
+ Story.get(cache_key = "title/#{story1.title}").should == [story1.id, story2.id]
133
+ story1.destroy
134
+ Story.get(cache_key).should == [story2.id]
135
+ end
136
+ end
137
+
138
+ describe 'when the object is a new record' do
139
+ it 'does nothing' do
140
+ story1 = Story.new
141
+ mock(Story).set.never
142
+ story1.destroy
143
+ end
144
+ end
145
+
146
+ describe 'when the cache is not yet populated' do
147
+ it "populates the cache with data" do
148
+ story1 = Story.create!(:title => "I am delicious")
149
+ story2 = Story.create!(:title => "I am delicious")
150
+ $memcache.flush_all
151
+ Story.get(cache_key = "title/#{story1.title}").should == nil
152
+ story1.destroy
153
+ Story.get(cache_key).should == [story2.id]
154
+ end
155
+ end
156
+
157
+ describe 'when the value is nil' do
158
+ it "does not delete through the cache on indexed attributes when the value is nil" do
159
+ story = Story.create!(:title => nil)
160
+ story.destroy
161
+ Story.get("title/").should == nil
162
+ end
163
+ end
164
+ end
165
+
166
+ describe 'InstanceMethods' do
167
+ describe '#expire_caches' do
168
+ it 'deletes the index' do
169
+ story = Story.create!(:title => "I am delicious")
170
+ Story.get(cache_key = "id/#{story.id}").should == [story]
171
+ story.expire_caches
172
+ Story.get(cache_key).should be_nil
173
+ end
174
+ end
175
+ end
176
+ end
177
+
178
+ describe "Locking" do
179
+ it "acquires and releases locks, in order, for all indices to be written" do
180
+ pending
181
+
182
+ story = Story.create!(:title => original_title = "original title")
183
+ story.title = tentative_title = "tentative title"
184
+ keys = ["id/#{story.id}", "title/#{original_title}", "title/#{story.title}", "id/#{story.id}/title/#{original_title}", "id/#{story.id}/title/#{tentative_title}"]
185
+
186
+ locks_should_be_acquired_and_released_in_order($lock, keys)
187
+ story.save!
188
+ end
189
+
190
+ it "acquires and releases locks on destroy" do
191
+ pending
192
+
193
+ story = Story.create!(:title => "title")
194
+ keys = ["id/#{story.id}", "title/#{story.title}", "id/#{story.id}/title/#{story.title}"]
195
+
196
+ locks_should_be_acquired_and_released_in_order($lock, keys)
197
+ story.destroy
198
+ end
199
+
200
+ def locks_should_be_acquired_and_released_in_order(lock, keys)
201
+ mock = keys.sort!.inject(mock = mock($lock)) do |mock, key|
202
+ mock.acquire_lock.with(Story.cache_key(key)).then
203
+ end
204
+ keys.inject(mock) do |mock, key|
205
+ mock.release_lock.with(Story.cache_key(key)).then
206
+ end
207
+ end
208
+ end
209
+
210
+ describe "Single Table Inheritence" do
211
+ describe 'A subclass' do
212
+ it "writes to indices of all superclasses" do
213
+ oral = Oral.create!(:title => 'title')
214
+ Story.get("title/#{oral.title}").should == [oral.id]
215
+ Epic.get("title/#{oral.title}").should == [oral.id]
216
+ Oral.get("title/#{oral.title}").should == [oral.id]
217
+ end
218
+
219
+ describe 'when one ancestor has its own indices' do
220
+ it "it only populates those indices for that ancestor" do
221
+ oral = Oral.create!(:subtitle => 'subtitle')
222
+ Story.get("subtitle/#{oral.subtitle}").should be_nil
223
+ Epic.get("subtitle/#{oral.subtitle}").should be_nil
224
+ Oral.get("subtitle/#{oral.subtitle}").should == [oral.id]
225
+ end
226
+ end
227
+ end
228
+ end
229
+
230
+ describe 'Transactions' do
231
+ def create_story_and_update
232
+ @story = Story.create!(:title => original_title = "original title")
233
+
234
+ Story.transaction do
235
+ @story.title = "new title"
236
+ @story.save
237
+ yield if block_given?
238
+ end
239
+ end
240
+
241
+ it 'should commit on success' do
242
+ create_story_and_update
243
+ @story.reload.title.should == "new title"
244
+ end
245
+
246
+ it 'should roll back transactions when ActiveRecord::Rollback is raised' do
247
+ create_story_and_update { raise ActiveRecord::Rollback }
248
+ @story.reload.title.should == "original title"
249
+ end
250
+ end
251
+ end
252
+ end