mguymon-cache-money 0.2.12

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,60 @@
1
+ require File.join(File.dirname(__FILE__), '..', 'spec_helper')
2
+
3
+ describe Marshal do
4
+ describe '#load' do
5
+ before do
6
+ class Constant; end
7
+ @reference_to_constant = Constant
8
+ @object = @reference_to_constant.new
9
+ @marshaled_object = Marshal.dump(@object)
10
+ end
11
+
12
+ describe 'when the constant is not yet loaded' do
13
+ it 'loads the constant' do
14
+ Object.send(:remove_const, :Constant)
15
+ stub(Marshal).constantize(@reference_to_constant.name) { Object.send(:const_set, :Constant, @reference_to_constant) }
16
+ Marshal.load(@marshaled_object).class.should == @object.class
17
+ end
18
+
19
+ it 'loads the constant with the scope operator' do
20
+ module Foo; class Bar; end; end
21
+
22
+ reference_to_module = Foo
23
+ reference_to_constant = Foo::Bar
24
+ object = reference_to_constant.new
25
+ marshaled_object = Marshal.dump(object)
26
+
27
+ Foo.send(:remove_const, :Bar)
28
+ Object.send(:remove_const, :Foo)
29
+ stub(Marshal).constantize(reference_to_module.name) { Object.send(:const_set, :Foo, reference_to_module) }
30
+ stub(Marshal).constantize(reference_to_constant.name) { Foo.send(:const_set, :Bar, reference_to_constant) }
31
+
32
+ Marshal.load(marshaled_object).class.should == object.class
33
+ end
34
+ end
35
+
36
+ describe 'when the constant does not exist' do
37
+ it 'raises a LoadError' do
38
+ Object.send(:remove_const, :Constant)
39
+ stub(Marshal).constantize { raise NameError }
40
+ lambda { Marshal.load(@marshaled_object) }.should raise_error(NameError)
41
+ end
42
+ end
43
+
44
+ describe 'when there are recursive constants to load' do
45
+ it 'loads all constants recursively' do
46
+ class Constant1; end
47
+ class Constant2; end
48
+ reference_to_constant1 = Constant1
49
+ reference_to_constant2 = Constant2
50
+ object = [reference_to_constant1.new, reference_to_constant2.new]
51
+ marshaled_object = Marshal.dump(object)
52
+ Object.send(:remove_const, :Constant1)
53
+ Object.send(:remove_const, :Constant2)
54
+ stub(Marshal).constantize(reference_to_constant1.name) { Object.send(:const_set, :Constant1, reference_to_constant1) }
55
+ stub(Marshal).constantize(reference_to_constant2.name) { Object.send(:const_set, :Constant2, reference_to_constant2) }
56
+ Marshal.load(marshaled_object).collect(&:class).should == object.collect(&:class)
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,172 @@
1
+ require "spec_helper"
2
+
3
+ module Cash
4
+ describe 'Ordering' do
5
+ before :suite do
6
+ FairyTale = Class.new(Story)
7
+ end
8
+
9
+ describe '#create!' do
10
+ describe 'the records are written-through in sorted order', :shared => true do
11
+ describe 'when there are not already records matching the index' do
12
+ it 'initializes the index' do
13
+ fairy_tale = FairyTale.create!(:title => 'title')
14
+ FairyTale.get("title/#{fairy_tale.title}").should == [fairy_tale.id]
15
+ end
16
+ end
17
+
18
+ describe 'when there are already records matching the index' do
19
+ before do
20
+ @fairy_tale1 = FairyTale.create!(:title => 'title')
21
+ FairyTale.get("title/#{@fairy_tale1.title}").should == sorted_and_serialized_records(@fairy_tale1)
22
+ end
23
+
24
+ describe 'when the index is populated' do
25
+ it 'appends to the index' do
26
+ fairy_tale2 = FairyTale.create!(:title => @fairy_tale1.title)
27
+ FairyTale.get("title/#{@fairy_tale1.title}").should == sorted_and_serialized_records(@fairy_tale1, fairy_tale2)
28
+ end
29
+ end
30
+
31
+ describe 'when the index is not populated' do
32
+ before do
33
+ $memcache.flush_all
34
+ end
35
+
36
+ it 'initializes the index' do
37
+ fairy_tale2 = FairyTale.create!(:title => @fairy_tale1.title)
38
+ FairyTale.get("title/#{@fairy_tale1.title}").should == sorted_and_serialized_records(@fairy_tale1, fairy_tale2)
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ describe 'when the order is ascending' do
45
+ it_should_behave_like 'the records are written-through in sorted order'
46
+
47
+ before :all do
48
+ FairyTale.index :title, :order => :asc
49
+ end
50
+
51
+ def sorted_and_serialized_records(*records)
52
+ records.collect(&:id).sort
53
+ end
54
+ end
55
+
56
+ describe 'when the order is descending' do
57
+ it_should_behave_like 'the records are written-through in sorted order'
58
+
59
+ before :all do
60
+ FairyTale.index :title, :order => :desc
61
+ end
62
+
63
+ def sorted_and_serialized_records(*records)
64
+ records.collect(&:id).sort.reverse
65
+ end
66
+ end
67
+ end
68
+
69
+ describe "#find(..., :order => ...)" do
70
+ before :each do
71
+ @fairy_tales = [FairyTale.create!(:title => @title = 'title'), FairyTale.create!(:title => @title)]
72
+ end
73
+
74
+ describe 'when the order is ascending' do
75
+ before :all do
76
+ FairyTale.index :title, :order => :asc
77
+ end
78
+
79
+ describe "#find(..., :order => 'id ASC')" do
80
+ describe 'when the cache is populated' do
81
+ it 'does not use the database' do
82
+ mock(FairyTale.connection).execute.never
83
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id ASC').should == @fairy_tales
84
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id').should == @fairy_tales
85
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => '`id`').should == @fairy_tales
86
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'stories.id').should == @fairy_tales
87
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => '`stories`.id').should == @fairy_tales
88
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => '`stories`.`id`').should == @fairy_tales
89
+ end
90
+
91
+ describe 'when the order is passed as a symbol' do
92
+ it 'works' do
93
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => :id)
94
+ end
95
+ end
96
+ end
97
+
98
+ describe 'when the cache is not populated' do
99
+ it 'populates the cache' do
100
+ $memcache.flush_all
101
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id ASC').should == @fairy_tales
102
+ FairyTale.get("title/#{@title}").should == @fairy_tales.collect(&:id)
103
+ end
104
+ end
105
+ end
106
+
107
+ describe "#find(..., :order => 'id DESC')" do
108
+ describe 'when the cache is populated' do
109
+ it 'uses the database, not the cache' do
110
+ mock(FairyTale).get.never
111
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id DESC').should == @fairy_tales.reverse
112
+ end
113
+ end
114
+
115
+ describe 'when the cache is not populated' do
116
+ it 'does not populate the cache' do
117
+ $memcache.flush_all
118
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id DESC').should == @fairy_tales.reverse
119
+ FairyTale.get("title/#{@title}").should be_nil
120
+ end
121
+ end
122
+ end
123
+ end
124
+
125
+ describe 'when the order is descending' do
126
+ before :all do
127
+ FairyTale.index :title, :order => :desc
128
+ end
129
+
130
+ describe "#find(..., :order => 'id DESC')" do
131
+ describe 'when the cache is populated' do
132
+ it 'does not use the database' do
133
+ mock(FairyTale.connection).execute.never
134
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id DESC').should == @fairy_tales.reverse
135
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id DESC').should == @fairy_tales.reverse
136
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => '`id` DESC').should == @fairy_tales.reverse
137
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'stories.id DESC').should == @fairy_tales.reverse
138
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => '`stories`.id DESC').should == @fairy_tales.reverse
139
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => '`stories`.`id` DESC').should == @fairy_tales.reverse
140
+ end
141
+ end
142
+
143
+ describe 'when the cache is not populated' do
144
+ it 'populates the cache' do
145
+ $memcache.flush_all
146
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id DESC')
147
+ FairyTale.get("title/#{@title}").should == @fairy_tales.collect(&:id).reverse
148
+ end
149
+ end
150
+ end
151
+
152
+ describe "#find(..., :order => 'id ASC')" do
153
+ describe 'when the cache is populated' do
154
+ it 'uses the database, not the cache' do
155
+ mock(FairyTale).get.never
156
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id ASC').should == @fairy_tales
157
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id').should == @fairy_tales
158
+ end
159
+ end
160
+
161
+ describe 'when the cache is not populated' do
162
+ it 'does not populate the cache' do
163
+ $memcache.flush_all
164
+ FairyTale.find(:all, :conditions => { :title => @title }, :order => 'id ASC').should == @fairy_tales
165
+ FairyTale.get("title/#{@title}").should be_nil
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,578 @@
1
+ require "spec_helper"
2
+
3
+ module Cash
4
+ describe Transactional do
5
+ before do
6
+ @cache = Transactional.new($memcache, $lock)
7
+ @value = "stuff to be cached"
8
+ @key = "key"
9
+ end
10
+
11
+ describe 'Basic Operations' do
12
+ it "gets through the real cache" do
13
+ $memcache.set(@key, @value)
14
+ @cache.get(@key).should == @value
15
+ end
16
+
17
+ it "sets through the real cache" do
18
+ mock($memcache).set(@key, @value, :option1, :option2)
19
+ @cache.set(@key, @value, :option1, :option2)
20
+ end
21
+
22
+ it "increments through the real cache" do
23
+ @cache.set(@key, 0, 0, true)
24
+ @cache.incr(@key, 3)
25
+
26
+ @cache.get(@key, true).to_i.should == 3
27
+ $memcache.get(@key, true).to_i.should == 3
28
+ end
29
+
30
+ it "decrements through the real cache" do
31
+ @cache.set(@key, 0, 0, true)
32
+ @cache.incr(@key, 3)
33
+ @cache.decr(@key, 2)
34
+
35
+ @cache.get(@key, true).to_i.should == 1
36
+ $memcache.get(@key, true).to_i.should == 1
37
+ end
38
+
39
+ it "adds through the real cache" do
40
+ @cache.add(@key, @value)
41
+ $memcache.get(@key).should == @value
42
+ @cache.get(@key).should == @value
43
+
44
+ @cache.add(@key, "another value")
45
+ $memcache.get(@key).should == @value
46
+ @cache.get(@key).should == @value
47
+ end
48
+
49
+ it "deletes through the real cache" do
50
+ $memcache.add(@key, @value)
51
+ $memcache.get(@key).should == @value
52
+
53
+ @cache.delete(@key)
54
+ $memcache.get(@key).should be_nil
55
+ end
56
+
57
+ it "returns true for respond_to? with what it responds to" do
58
+ @cache.respond_to?(:get).should be_true
59
+ @cache.respond_to?(:set).should be_true
60
+ @cache.respond_to?(:get_multi).should be_true
61
+ @cache.respond_to?(:incr).should be_true
62
+ @cache.respond_to?(:decr).should be_true
63
+ @cache.respond_to?(:add).should be_true
64
+ end
65
+
66
+ it "delegates unsupported messages back to the real cache" do
67
+ mock($memcache).foo(:bar)
68
+ @cache.foo(:bar)
69
+ end
70
+
71
+ describe '#get_multi' do
72
+ describe 'when everything is a hit' do
73
+ it 'returns a hash' do
74
+ @cache.set('key1', @value)
75
+ @cache.set('key2', @value)
76
+ @cache.get_multi('key1', 'key2').should == { 'key1' => @value, 'key2' => @value }
77
+ end
78
+ end
79
+
80
+ describe 'when there are misses' do
81
+ it 'only returns results for hits' do
82
+ @cache.set('key1', @value)
83
+ @cache.get_multi('key1', 'key2').should == { 'key1' => @value }
84
+ end
85
+ end
86
+ end
87
+ end
88
+
89
+ describe 'In a Transaction' do
90
+ it "commits to the real cache" do
91
+ $memcache.get(@key).should == nil
92
+ @cache.transaction do
93
+ @cache.set(@key, @value)
94
+ end
95
+ $memcache.get(@key).should == @value
96
+ end
97
+
98
+ describe 'when there is a return/next/break in the transaction' do
99
+ it 'commits to the real cache' do
100
+ $memcache.get(@key).should == nil
101
+ @cache.transaction do
102
+ @cache.set(@key, @value)
103
+ next
104
+ end
105
+ $memcache.get(@key).should == @value
106
+ end
107
+ end
108
+
109
+ it "reads through the real cache if key has not been written to" do
110
+ $memcache.set(@key, @value)
111
+ @cache.transaction do
112
+ @cache.get(@key).should == @value
113
+ end
114
+ @cache.get(@key).should == @value
115
+ end
116
+
117
+ it "delegates unsupported messages back to the real cache" do
118
+ @cache.transaction do
119
+ mock($memcache).foo(:bar)
120
+ @cache.foo(:bar)
121
+ end
122
+ end
123
+
124
+ it "returns the result of the block passed to the transaction" do
125
+ @cache.transaction do
126
+ :result
127
+ end.should == :result
128
+ end
129
+
130
+ describe 'Increment and Decrement' do
131
+ describe '#incr' do
132
+ it "works" do
133
+ @cache.set(@key, 0, 0, true)
134
+ @cache.incr(@key)
135
+ @cache.transaction do
136
+ @cache.incr(@key).should == 2
137
+ end
138
+ end
139
+
140
+ it "is buffered" do
141
+ @cache.transaction do
142
+ @cache.set(@key, 0, 0, true)
143
+ @cache.incr(@key, 2).should == 2
144
+ @cache.get(@key).should == 2
145
+ $memcache.get(@key).should == nil
146
+ end
147
+ @cache.get(@key, true).to_i.should == 2
148
+ $memcache.get(@key, true).to_i.should == 2
149
+ end
150
+
151
+ it "returns nil if there is no key already at that value" do
152
+ @cache.transaction do
153
+ @cache.incr(@key).should == nil
154
+ end
155
+ end
156
+
157
+ end
158
+
159
+ describe '#decr' do
160
+ it "works" do
161
+ @cache.set(@key, 0, 0, true)
162
+ @cache.incr(@key)
163
+ @cache.transaction do
164
+ @cache.decr(@key).should == 0
165
+ end
166
+ end
167
+
168
+ it "is buffered" do
169
+ @cache.transaction do
170
+ @cache.set(@key, 0, 0, true)
171
+ @cache.incr(@key, 3)
172
+ @cache.decr(@key, 2).should == 1
173
+ @cache.get(@key, true).to_i.should == 1
174
+ $memcache.get(@key).should == nil
175
+ end
176
+ @cache.get(@key, true).to_i.should == 1
177
+ $memcache.get(@key, true).to_i.should == 1
178
+ end
179
+
180
+ it "returns nil if there is no key already at that value" do
181
+ @cache.transaction do
182
+ @cache.decr(@key).should == nil
183
+ end
184
+ end
185
+
186
+ it "bottoms out at zero" do
187
+ @cache.transaction do
188
+ @cache.set(@key, 0, 0, true)
189
+ @cache.incr(@key, 1)
190
+ @cache.get(@key, true).should == 1
191
+ @cache.decr(@key)
192
+ @cache.get(@key, true).should == 0
193
+ @cache.decr(@key)
194
+ @cache.get(@key, true).should == 0
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ describe '#get_multi' do
201
+ describe 'when a hit value is the empty array' do
202
+ it 'returns a hash' do
203
+ @cache.transaction do
204
+ @cache.set('key1', @value)
205
+ @cache.set('key2', [])
206
+ @cache.get_multi('key1', 'key2').should == { 'key1' => @value, 'key2' => [] }
207
+ end
208
+ end
209
+ end
210
+
211
+ describe 'when everything is a hit' do
212
+ it 'returns a hash' do
213
+ @cache.transaction do
214
+ @cache.set('key1', @value)
215
+ @cache.set('key2', @value)
216
+ @cache.get_multi('key1', 'key2').should == { 'key1' => @value, 'key2' => @value }
217
+ end
218
+ end
219
+ end
220
+
221
+ describe 'when there are misses' do
222
+ it 'only returns results for hits' do
223
+ @cache.transaction do
224
+ @cache.set('key1', @value)
225
+ @cache.get_multi('key1', 'key2').should == { 'key1' => @value }
226
+ end
227
+ end
228
+ end
229
+ end
230
+
231
+ describe 'Lock Acquisition' do
232
+ it "locks @keys to be written before writing to memcache and release them after" do
233
+ mock($lock).acquire_lock(@key)
234
+ mock($memcache).set(@key, @value)
235
+ mock($lock).release_lock(@key)
236
+
237
+ @cache.transaction do
238
+ @cache.set(@key, @value)
239
+ end
240
+ end
241
+
242
+ it "does not acquire locks on reads" do
243
+ mock($lock).acquire_lock.never
244
+ mock($lock).release_lock.never
245
+
246
+ @cache.transaction do
247
+ @cache.get(@key)
248
+ end
249
+ end
250
+
251
+ it "locks @keys in lexically sorted order" do
252
+ keys = ['c', 'a', 'b']
253
+ keys.sort.inject(mock($lock)) do |mock, key|
254
+ mock.acquire_lock(key).then
255
+ end
256
+ keys.each { |key| mock($memcache).set(key, @value) }
257
+ keys.each { |key| mock($lock).release_lock(key) }
258
+ @cache.transaction do
259
+ @cache.set(keys[0], @value)
260
+ @cache.set(keys[1], @value)
261
+ @cache.set(keys[2], @value)
262
+ end
263
+ end
264
+
265
+ it "releases locks even if memcache blows up" do
266
+ mock($lock).acquire_lock.with(@key)
267
+ mock($lock).release_lock.with(@key)
268
+ stub($memcache).set(anything, anything) { raise }
269
+ @cache.transaction do
270
+ @cache.set(@key, @value)
271
+ end rescue nil
272
+ end
273
+
274
+ end
275
+
276
+ describe 'Buffering' do
277
+ it "reading from the cache show uncommitted writes" do
278
+ @cache.get(@key).should == nil
279
+ @cache.transaction do
280
+ @cache.set(@key, @value)
281
+ @cache.get(@key).should == @value
282
+ end
283
+ end
284
+
285
+ it "get_multi is buffered" do
286
+ @cache.transaction do
287
+ @cache.set('key1', @value)
288
+ @cache.set('key2', @value)
289
+ @cache.get_multi('key1', 'key2').should == { 'key1' => @value, 'key2' => @value }
290
+ $memcache.get_multi('key1', 'key2').should == {}
291
+ end
292
+ end
293
+
294
+ it "get is memoized" do
295
+ @cache.set(@key, @value)
296
+ @cache.transaction do
297
+ @cache.get(@key).should == @value
298
+ $memcache.set(@key, "new value")
299
+ @cache.get(@key).should == @value
300
+ end
301
+ end
302
+
303
+ it "add is buffered" do
304
+ @cache.transaction do
305
+ @cache.add(@key, @value)
306
+ $memcache.get(@key).should == nil
307
+ @cache.get(@key).should == @value
308
+ end
309
+ @cache.get(@key).should == @value
310
+ $memcache.get(@key).should == @value
311
+ end
312
+
313
+ describe '#delete' do
314
+ it "within a transaction, delete is isolated" do
315
+ @cache.add(@key, @value)
316
+ @cache.transaction do
317
+ @cache.delete(@key)
318
+ $memcache.add(@key, "another value")
319
+ end
320
+ @cache.get(@key).should == nil
321
+ $memcache.get(@key).should == nil
322
+ end
323
+
324
+ it "within a transaction, delete is buffered" do
325
+ @cache.set(@key, @value)
326
+ @cache.transaction do
327
+ @cache.delete(@key)
328
+ $memcache.get(@key).should == @value
329
+ @cache.get(@key).should == nil
330
+ end
331
+ @cache.get(@key).should == nil
332
+ $memcache.get(@key).should == nil
333
+ end
334
+ end
335
+ end
336
+
337
+ describe '#incr' do
338
+ it "increment be atomic" do
339
+ @cache.set(@key, 0, 0, true)
340
+ @cache.transaction do
341
+ @cache.incr(@key)
342
+ $memcache.incr(@key)
343
+ end
344
+ @cache.get(@key, true).to_i.should == 2
345
+ $memcache.get(@key, true).to_i.should == 2
346
+ end
347
+
348
+ it "interleaved, etc. increments and sets be ordered" do
349
+ @cache.set(@key, 0, 0, true)
350
+ @cache.transaction do
351
+ @cache.incr(@key)
352
+ @cache.incr(@key)
353
+ @cache.set(@key, 0, 0, true)
354
+ @cache.incr(@key)
355
+ @cache.incr(@key)
356
+ end
357
+ @cache.get(@key, true).to_i.should == 2
358
+ $memcache.get(@key, true).to_i.should == 2
359
+ end
360
+ end
361
+
362
+ describe '#decr' do
363
+ it "decrement be atomic" do
364
+ @cache.set(@key, 0, 0, true)
365
+ @cache.incr(@key, 3)
366
+ @cache.transaction do
367
+ @cache.decr(@key)
368
+ $memcache.decr(@key)
369
+ end
370
+ @cache.get(@key, true).to_i.should == 1
371
+ $memcache.get(@key, true).to_i.should == 1
372
+ end
373
+ end
374
+
375
+ it "retains the value in the transactional cache after committing the transaction" do
376
+ @cache.get(@key).should == nil
377
+ @cache.transaction do
378
+ @cache.set(@key, @value)
379
+ end
380
+ @cache.get(@key).should == @value
381
+ end
382
+
383
+ describe 'when reading from the memcache' do
384
+ it "does NOT show uncommitted writes" do
385
+ @cache.transaction do
386
+ $memcache.get(@key).should == nil
387
+ @cache.set(@key, @value)
388
+ $memcache.get(@key).should == nil
389
+ end
390
+ end
391
+ end
392
+ end
393
+
394
+ describe 'Exception Handling' do
395
+
396
+ it "re-raises exceptions thrown by memcache" do
397
+ stub($memcache).set(anything, anything) { raise }
398
+ lambda do
399
+ @cache.transaction do
400
+ @cache.set(@key, @value)
401
+ end
402
+ end.should raise_error
403
+ end
404
+
405
+ it "rolls back transaction cleanly if an exception is raised" do
406
+ $memcache.get(@key).should == nil
407
+ @cache.get(@key).should == nil
408
+ @cache.transaction do
409
+ @cache.set(@key, @value)
410
+ raise
411
+ end rescue nil
412
+ @cache.get(@key).should == nil
413
+ $memcache.get(@key).should == nil
414
+ end
415
+
416
+ it "does not acquire locks if transaction is rolled back" do
417
+ mock($lock).acquire_lock.never
418
+ mock($lock).release_lock.never
419
+
420
+ @cache.transaction do
421
+ @cache.set(@key, value)
422
+ raise
423
+ end rescue nil
424
+ end
425
+ end
426
+
427
+ describe 'Nested Transactions' do
428
+ it "delegate unsupported messages back to the real cache" do
429
+ @cache.transaction do
430
+ @cache.transaction do
431
+ @cache.transaction do
432
+ mock($memcache).foo(:bar)
433
+ @cache.foo(:bar)
434
+ end
435
+ end
436
+ end
437
+ end
438
+
439
+ it "makes newly set keys only be visible within the transaction in which they were set" do
440
+ @cache.transaction do
441
+ @cache.set('key1', @value)
442
+ @cache.transaction do
443
+ @cache.get('key1').should == @value
444
+ @cache.set('key2', @value)
445
+ @cache.transaction do
446
+ @cache.get('key1').should == @value
447
+ @cache.get('key2').should == @value
448
+ @cache.set('key3', @value)
449
+ end
450
+ end
451
+ @cache.get('key1').should == @value
452
+ @cache.get('key2').should == @value
453
+ @cache.get('key3').should == @value
454
+ end
455
+ @cache.get('key1').should == @value
456
+ @cache.get('key2').should == @value
457
+ @cache.get('key3').should == @value
458
+ end
459
+
460
+ it "not write any values to memcache until the outermost transaction commits" do
461
+ @cache.transaction do
462
+ @cache.set('key1', @value)
463
+ @cache.transaction do
464
+ @cache.set('key2', @value)
465
+ $memcache.get('key1').should == nil
466
+ $memcache.get('key2').should == nil
467
+ end
468
+ $memcache.get('key1').should == nil
469
+ $memcache.get('key2').should == nil
470
+ end
471
+ $memcache.get('key1').should == @value
472
+ $memcache.get('key2').should == @value
473
+ end
474
+
475
+ it "acquire locks in lexical order for all keys" do
476
+ keys = ['c', 'a', 'b']
477
+ keys.sort.inject(mock($lock)) do |mock, key|
478
+ mock.acquire_lock(key).then
479
+ end
480
+ keys.each { |key| mock($memcache).set(key, @value) }
481
+ keys.each { |key| mock($lock).release_lock(key) }
482
+ @cache.transaction do
483
+ @cache.set(keys[0], @value)
484
+ @cache.transaction do
485
+ @cache.set(keys[1], @value)
486
+ @cache.transaction do
487
+ @cache.set(keys[2], @value)
488
+ end
489
+ end
490
+ end
491
+ end
492
+
493
+ it "reads through the real memcache if key has not been written to in a transaction" do
494
+ $memcache.set(@key, @value)
495
+ @cache.transaction do
496
+ @cache.transaction do
497
+ @cache.transaction do
498
+ @cache.get(@key).should == @value
499
+ end
500
+ end
501
+ end
502
+ @cache.get(@key).should == @value
503
+ end
504
+
505
+ describe 'Error Handling' do
506
+ it "releases locks even if memcache blows up" do
507
+ mock($lock).acquire_lock(@key)
508
+ mock($lock).release_lock(@key)
509
+ stub($memcache).set(anything, anything) { raise }
510
+ @cache.transaction do
511
+ @cache.transaction do
512
+ @cache.transaction do
513
+ @cache.set(@key, @value)
514
+ end
515
+ end
516
+ end rescue nil
517
+ end
518
+
519
+ it "re-raise exceptions thrown by memcache" do
520
+ stub($memcache).set(anything, anything) { raise }
521
+ lambda do
522
+ @cache.transaction do
523
+ @cache.transaction do
524
+ @cache.transaction do
525
+ @cache.set(@key, @value)
526
+ end
527
+ end
528
+ end
529
+ end.should raise_error
530
+ end
531
+
532
+ it "rollback transaction cleanly if an exception is raised" do
533
+ $memcache.get(@key).should == nil
534
+ @cache.get(@key).should == nil
535
+ @cache.transaction do
536
+ @cache.transaction do
537
+ @cache.set(@key, @value)
538
+ raise
539
+ end
540
+ end rescue nil
541
+ @cache.get(@key).should == nil
542
+ $memcache.get(@key).should == nil
543
+ end
544
+
545
+ it "not acquire locks if transaction is rolled back" do
546
+ mock($lock).acquire_lock.never
547
+ mock($lock).release_lock.never
548
+
549
+ @cache.transaction do
550
+ @cache.transaction do
551
+ @cache.set(@key, @value)
552
+ raise
553
+ end
554
+ end rescue nil
555
+ end
556
+
557
+ it "support rollbacks" do
558
+ @cache.transaction do
559
+ @cache.set('key1', @value)
560
+ @cache.transaction do
561
+ @cache.get('key1').should == @value
562
+ @cache.set('key2', @value)
563
+ raise
564
+ end rescue nil
565
+ @cache.get('key1').should == @value
566
+ @cache.get('key2').should == nil
567
+ end
568
+ $memcache.get('key1').should == @value
569
+ $memcache.get('key2').should == nil
570
+ end
571
+ end
572
+ end
573
+
574
+ it "should have method_missing as a private method" do
575
+ Transactional.private_instance_methods.should include("method_missing")
576
+ end
577
+ end
578
+ end