zeevex_concurrency 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/.gitignore +17 -0
- data/Gemfile +16 -0
- data/LICENSE.txt +22 -0
- data/README.md +29 -0
- data/Rakefile +1 -0
- data/lib/zeevex_concurrency/delay.rb +50 -0
- data/lib/zeevex_concurrency/delayed.rb +233 -0
- data/lib/zeevex_concurrency/event_loop.rb +154 -0
- data/lib/zeevex_concurrency/future.rb +60 -0
- data/lib/zeevex_concurrency/logging.rb +7 -0
- data/lib/zeevex_concurrency/nil_logger.rb +7 -0
- data/lib/zeevex_concurrency/promise.rb +32 -0
- data/lib/zeevex_concurrency/synchronized.rb +46 -0
- data/lib/zeevex_concurrency/thread_pool.rb +346 -0
- data/lib/zeevex_concurrency/version.rb +3 -0
- data/lib/zeevex_concurrency.rb +29 -0
- data/script/repl +10 -0
- data/script/testall +2 -0
- data/spec/delay_spec.rb +172 -0
- data/spec/delayed_spec.rb +104 -0
- data/spec/event_loop_spec.rb +161 -0
- data/spec/future_spec.rb +316 -0
- data/spec/promise_spec.rb +172 -0
- data/spec/spec_helper.rb +8 -0
- data/spec/thread_pool_spec.rb +281 -0
- data/zeevex_concurrency.gemspec +30 -0
- metadata +187 -0
@@ -0,0 +1,161 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
require 'zeevex_concurrency/promise.rb'
|
3
|
+
require 'zeevex_concurrency/event_loop.rb'
|
4
|
+
|
5
|
+
describe ZeevexConcurrency::EventLoop do
|
6
|
+
let :loop do
|
7
|
+
ZeevexConcurrency::EventLoop.new
|
8
|
+
end
|
9
|
+
before do
|
10
|
+
loop.start
|
11
|
+
end
|
12
|
+
let :queue do
|
13
|
+
Queue.new
|
14
|
+
end
|
15
|
+
|
16
|
+
before do
|
17
|
+
queue
|
18
|
+
end
|
19
|
+
|
20
|
+
around :each do |ex|
|
21
|
+
Timeout::timeout(15) do
|
22
|
+
ex.run
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context 'basic usage' do
|
27
|
+
it 'should allow enqueue of a proc' do
|
28
|
+
loop.enqueue(Proc.new { true }).should be_a(ZeevexConcurrency::Promise)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should allow enqueue of a block' do
|
32
|
+
loop.enqueue do
|
33
|
+
true
|
34
|
+
end.should be_a(ZeevexConcurrency::Promise)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'should allow enqueue of a Promise, and return same promise' do
|
38
|
+
promise = ZeevexConcurrency::Promise.new(Proc.new {true})
|
39
|
+
loop.enqueue(promise).should == promise
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'running tasks asynchronously' do
|
44
|
+
|
45
|
+
|
46
|
+
it 'should execute the task on the event loop' do
|
47
|
+
loop.enqueue { queue << Thread.current.__id__ }
|
48
|
+
queue.pop.should_not == Thread.current.__id__
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should return the callable\'s value in the returned promise' do
|
52
|
+
res = loop.enqueue { 100 * 2 }
|
53
|
+
res.value.should == 200
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should update the promise only when ready' do
|
57
|
+
res = loop.enqueue { queue.pop; "foo" }
|
58
|
+
res.should_not be_ready
|
59
|
+
queue << "go ahead"
|
60
|
+
res.value.should == "foo"
|
61
|
+
end
|
62
|
+
|
63
|
+
it 'should allow enqueueing from the event loop, and execute in order' do
|
64
|
+
loop.enqueue do
|
65
|
+
# runs after this block finishes
|
66
|
+
loop.enqueue { queue << "val2" }
|
67
|
+
queue << "val1"
|
68
|
+
end
|
69
|
+
[queue.pop, queue.pop].should == ["val1", "val2"]
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context '#on_event_loop' do
|
74
|
+
it 'should execute the task asynchronously from client code' do
|
75
|
+
loop.on_event_loop { queue << Thread.current.__id__ }.wait
|
76
|
+
queue.pop.should_not == Thread.current.__id__
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should execute the task synchronously when called from event loop' do
|
80
|
+
loop.enqueue do
|
81
|
+
loop.on_event_loop { queue << "foo" }
|
82
|
+
res = queue.pop
|
83
|
+
queue << "done"
|
84
|
+
end.wait
|
85
|
+
queue.pop.should == "done"
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context '#run_and_wait' do
|
90
|
+
it 'should not return a promise, but the result of the computation' do
|
91
|
+
loop.run_and_wait { queue }.should == queue
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should execute the task on a different thread from client code' do
|
95
|
+
loop.run_and_wait { queue << Thread.current.__id__ }
|
96
|
+
queue.pop.should_not == Thread.current.__id__
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should execute the task synchronously when called from event loop' do
|
100
|
+
res = loop.run_and_wait do
|
101
|
+
loop.run_and_wait { queue << "foo" }
|
102
|
+
queue << "done"
|
103
|
+
queue.pop.should == "foo"
|
104
|
+
"hey"
|
105
|
+
end
|
106
|
+
queue.pop.should == "done"
|
107
|
+
res.should == "hey"
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
context 'null event loop' do
|
112
|
+
let :loop do
|
113
|
+
ZeevexConcurrency::EventLoop::Null.new
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'should not run the callable provided' do
|
117
|
+
foo = 100
|
118
|
+
promise = loop.enqueue do
|
119
|
+
foo += 1
|
120
|
+
end
|
121
|
+
promise.should be_ready
|
122
|
+
foo.should == 100
|
123
|
+
end
|
124
|
+
|
125
|
+
it 'should return nil in the promise' do
|
126
|
+
promise = loop.enqueue do
|
127
|
+
75
|
128
|
+
end
|
129
|
+
promise.should be_ready
|
130
|
+
promise.value.should be_nil
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'inline event loop' do
|
135
|
+
let :loop do
|
136
|
+
ZeevexConcurrency::EventLoop::Inline.new
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should run the callable provided' do
|
140
|
+
Thread.exclusive do
|
141
|
+
foo = 100
|
142
|
+
promise = loop.enqueue do
|
143
|
+
foo += 1
|
144
|
+
end
|
145
|
+
promise.should be_ready
|
146
|
+
foo.should == 101
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
it 'should return the value in the promise' do
|
151
|
+
Thread.exclusive do
|
152
|
+
promise = loop.enqueue do
|
153
|
+
75
|
154
|
+
end
|
155
|
+
promise.should be_ready
|
156
|
+
promise.value.should == 75
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
data/spec/future_spec.rb
ADDED
@@ -0,0 +1,316 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
require 'zeevex_concurrency/future.rb'
|
3
|
+
require 'zeevex_concurrency/event_loop.rb'
|
4
|
+
require 'zeevex_concurrency/thread_pool.rb'
|
5
|
+
|
6
|
+
#
|
7
|
+
# this test counts on a single-threaded worker pool
|
8
|
+
#
|
9
|
+
describe ZeevexConcurrency::Future do
|
10
|
+
clazz = ZeevexConcurrency::Future
|
11
|
+
|
12
|
+
let :empty_proc do
|
13
|
+
Proc.new { 8800 }
|
14
|
+
end
|
15
|
+
|
16
|
+
let :sleep_proc do
|
17
|
+
Proc.new { sleep 60 }
|
18
|
+
end
|
19
|
+
|
20
|
+
let :queue do
|
21
|
+
Queue.new
|
22
|
+
end
|
23
|
+
|
24
|
+
let :loop do
|
25
|
+
loop = ZeevexConcurrency::ThreadPool::FixedPool.new(1)
|
26
|
+
end
|
27
|
+
|
28
|
+
before :each do
|
29
|
+
queue
|
30
|
+
pause_queue
|
31
|
+
loop.start
|
32
|
+
ZeevexConcurrency::Future.worker_pool = loop
|
33
|
+
end
|
34
|
+
|
35
|
+
around :each do |ex|
|
36
|
+
Timeout::timeout(10) do
|
37
|
+
ex.run
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
let :pause_queue do
|
42
|
+
Queue.new
|
43
|
+
end
|
44
|
+
|
45
|
+
def pause_futures
|
46
|
+
pause_queue
|
47
|
+
loop.enqueue do
|
48
|
+
pause_queue.pop
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def resume_futures
|
53
|
+
pause_queue << "continue"
|
54
|
+
end
|
55
|
+
|
56
|
+
# ensure previous futures have completed - serial single threaded worker pool necessary
|
57
|
+
def wait_for_queue_to_empty
|
58
|
+
ZeevexConcurrency::Future.create(Proc.new {}).wait
|
59
|
+
end
|
60
|
+
|
61
|
+
context 'argument checking' do
|
62
|
+
it 'should require a callable or a block' do
|
63
|
+
expect { clazz.create }.
|
64
|
+
to raise_error(ArgumentError)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'should not allow both a callable AND a block' do
|
68
|
+
expect {
|
69
|
+
clazz.create(empty_proc) do
|
70
|
+
1
|
71
|
+
end
|
72
|
+
}.to raise_error(ArgumentError)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'should accept a proc' do
|
76
|
+
expect { clazz.create(empty_proc) }.
|
77
|
+
not_to raise_error(ArgumentError)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'should accept a block' do
|
81
|
+
expect {
|
82
|
+
clazz.create do
|
83
|
+
1
|
84
|
+
end
|
85
|
+
}.not_to raise_error(ArgumentError)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'before receiving value' do
|
90
|
+
subject { clazz.create(sleep_proc) }
|
91
|
+
it { should_not be_ready }
|
92
|
+
end
|
93
|
+
|
94
|
+
context 'after executing' do
|
95
|
+
subject {
|
96
|
+
clazz.create do
|
97
|
+
@counter += 1
|
98
|
+
end
|
99
|
+
}
|
100
|
+
|
101
|
+
before do
|
102
|
+
@counter = 55
|
103
|
+
subject.wait
|
104
|
+
end
|
105
|
+
|
106
|
+
it { should be_ready }
|
107
|
+
its(:value) { should == 56 }
|
108
|
+
it 'should return same value for repeated calls' do
|
109
|
+
subject.value
|
110
|
+
subject.value.should == 56
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
context 'with exception' do
|
115
|
+
class FooBar < StandardError; end
|
116
|
+
subject do
|
117
|
+
clazz.create do
|
118
|
+
# binding.pry
|
119
|
+
raise FooBar, "test"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
before do
|
124
|
+
subject.wait
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'should be ready' do
|
128
|
+
subject.should be_ready
|
129
|
+
end
|
130
|
+
|
131
|
+
it 'should reraise exception' do
|
132
|
+
expect { subject.value }.
|
133
|
+
to raise_error(FooBar)
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'should optionally not reraise' do
|
137
|
+
expect { subject.value(false) }.
|
138
|
+
not_to raise_error(FooBar)
|
139
|
+
subject.value(false).should be_a(FooBar)
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
context '#wait' do
|
144
|
+
subject { clazz.create(Proc.new { queue.pop }) }
|
145
|
+
it 'should wait for 2 seconds' do
|
146
|
+
t_start = Time.now
|
147
|
+
res = subject.wait 2
|
148
|
+
t_end = Time.now
|
149
|
+
(t_end-t_start).round.should == 2
|
150
|
+
res.should be_false
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should return immediately if ready' do
|
154
|
+
t_start = Time.now
|
155
|
+
queue << 99
|
156
|
+
res = subject.wait 2
|
157
|
+
t_end = Time.now
|
158
|
+
(t_end-t_start).round.should == 0
|
159
|
+
res.should be_true
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
context 'observing' do
|
164
|
+
subject { clazz.create(Proc.new { @callable.call }, :observer => observer) }
|
165
|
+
let :observer do
|
166
|
+
mock()
|
167
|
+
end
|
168
|
+
|
169
|
+
#
|
170
|
+
#
|
171
|
+
#
|
172
|
+
it 'should notify observer after set_result' do
|
173
|
+
pause_futures
|
174
|
+
@callable = Proc.new { 10 }
|
175
|
+
observer.should_receive(:update).with(subject, 10, true)
|
176
|
+
resume_futures
|
177
|
+
wait_for_queue_to_empty
|
178
|
+
end
|
179
|
+
|
180
|
+
it 'should notify observer after set_result raises exception' do
|
181
|
+
pause_futures
|
182
|
+
@callable = Proc.new { raise "foo" }
|
183
|
+
observer.should_receive(:update).with(subject, kind_of(Exception), false)
|
184
|
+
resume_futures
|
185
|
+
wait_for_queue_to_empty
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
context 'cancelling' do
|
190
|
+
subject do
|
191
|
+
clazz.create(Proc.new { @value += 1})
|
192
|
+
end
|
193
|
+
|
194
|
+
before do
|
195
|
+
@value = 100
|
196
|
+
pause_futures
|
197
|
+
subject
|
198
|
+
end
|
199
|
+
|
200
|
+
it 'should not be cancelled at creation time' do
|
201
|
+
subject.should_not be_cancelled
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'should not be cancelled after execution' do
|
205
|
+
resume_futures
|
206
|
+
subject.wait
|
207
|
+
subject.should_not be_cancelled
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'should not be cancelled after raising an exception' do
|
211
|
+
future = clazz.create(Proc.new { raise "bar" })
|
212
|
+
resume_futures
|
213
|
+
future.wait
|
214
|
+
future.should_not be_cancelled
|
215
|
+
end
|
216
|
+
|
217
|
+
it 'should be cancelled after cancellation but before execution' do
|
218
|
+
subject.cancel
|
219
|
+
subject.should be_cancelled
|
220
|
+
end
|
221
|
+
|
222
|
+
it 'should not be marked as executed after cancellation but before execution' do
|
223
|
+
subject.cancel
|
224
|
+
subject.should_not be_executed
|
225
|
+
end
|
226
|
+
|
227
|
+
it 'should be marked as ready after cancellation but before execution' do
|
228
|
+
subject.cancel
|
229
|
+
subject.should be_ready
|
230
|
+
end
|
231
|
+
|
232
|
+
it 'should be cancelled after cancellation and attempted execution' do
|
233
|
+
subject.cancel
|
234
|
+
resume_futures
|
235
|
+
wait_for_queue_to_empty
|
236
|
+
subject.should be_cancelled
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'should skip execution after cancellation' do
|
240
|
+
subject.cancel
|
241
|
+
resume_futures
|
242
|
+
wait_for_queue_to_empty
|
243
|
+
subject.should be_cancelled
|
244
|
+
subject.should_not be_executed
|
245
|
+
@value.should == 100
|
246
|
+
end
|
247
|
+
|
248
|
+
it 'should not allow cancellation after execution' do
|
249
|
+
resume_futures
|
250
|
+
wait_for_queue_to_empty
|
251
|
+
subject.cancel.should be_false
|
252
|
+
end
|
253
|
+
|
254
|
+
it 'should raise exception if #value is called on cancelled future' do
|
255
|
+
subject.cancel
|
256
|
+
resume_futures
|
257
|
+
wait_for_queue_to_empty
|
258
|
+
expect { subject.value }.
|
259
|
+
to raise_error(ZeevexConcurrency::Delayed::CancelledException)
|
260
|
+
end
|
261
|
+
|
262
|
+
it 'should return from wait after processing when cancelled' do
|
263
|
+
subject.cancel
|
264
|
+
resume_futures
|
265
|
+
wait_for_queue_to_empty
|
266
|
+
Timeout::timeout(1) { subject.wait }
|
267
|
+
end
|
268
|
+
|
269
|
+
it 'should return from wait before processing when cancelled' do
|
270
|
+
subject.cancel
|
271
|
+
Timeout::timeout(1) { subject.wait }
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
context 'access from multiple threads' do
|
276
|
+
let :future do
|
277
|
+
clazz.create(Proc.new { @value += 1})
|
278
|
+
end
|
279
|
+
|
280
|
+
before do
|
281
|
+
@value = 20
|
282
|
+
pause_futures
|
283
|
+
future
|
284
|
+
queue
|
285
|
+
threads = []
|
286
|
+
5.times do
|
287
|
+
threads << Thread.new do
|
288
|
+
queue << future.value
|
289
|
+
end
|
290
|
+
end
|
291
|
+
Thread.pass
|
292
|
+
@queue_size_before_set = queue.size
|
293
|
+
resume_futures
|
294
|
+
threads.map &:join
|
295
|
+
end
|
296
|
+
|
297
|
+
it 'should block all threads before set_result' do
|
298
|
+
@queue_size_before_set.should == 0
|
299
|
+
end
|
300
|
+
|
301
|
+
it 'should allow all threads to receive a value' do
|
302
|
+
queue.size.should == 5
|
303
|
+
end
|
304
|
+
|
305
|
+
it 'should only evaluate the computation once' do
|
306
|
+
@value.should == 21
|
307
|
+
end
|
308
|
+
|
309
|
+
it 'should send the same value to all threads' do
|
310
|
+
list = []
|
311
|
+
5.times { list << queue.pop }
|
312
|
+
list.should == [21,21,21,21,21]
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
@@ -0,0 +1,172 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'spec_helper')
|
2
|
+
require 'zeevex_concurrency/future.rb'
|
3
|
+
require 'zeevex_concurrency/event_loop.rb'
|
4
|
+
|
5
|
+
describe ZeevexConcurrency::Promise do
|
6
|
+
clazz = ZeevexConcurrency::Promise
|
7
|
+
|
8
|
+
around :each do |ex|
|
9
|
+
Timeout::timeout(10) do
|
10
|
+
ex.run
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
context 'argument checking' do
|
15
|
+
|
16
|
+
it 'should allow neither a callable nor a block' do
|
17
|
+
expect { clazz.new }.
|
18
|
+
not_to raise_error(ArgumentError)
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should not allow both a callable AND a block' do
|
22
|
+
expect {
|
23
|
+
clazz.new(Proc.new { 2 }) do
|
24
|
+
1
|
25
|
+
end
|
26
|
+
}.to raise_error(ArgumentError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'should accept a proc' do
|
30
|
+
expect { clazz.new(Proc.new {}) }.
|
31
|
+
not_to raise_error(ArgumentError)
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'should accept a block' do
|
35
|
+
expect {
|
36
|
+
clazz.new do
|
37
|
+
1
|
38
|
+
end
|
39
|
+
}.not_to raise_error(ArgumentError)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
context 'before receiving value' do
|
44
|
+
subject { clazz.new() }
|
45
|
+
it { should_not be_ready }
|
46
|
+
end
|
47
|
+
|
48
|
+
context 'after using set_result' do
|
49
|
+
subject { clazz.new(nil) }
|
50
|
+
before do
|
51
|
+
@counter = 55
|
52
|
+
subject.set_result { @counter += 1 }
|
53
|
+
end
|
54
|
+
|
55
|
+
it { should be_ready }
|
56
|
+
its(:value) { should == 56 }
|
57
|
+
it 'should return same value for repeated calls' do
|
58
|
+
subject.value
|
59
|
+
subject.value.should == 56
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
context 'with exception' do
|
64
|
+
class FooBar < StandardError; end
|
65
|
+
subject do
|
66
|
+
clazz.new lambda {
|
67
|
+
raise FooBar, "test"
|
68
|
+
}
|
69
|
+
end
|
70
|
+
|
71
|
+
before do
|
72
|
+
subject.execute
|
73
|
+
end
|
74
|
+
|
75
|
+
it { should be_ready }
|
76
|
+
it 'should reraise exception' do
|
77
|
+
expect { subject.value }.
|
78
|
+
to raise_error(FooBar)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should optionally not reraise' do
|
82
|
+
expect { subject.value(false) }.
|
83
|
+
not_to raise_error(FooBar)
|
84
|
+
subject.value(false).should be_a(FooBar)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
context '#wait' do
|
89
|
+
subject { clazz.new }
|
90
|
+
it 'should wait for 2 seconds' do
|
91
|
+
t_start = Time.now
|
92
|
+
res = subject.wait 2
|
93
|
+
t_end = Time.now
|
94
|
+
(t_end-t_start).round.should == 2
|
95
|
+
res.should be_false
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should return immediately if ready' do
|
99
|
+
t_start = Time.now
|
100
|
+
subject.set_result { 99 }
|
101
|
+
res = subject.wait 2
|
102
|
+
t_end = Time.now
|
103
|
+
(t_end-t_start).round.should == 0
|
104
|
+
res.should be_true
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'observing' do
|
109
|
+
subject { clazz.new(nil) }
|
110
|
+
let :observer do
|
111
|
+
mock()
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should notify observer after set_result' do
|
115
|
+
observer.should_receive(:update).with(subject, 10, true)
|
116
|
+
subject.add_observer observer
|
117
|
+
subject.set_result { 10 }
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should notify observer after set_result raises exception' do
|
121
|
+
observer.should_receive(:update).with(subject, kind_of(Exception), false)
|
122
|
+
subject.add_observer observer
|
123
|
+
subject.set_result { raise "foo" }
|
124
|
+
end
|
125
|
+
|
126
|
+
it 'should notify observer after #execute' do
|
127
|
+
future = clazz.new(Proc.new { 4 + 20 })
|
128
|
+
observer.should_receive(:update)
|
129
|
+
future.add_observer observer
|
130
|
+
future.execute
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
context 'access from multiple threads' do
|
135
|
+
subject { clazz.new(nil) }
|
136
|
+
|
137
|
+
before do
|
138
|
+
@value = 20
|
139
|
+
subject
|
140
|
+
@queue = Queue.new
|
141
|
+
threads = []
|
142
|
+
5.times do
|
143
|
+
threads << Thread.new do
|
144
|
+
@queue << subject.value
|
145
|
+
end
|
146
|
+
end
|
147
|
+
Thread.pass
|
148
|
+
@queue_size_before_set = @queue.size
|
149
|
+
subject.set_result { @value += 1 }
|
150
|
+
threads.map &:join
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'should block all threads before set_result' do
|
154
|
+
@queue_size_before_set.should == 0
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should allow all threads to receive a value' do
|
158
|
+
@queue.size.should == 5
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'should only evaluate the computation once' do
|
162
|
+
@value.should == 21
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'should send the same value to all threads' do
|
166
|
+
list = []
|
167
|
+
5.times { list << @queue.pop }
|
168
|
+
list.should == [21,21,21,21,21]
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|