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.
@@ -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
+