zeevex_concurrency 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+
@@ -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
+
@@ -0,0 +1,8 @@
1
+ require 'rspec'
2
+
3
+ $: << File.expand_path(File.dirname(__FILE__) + '../lib')
4
+ require 'zeevex_concurrency'
5
+
6
+ require 'pry'
7
+ require 'timeout'
8
+