dp_stm_map 0.0.2
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.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +622 -0
- data/README.md +115 -0
- data/Rakefile +1 -0
- data/bin/dp_map_manager.rb +47 -0
- data/cucumber.yml +2 -0
- data/dp_stm_map.gemspec +29 -0
- data/features/client_reconnect.feature +13 -0
- data/features/persistence.feature +12 -0
- data/features/replication.feature +9 -0
- data/features/running_manager.feature +19 -0
- data/features/step_definitions/client_reconnect_steps.rb +28 -0
- data/features/step_definitions/persistence_steps.rb +49 -0
- data/features/step_definitions/replication_steps.rb +15 -0
- data/features/step_definitions/running_server_steps.rb +10 -0
- data/features/step_definitions/transaction_fail_steps.rb +11 -0
- data/features/support/env.rb +80 -0
- data/features/transaction_fail.feature +7 -0
- data/lib/dp_stm_map/Client.rb +268 -0
- data/lib/dp_stm_map/ClientLocalStore.rb +119 -0
- data/lib/dp_stm_map/InMemoryStmMap.rb +147 -0
- data/lib/dp_stm_map/Manager.rb +370 -0
- data/lib/dp_stm_map/Message.rb +126 -0
- data/lib/dp_stm_map/ObjectStore.rb +99 -0
- data/lib/dp_stm_map/version.rb +16 -0
- data/lib/dp_stm_map.rb +20 -0
- data/server.profile +547 -0
- data/spec/dp_stm_map/ClientLocalStore_spec.rb +78 -0
- data/spec/dp_stm_map/Client_spec.rb +133 -0
- data/spec/dp_stm_map/InMemoryStmMap_spec.rb +10 -0
- data/spec/dp_stm_map/Manager_spec.rb +323 -0
- data/spec/dp_stm_map/Message_spec.rb +21 -0
- data/spec/dp_stm_map/ObjectStore_spec.rb +87 -0
- data/spec/dp_stm_map/StmMap_shared.rb +432 -0
- metadata +235 -0
@@ -0,0 +1,133 @@
|
|
1
|
+
require 'dp_stm_map'
|
2
|
+
|
3
|
+
require 'thread'
|
4
|
+
require 'timeout'
|
5
|
+
require 'tmpdir'
|
6
|
+
require 'fileutils'
|
7
|
+
Dir["./spec/**/*_shared.rb"].sort.each {|f| require f}
|
8
|
+
|
9
|
+
module DpStmMap
|
10
|
+
|
11
|
+
describe DistributedPersistentStmMap do
|
12
|
+
|
13
|
+
before do
|
14
|
+
@storage_dir=Dir.mktmpdir
|
15
|
+
Dir.mkdir "%s/server" % @storage_dir
|
16
|
+
Dir.mkdir "%s/local" % @storage_dir
|
17
|
+
server.start
|
18
|
+
end
|
19
|
+
|
20
|
+
after do
|
21
|
+
server.stop
|
22
|
+
FileUtils.rm_rf(@storage_dir)
|
23
|
+
subject.stop
|
24
|
+
end
|
25
|
+
|
26
|
+
let(:server) {Manager.new port, "%s/server" % @storage_dir}
|
27
|
+
|
28
|
+
let(:port) {31337}
|
29
|
+
|
30
|
+
subject { DistributedPersistentStmMap.new 'localhost', port, "%s/local" % @storage_dir }
|
31
|
+
|
32
|
+
|
33
|
+
describe :map_behaviour do
|
34
|
+
|
35
|
+
before do
|
36
|
+
queue=Queue.new
|
37
|
+
subject.on_connected do
|
38
|
+
queue << "connected"
|
39
|
+
end
|
40
|
+
subject.start
|
41
|
+
timeout(1) do
|
42
|
+
queue.pop.should == "connected"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
it_behaves_like "StmMap"
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
it "should reconnect to server that has rebooted" do
|
52
|
+
queue=Queue.new
|
53
|
+
subject.on_connected do
|
54
|
+
queue << true
|
55
|
+
end
|
56
|
+
subject.start
|
57
|
+
end
|
58
|
+
|
59
|
+
describe :on_connected do
|
60
|
+
it "should be executed when client did connect to the manager" do
|
61
|
+
|
62
|
+
queue=Queue.new
|
63
|
+
subject.on_disconnected do
|
64
|
+
queue << "disconnected"
|
65
|
+
end
|
66
|
+
subject.on_connected do
|
67
|
+
queue << "connected"
|
68
|
+
end
|
69
|
+
subject.start
|
70
|
+
timeout(1) do
|
71
|
+
queue.pop.should == "connected"
|
72
|
+
end
|
73
|
+
|
74
|
+
sleep 0.1
|
75
|
+
server.stop
|
76
|
+
|
77
|
+
timeout(5) do
|
78
|
+
queue.pop.should == "disconnected"
|
79
|
+
end
|
80
|
+
|
81
|
+
server.start
|
82
|
+
|
83
|
+
timeout(5) do
|
84
|
+
queue.pop.should == "connected"
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe :on_disconnected do
|
91
|
+
it "should be executed when client did disconnect from the manager" do
|
92
|
+
queue=Queue.new
|
93
|
+
subject.on_disconnected do
|
94
|
+
queue << "disconnected"
|
95
|
+
end
|
96
|
+
subject.on_connected do
|
97
|
+
queue << "connected"
|
98
|
+
end
|
99
|
+
subject.start
|
100
|
+
|
101
|
+
timeout(1) do
|
102
|
+
queue.pop.should == "connected"
|
103
|
+
end
|
104
|
+
|
105
|
+
subject.stop
|
106
|
+
|
107
|
+
timeout(1) do
|
108
|
+
queue.pop.should == "disconnected"
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe :stop do
|
114
|
+
it "should stop client" do
|
115
|
+
queue=Queue.new
|
116
|
+
subject.on_disconnected do
|
117
|
+
queue << "disconnected"
|
118
|
+
end
|
119
|
+
subject.on_connected do
|
120
|
+
queue << "connected"
|
121
|
+
end
|
122
|
+
subject.start
|
123
|
+
timeout(1) do
|
124
|
+
queue.pop.should == "connected"
|
125
|
+
end
|
126
|
+
subject.stop
|
127
|
+
timeout(1) do
|
128
|
+
queue.pop.should == "disconnected"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,323 @@
|
|
1
|
+
require 'dp_stm_map'
|
2
|
+
require 'tmpdir'
|
3
|
+
require 'yaml'
|
4
|
+
require 'set'
|
5
|
+
|
6
|
+
def send_message socket, message
|
7
|
+
yaml=message.serialize
|
8
|
+
socket.write([yaml.bytesize].pack("Q>"))
|
9
|
+
socket.write(yaml)
|
10
|
+
socket.flush
|
11
|
+
socket.rewind
|
12
|
+
end
|
13
|
+
|
14
|
+
module DpStmMap
|
15
|
+
|
16
|
+
|
17
|
+
describe CurrentValues do
|
18
|
+
|
19
|
+
before do
|
20
|
+
@storage_dir=Dir.mktmpdir
|
21
|
+
end
|
22
|
+
|
23
|
+
after do
|
24
|
+
FileUtils.rm_rf(@storage_dir)
|
25
|
+
end
|
26
|
+
|
27
|
+
subject {CurrentValues.new @storage_dir}
|
28
|
+
|
29
|
+
it "should store a new value" do
|
30
|
+
subject['x']='y'
|
31
|
+
subject['x'].should == 'y'
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
it "should change existing value" do
|
36
|
+
subject['x']='y'
|
37
|
+
subject['x']='z'
|
38
|
+
subject['x'].should == 'z'
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should store nil" do
|
42
|
+
subject['x']=nil
|
43
|
+
subject['x'].should be_nil
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
47
|
+
|
48
|
+
|
49
|
+
|
50
|
+
describe TransactionLog do
|
51
|
+
|
52
|
+
before do
|
53
|
+
@storage_dir=Dir.mktmpdir
|
54
|
+
end
|
55
|
+
|
56
|
+
after do
|
57
|
+
FileUtils.rm_rf(@storage_dir)
|
58
|
+
end
|
59
|
+
|
60
|
+
subject {TransactionLog.new @storage_dir}
|
61
|
+
|
62
|
+
describe :store_transaction do
|
63
|
+
it "should store first transaction with id 1" do
|
64
|
+
subject.store_transaction({'x' => 'abc'},{'abc' => 'some data'},[]).should == 1
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
|
69
|
+
describe :add_listener do
|
70
|
+
it "should call listener with transaction when new transaction is available" do
|
71
|
+
queue=Queue.new
|
72
|
+
subject.add_listener(0) do |txid, change|
|
73
|
+
queue << [txid, change]
|
74
|
+
end
|
75
|
+
subject.store_transaction({'x' => 'abc'},{'abc' => 'some data'},[])
|
76
|
+
queue.pop.should == [1,[{'x' => 'abc'},{'abc' => 'some data'},[]]]
|
77
|
+
end
|
78
|
+
|
79
|
+
|
80
|
+
it "should call listener for all missed transactions" do
|
81
|
+
queue=Queue.new
|
82
|
+
subject.store_transaction({'x' => 'abc'},{'abc' => 'some data'},[])
|
83
|
+
subject.add_listener(0) do |txid, change|
|
84
|
+
queue << [txid, change]
|
85
|
+
end
|
86
|
+
queue.pop.should == [1,[{'x' => 'abc'},{'abc' => 'some data'},[]]]
|
87
|
+
end
|
88
|
+
|
89
|
+
|
90
|
+
it "should call all registered listeners" do
|
91
|
+
queue1=Queue.new
|
92
|
+
queue2=Queue.new
|
93
|
+
subject.store_transaction({'x' => 'abc'},{'abc' => 'some data'},[])
|
94
|
+
subject.add_listener(0) do |txid, change|
|
95
|
+
queue1 << [txid, change]
|
96
|
+
end
|
97
|
+
subject.add_listener(0) do |txid, change|
|
98
|
+
queue2 << [txid, change]
|
99
|
+
end
|
100
|
+
queue1.pop.should == [1,[{'x' => 'abc'},{'abc' => 'some data'},[]]]
|
101
|
+
queue2.pop.should == [1,[{'x' => 'abc'},{'abc' => 'some data'},[]]]
|
102
|
+
end
|
103
|
+
|
104
|
+
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
|
109
|
+
describe ReferenceCounts do
|
110
|
+
|
111
|
+
before do
|
112
|
+
@storage_dir=Dir.mktmpdir
|
113
|
+
end
|
114
|
+
|
115
|
+
after do
|
116
|
+
FileUtils.rm_rf(@storage_dir)
|
117
|
+
end
|
118
|
+
|
119
|
+
|
120
|
+
subject {ReferenceCounts.new @storage_dir}
|
121
|
+
|
122
|
+
|
123
|
+
describe :add_reference do
|
124
|
+
it "should record new reference" do
|
125
|
+
subject.add_reference 'abc'
|
126
|
+
subject.should have_references('abc')
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe :remove_reference do
|
131
|
+
it "should remove existing reference" do
|
132
|
+
subject.add_reference 'abc'
|
133
|
+
subject.remove_reference 'abc'
|
134
|
+
subject.should_not have_references('abc')
|
135
|
+
end
|
136
|
+
|
137
|
+
it "should raise exception when reference does not exist" do
|
138
|
+
expect{subject.remove_reference 'abc'}.to raise_error
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe :has_references? do
|
143
|
+
context "when there is one reference" do
|
144
|
+
before {subject.add_reference 'abc'}
|
145
|
+
it "should return true" do
|
146
|
+
subject.should have_references('abc')
|
147
|
+
end
|
148
|
+
end
|
149
|
+
context "when there are no references" do
|
150
|
+
it "should return false" do
|
151
|
+
subject.should_not have_references('abc')
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
describe ClientTransactionManager do
|
160
|
+
|
161
|
+
let(:current_values) {double("current values")}
|
162
|
+
let(:reference_counts) {double("reference counts")}
|
163
|
+
let(:transaction_log) {double("transaction log")}
|
164
|
+
|
165
|
+
subject {ClientTransactionManager.new current_values, reference_counts, transaction_log}
|
166
|
+
|
167
|
+
it "should apply valid transaction" do
|
168
|
+
current_values.should_receive(:[]).with('x').and_return(nil)
|
169
|
+
current_values.should_receive(:[]=).with('x','abc')
|
170
|
+
reference_counts.should_receive(:has_references?).with('abc').and_return(false)
|
171
|
+
reference_counts.should_receive(:add_reference).with('abc')
|
172
|
+
transaction_log.should_receive(:store_transaction).with({'x' => 'abc'},{'abc' => 'some value'}, []).and_return(1)
|
173
|
+
subject.apply_transaction({'x' => [nil,'abc']},{'abc' => 'some value'}).should == 1
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should increase reference count of the new value" do
|
177
|
+
current_values.should_receive(:[]).with('x').and_return('def')
|
178
|
+
current_values.should_receive(:[]=).with('x','abc')
|
179
|
+
reference_counts.should_receive(:has_references?).with('abc').and_return(false)
|
180
|
+
reference_counts.should_receive(:has_references?).with('def').and_return(false)
|
181
|
+
reference_counts.should_receive(:add_reference).with('abc')
|
182
|
+
reference_counts.should_receive(:remove_reference).with('def')
|
183
|
+
transaction_log.should_receive(:store_transaction).with({'x' => 'abc'},{'abc' => 'some value'}, ['def'])
|
184
|
+
subject.apply_transaction({'x' => ['def','abc']},{'abc' => 'some value'})
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should not increase reference count of nil" do
|
188
|
+
current_values.should_receive(:[]).with('x').and_return('def')
|
189
|
+
current_values.should_receive(:[]=).with('x',nil)
|
190
|
+
reference_counts.should_receive(:remove_reference).with('def')
|
191
|
+
reference_counts.should_receive(:has_references?).with('def').and_return(false)
|
192
|
+
transaction_log.should_receive(:store_transaction).with({'x' => nil},{}, ['def'])
|
193
|
+
subject.apply_transaction({'x' => ['def',nil]},{})
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should refuse transaction referencing not existing data" do
|
197
|
+
current_values.should_receive(:[]).with('x').and_return(nil)
|
198
|
+
reference_counts.should_receive(:has_references?).with('abc').and_return(false)
|
199
|
+
expect {subject.apply_transaction({'x' => [nil,'abc']},{})}.to raise_error StaleTransactionError
|
200
|
+
end
|
201
|
+
|
202
|
+
it "should apply changes if nothing has changed" do
|
203
|
+
current_values.should_receive(:[]).with('x').and_return('abc')
|
204
|
+
transaction_log.should_receive(:store_transaction).with({},{}, [])
|
205
|
+
subject.apply_transaction({'x' => ['abc','abc']},{})
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should refuse stale transaction" do
|
209
|
+
current_values.should_receive(:[]).with('x').and_return('def')
|
210
|
+
expect {subject.apply_transaction({'x' => [nil,'abc']},{'abc' => 'some value'})}.to raise_error StaleTransactionError
|
211
|
+
end
|
212
|
+
|
213
|
+
|
214
|
+
end
|
215
|
+
|
216
|
+
|
217
|
+
|
218
|
+
describe Manager do
|
219
|
+
|
220
|
+
|
221
|
+
let (:port) {0}
|
222
|
+
|
223
|
+
|
224
|
+
before do
|
225
|
+
@storage_dir=Dir.mktmpdir
|
226
|
+
subject.start
|
227
|
+
end
|
228
|
+
|
229
|
+
after do
|
230
|
+
FileUtils.rm_rf(@storage_dir)
|
231
|
+
begin
|
232
|
+
subject.stop
|
233
|
+
rescue => ex
|
234
|
+
STDERR.puts "#{ex.backtrace.join("\n")}: #{ex.message} (#{ex.class})"
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
|
239
|
+
before do
|
240
|
+
@storage_dir=Dir.mktmpdir
|
241
|
+
end
|
242
|
+
|
243
|
+
subject {Manager.new port, @storage_dir}
|
244
|
+
|
245
|
+
it "should start and stop" do
|
246
|
+
TCPSocket.new 'localhost',subject.port
|
247
|
+
end
|
248
|
+
|
249
|
+
end
|
250
|
+
|
251
|
+
|
252
|
+
describe SocketTransport do
|
253
|
+
|
254
|
+
let (:socket) {StringIO.new}
|
255
|
+
|
256
|
+
subject {SocketTransport.new socket}
|
257
|
+
|
258
|
+
describe :next_message do
|
259
|
+
it "should read sent message" do
|
260
|
+
send_message(socket, ClientHelloMessage.new(0))
|
261
|
+
subject.next_message.should be_a ClientHelloMessage
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
describe :send_message do
|
266
|
+
it "should serialize message" do
|
267
|
+
subject.send_message ClientHelloMessage.new(1)
|
268
|
+
socket.rewind
|
269
|
+
subject.next_message.should be_a ClientHelloMessage
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
describe :close do
|
274
|
+
it "should close socket" do
|
275
|
+
subject.close
|
276
|
+
socket.should be_closed
|
277
|
+
end
|
278
|
+
end
|
279
|
+
end
|
280
|
+
|
281
|
+
|
282
|
+
describe ClientHandler do
|
283
|
+
|
284
|
+
let (:client_transport) {double("client transport")}
|
285
|
+
let (:transaction_manager) {double("transaction manager")}
|
286
|
+
|
287
|
+
|
288
|
+
subject {ClientHandler.new client_transport, transaction_manager}
|
289
|
+
|
290
|
+
|
291
|
+
# it "should send server hello message to client" do
|
292
|
+
# transaction_log.should_receive(:register_listener).with(0)
|
293
|
+
# client_transport.should_receive(:send_message).with(an_instance_of(ServerHelloMessage))
|
294
|
+
# subject.handle(ClientHelloMessage.new 0)
|
295
|
+
# end
|
296
|
+
|
297
|
+
|
298
|
+
it "should send transaction messages for all previous state transitions" do
|
299
|
+
client_transport.should_receive(:send_message).with(an_instance_of(TransactionMessage))
|
300
|
+
subject.handle(TransactionMessage.new(1, {},{}, []))
|
301
|
+
end
|
302
|
+
|
303
|
+
it "should invoke transaction manager operations on client transaction messages" do
|
304
|
+
transaction_manager.should_receive(:apply_transaction).with({"x"=>["a", "b"]}, {"b"=>"content"}).and_return(1)
|
305
|
+
client_transport.should_receive(:send_message) do |msg|
|
306
|
+
msg.should be_a ClientTransactionSuccessfulMessage
|
307
|
+
msg.transaction_sequence.should == 1
|
308
|
+
end
|
309
|
+
subject.handle(ClientTransactionMessage.new('tx1',{'x' => ['a','b']},{'b' => 'content'}))
|
310
|
+
end
|
311
|
+
|
312
|
+
it "should send ClientTransactionFailedMessage when transaction fails" do
|
313
|
+
transaction_manager.should_receive(:apply_transaction).with({"x"=>["a", "b"]}, {"b"=>"content"}).and_raise(StaleTransactionError)
|
314
|
+
client_transport.should_receive(:send_message).with(an_instance_of(ClientTransactionFailedMessage))
|
315
|
+
subject.handle(ClientTransactionMessage.new('tx1',{'x' => ['a','b']},{'b' => 'content'}))
|
316
|
+
end
|
317
|
+
|
318
|
+
|
319
|
+
|
320
|
+
|
321
|
+
|
322
|
+
end
|
323
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'dp_stm_map/Message'
|
2
|
+
|
3
|
+
module DpStmMap
|
4
|
+
|
5
|
+
describe JsonMessage do
|
6
|
+
|
7
|
+
subject {ServerHelloMessage.new}
|
8
|
+
|
9
|
+
# it "should be serializable to json" do
|
10
|
+
# subject.serialize.should == "{\"type\":\"DpStmMap::ServerHelloMessage\"}"
|
11
|
+
# end
|
12
|
+
|
13
|
+
# it "should be deserializable from json" do
|
14
|
+
# JsonMessage.deserialize("{\"type\":\"DpStmMap::ServerHelloMessage\"}").should be_a ServerHelloMessage
|
15
|
+
# end
|
16
|
+
|
17
|
+
|
18
|
+
end
|
19
|
+
|
20
|
+
|
21
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'dp_stm_map'
|
2
|
+
|
3
|
+
module DpStmMap
|
4
|
+
|
5
|
+
class SomeObject
|
6
|
+
attr_accessor :value
|
7
|
+
|
8
|
+
def == other
|
9
|
+
case other
|
10
|
+
when SomeObject
|
11
|
+
other.value == self.value
|
12
|
+
else
|
13
|
+
false
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
describe ObjectStore do
|
19
|
+
|
20
|
+
|
21
|
+
let(:some_object) {v=SomeObject.new; v.value='v1'; v}
|
22
|
+
|
23
|
+
subject {ObjectStore.new InMemoryStmMap.new}
|
24
|
+
|
25
|
+
|
26
|
+
describe :on_atomic do
|
27
|
+
it "should be executed after atomic change" do
|
28
|
+
changes=nil
|
29
|
+
subject.on_atomic do |c|
|
30
|
+
changes=c
|
31
|
+
end
|
32
|
+
subject.atomic do |tx|
|
33
|
+
tx[:user,'abc']=some_object
|
34
|
+
end
|
35
|
+
changes.should == {[:user, 'abc'] => [nil, some_object]}
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
|
40
|
+
|
41
|
+
describe :validate_atomic do
|
42
|
+
it "should stop transaction when exception is raised" do
|
43
|
+
|
44
|
+
changes=nil
|
45
|
+
subject.validate_atomic do |c|
|
46
|
+
changes=c
|
47
|
+
raise "something went wrong"
|
48
|
+
end
|
49
|
+
|
50
|
+
expect do
|
51
|
+
subject.atomic do |tx|
|
52
|
+
tx[:user,'abc']=some_object
|
53
|
+
end
|
54
|
+
end.to raise_error "something went wrong"
|
55
|
+
|
56
|
+
changes.should == {[:user, 'abc'] => [nil, some_object]}
|
57
|
+
|
58
|
+
subject.atomic_read do |tx|
|
59
|
+
tx[:user,'abc'].should == nil
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe :atomic do
|
66
|
+
it "should store Ruby objects" do
|
67
|
+
subject.atomic do |tx|
|
68
|
+
tx[:user,'abc']=some_object
|
69
|
+
end
|
70
|
+
|
71
|
+
subject.atomic_read do |tx|
|
72
|
+
tx.should have_key(:user, 'abc')
|
73
|
+
tx[:user,'abc'].should == some_object
|
74
|
+
end
|
75
|
+
end
|
76
|
+
it "should store nil values" do
|
77
|
+
subject.atomic do |tx|
|
78
|
+
tx[:user,'abc']=nil
|
79
|
+
end
|
80
|
+
|
81
|
+
subject.atomic_read do |tx|
|
82
|
+
tx[:user,'abc'].should == nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|