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