concurrent-ruby 0.1.1 → 0.2.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.
- checksums.yaml +4 -4
- data/README.md +48 -1
- data/lib/concurrent.rb +8 -1
- data/lib/concurrent/agent.rb +19 -40
- data/lib/concurrent/cached_thread_pool.rb +10 -11
- data/lib/concurrent/defer.rb +8 -12
- data/lib/concurrent/executor.rb +95 -0
- data/lib/concurrent/fixed_thread_pool.rb +12 -6
- data/lib/concurrent/functions.rb +120 -0
- data/lib/concurrent/future.rb +8 -20
- data/lib/concurrent/global_thread_pool.rb +13 -0
- data/lib/concurrent/goroutine.rb +5 -1
- data/lib/concurrent/null_thread_pool.rb +22 -0
- data/lib/concurrent/obligation.rb +10 -64
- data/lib/concurrent/promise.rb +38 -60
- data/lib/concurrent/reactor.rb +166 -0
- data/lib/concurrent/reactor/drb_async_demux.rb +83 -0
- data/lib/concurrent/reactor/tcp_sync_demux.rb +131 -0
- data/lib/concurrent/supervisor.rb +100 -0
- data/lib/concurrent/thread_pool.rb +16 -5
- data/lib/concurrent/utilities.rb +8 -0
- data/lib/concurrent/version.rb +1 -1
- data/md/defer.md +4 -4
- data/md/executor.md +187 -0
- data/md/promise.md +2 -0
- data/md/thread_pool.md +27 -0
- data/spec/concurrent/agent_spec.rb +8 -27
- data/spec/concurrent/cached_thread_pool_spec.rb +14 -1
- data/spec/concurrent/defer_spec.rb +17 -21
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +159 -149
- data/spec/concurrent/executor_spec.rb +200 -0
- data/spec/concurrent/fixed_thread_pool_spec.rb +2 -3
- data/spec/concurrent/functions_spec.rb +217 -0
- data/spec/concurrent/future_spec.rb +4 -11
- data/spec/concurrent/global_thread_pool_spec.rb +38 -0
- data/spec/concurrent/goroutine_spec.rb +15 -0
- data/spec/concurrent/null_thread_pool_spec.rb +54 -0
- data/spec/concurrent/obligation_shared.rb +127 -116
- data/spec/concurrent/promise_spec.rb +16 -14
- data/spec/concurrent/reactor/drb_async_demux_spec.rb +196 -0
- data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +410 -0
- data/spec/concurrent/reactor_spec.rb +364 -0
- data/spec/concurrent/supervisor_spec.rb +258 -0
- data/spec/concurrent/thread_pool_shared.rb +156 -161
- data/spec/concurrent/utilities_spec.rb +30 -1
- data/spec/spec_helper.rb +13 -0
- metadata +38 -9
@@ -0,0 +1,364 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
describe Reactor do
|
6
|
+
|
7
|
+
let(:sync_demux) do
|
8
|
+
Class.new {
|
9
|
+
def initialize
|
10
|
+
@running = false
|
11
|
+
@queue = Queue.new
|
12
|
+
end
|
13
|
+
def run() @running = true; end
|
14
|
+
def stop
|
15
|
+
@queue.push(:stop)
|
16
|
+
@running = false
|
17
|
+
end
|
18
|
+
def running?() return @running == true; end
|
19
|
+
def accept()
|
20
|
+
event = @queue.pop
|
21
|
+
if event == :stop
|
22
|
+
return nil
|
23
|
+
else
|
24
|
+
return Reactor::EventContext.new(event)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
def respond(result, message) return [result, message]; end
|
28
|
+
def send(event) @queue.push(event) end
|
29
|
+
}.new
|
30
|
+
end
|
31
|
+
|
32
|
+
let(:async_demux) do
|
33
|
+
Class.new {
|
34
|
+
def initialize() @running = false; end
|
35
|
+
def run() @running = true; end
|
36
|
+
def stop() @running = false; end
|
37
|
+
def running?() return @running == true; end
|
38
|
+
def set_reactor(reactor) @reactor = reactor; end
|
39
|
+
def send(event) @reactor.handle(event); end
|
40
|
+
}.new
|
41
|
+
end
|
42
|
+
|
43
|
+
after(:each) do
|
44
|
+
Thread.kill(@thread) unless @thread.nil?
|
45
|
+
end
|
46
|
+
|
47
|
+
context '#initialize' do
|
48
|
+
|
49
|
+
it 'raises an exception when the demux is not valid' do
|
50
|
+
lambda {
|
51
|
+
Reactor.new('bogus demux')
|
52
|
+
}.should raise_error(ArgumentError)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'sets the initial state to not running' do
|
56
|
+
Reactor.new.should_not be_running
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
context '#running?' do
|
61
|
+
|
62
|
+
it 'returns true when the reactor is running' do
|
63
|
+
reactor = Reactor.new
|
64
|
+
@thread = Thread.new{ reactor.run }
|
65
|
+
sleep(0.1)
|
66
|
+
reactor.should be_running
|
67
|
+
reactor.stop
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'returns false when the reactor is stopped' do
|
71
|
+
reactor = Reactor.new
|
72
|
+
@thread = Thread.new{ reactor.run }
|
73
|
+
sleep(0.1)
|
74
|
+
reactor.stop
|
75
|
+
sleep(0.1)
|
76
|
+
reactor.should_not be_running
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context '#add_handler' do
|
81
|
+
|
82
|
+
it 'raises an exception is the event name is reserved' do
|
83
|
+
reactor = Reactor.new
|
84
|
+
lambda {
|
85
|
+
reactor.add_handler(Reactor::RESERVED_EVENTS.first){ nil }
|
86
|
+
}.should raise_error(ArgumentError)
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'raises an exception if no block is given' do
|
90
|
+
reactor = Reactor.new
|
91
|
+
lambda {
|
92
|
+
reactor.add_handler('no block given')
|
93
|
+
}.should raise_error(ArgumentError)
|
94
|
+
end
|
95
|
+
|
96
|
+
it 'returns true if the handler is added' do
|
97
|
+
reactor = Reactor.new
|
98
|
+
reactor.add_handler('good'){ nil }.should be_true
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
context '#remove_handler' do
|
103
|
+
|
104
|
+
it 'returns true if the handler is found and removed' do
|
105
|
+
reactor = Reactor.new
|
106
|
+
reactor.add_handler('good'){ nil }
|
107
|
+
reactor.remove_handler('good').should be_true
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'returns false if the handler is not found' do
|
111
|
+
reactor = Reactor.new
|
112
|
+
reactor.remove_handler('not found').should be_false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
context '#stop_on_signal' do
|
117
|
+
|
118
|
+
if Functional::PLATFORM.mri? && ! Functional::PLATFORM.windows?
|
119
|
+
|
120
|
+
it 'traps each valid signal' do
|
121
|
+
Signal.should_receive(:trap).with('USR1')
|
122
|
+
Signal.should_receive(:trap).with('USR2')
|
123
|
+
reactor = Reactor.new
|
124
|
+
reactor.stop_on_signal('USR1', 'USR2')
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'raises an exception if given an invalid signal' do
|
128
|
+
if Functional::PLATFORM.mri?
|
129
|
+
reactor = Reactor.new
|
130
|
+
lambda {
|
131
|
+
reactor.stop_on_signal('BOGUS')
|
132
|
+
}.should raise_error(ArgumentError)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
it 'stops the reactor when it receives a trapped signal' do
|
137
|
+
reactor = Reactor.new
|
138
|
+
reactor.stop_on_signal('USR1')
|
139
|
+
reactor.should_receive(:stop).with(no_args())
|
140
|
+
Process.kill('USR1', Process.pid)
|
141
|
+
sleep(0.1)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
context '#handle' do
|
147
|
+
|
148
|
+
it 'raises an exception if the demux is synchronous' do
|
149
|
+
reactor = Reactor.new(sync_demux)
|
150
|
+
lambda {
|
151
|
+
reactor.handle('event')
|
152
|
+
}.should raise_error(NotImplementedError)
|
153
|
+
end
|
154
|
+
|
155
|
+
it 'returns :stopped if the reactor is not running' do
|
156
|
+
reactor = Reactor.new
|
157
|
+
reactor.handle('event').first.should eq :stopped
|
158
|
+
end
|
159
|
+
|
160
|
+
it 'returns :ok and the block result on success' do
|
161
|
+
reactor = Reactor.new
|
162
|
+
reactor.add_handler(:event){ 10 }
|
163
|
+
@thread = Thread.new{ reactor.run }
|
164
|
+
sleep(0.1)
|
165
|
+
result = reactor.handle(:event)
|
166
|
+
result.first.should eq :ok
|
167
|
+
result.last.should eq 10
|
168
|
+
reactor.stop
|
169
|
+
end
|
170
|
+
|
171
|
+
it 'returns :ex and the exception on failure' do
|
172
|
+
reactor = Reactor.new
|
173
|
+
reactor.add_handler(:event){ raise StandardError }
|
174
|
+
@thread = Thread.new{ reactor.run }
|
175
|
+
sleep(0.1)
|
176
|
+
result = reactor.handle(:event)
|
177
|
+
result.first.should eq :ex
|
178
|
+
result.last.should be_a(StandardError)
|
179
|
+
reactor.stop
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'returns :noop when there is no handler' do
|
183
|
+
reactor = Reactor.new
|
184
|
+
@thread = Thread.new{ reactor.run }
|
185
|
+
sleep(0.1)
|
186
|
+
result = reactor.handle(:event)
|
187
|
+
sleep(0.1)
|
188
|
+
result.first.should eq :noop
|
189
|
+
reactor.stop
|
190
|
+
end
|
191
|
+
|
192
|
+
it 'triggers handlers added after the reactor is runed' do
|
193
|
+
@expected = false
|
194
|
+
reactor = Reactor.new
|
195
|
+
@thread = Thread.new{ reactor.run }
|
196
|
+
sleep(0.1)
|
197
|
+
reactor.add_handler(:event){ @expected = true }
|
198
|
+
reactor.handle(:event)
|
199
|
+
@expected.should be_true
|
200
|
+
reactor.stop
|
201
|
+
end
|
202
|
+
|
203
|
+
it 'does not trigger an event that was removed' do
|
204
|
+
@expected = false
|
205
|
+
reactor = Reactor.new
|
206
|
+
reactor.add_handler(:event){ @expected = true }
|
207
|
+
reactor.remove_handler(:event)
|
208
|
+
@thread = Thread.new{ reactor.run }
|
209
|
+
sleep(0.1)
|
210
|
+
reactor.handle(:event)
|
211
|
+
@expected.should be_false
|
212
|
+
reactor.stop
|
213
|
+
end
|
214
|
+
end
|
215
|
+
|
216
|
+
context '#run' do
|
217
|
+
|
218
|
+
it 'raises an exception if the reactor is already running' do
|
219
|
+
reactor = Reactor.new
|
220
|
+
@thread = Thread.new{ reactor.run }
|
221
|
+
sleep(0.1)
|
222
|
+
lambda {
|
223
|
+
reactor.run
|
224
|
+
}.should raise_error(StandardError)
|
225
|
+
reactor.stop
|
226
|
+
end
|
227
|
+
|
228
|
+
it 'runs the reactor if it is not running' do
|
229
|
+
reactor = Reactor.new(async_demux)
|
230
|
+
reactor.should_receive(:run_async).with(no_args())
|
231
|
+
@thread = Thread.new{ reactor.run }
|
232
|
+
sleep(0.1)
|
233
|
+
reactor.should be_running
|
234
|
+
reactor.stop
|
235
|
+
|
236
|
+
reactor = Reactor.new(sync_demux)
|
237
|
+
reactor.should_receive(:run_sync).with(no_args())
|
238
|
+
@thread = Thread.new{ reactor.run }
|
239
|
+
sleep(0.1)
|
240
|
+
reactor.should be_running
|
241
|
+
reactor.stop
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
context '#stop' do
|
246
|
+
|
247
|
+
it 'returns if the reactor is not running' do
|
248
|
+
reactor = Reactor.new
|
249
|
+
reactor.stop.should be_true
|
250
|
+
end
|
251
|
+
|
252
|
+
it 'stops the reactor when running and synchronous' do
|
253
|
+
reactor = Reactor.new(sync_demux)
|
254
|
+
@thread = Thread.new{ sleep(0.1); reactor.stop }
|
255
|
+
Thread.pass
|
256
|
+
reactor.run
|
257
|
+
end
|
258
|
+
|
259
|
+
it 'stops the reactor when running and asynchronous' do
|
260
|
+
reactor = Reactor.new(async_demux)
|
261
|
+
@thread = Thread.new{ sleep(0.1); reactor.stop }
|
262
|
+
Thread.pass
|
263
|
+
reactor.run
|
264
|
+
end
|
265
|
+
|
266
|
+
it 'stops the reactor when running without a demux' do
|
267
|
+
reactor = Reactor.new
|
268
|
+
@thread = Thread.new{ sleep(0.1); reactor.stop }
|
269
|
+
Thread.pass
|
270
|
+
reactor.run
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
specify 'synchronous demultiplexing' do
|
275
|
+
|
276
|
+
if Functional::PLATFORM.mri? && ! Functional::PLATFORM.windows?
|
277
|
+
|
278
|
+
demux = sync_demux
|
279
|
+
reactor = Concurrent::Reactor.new(demux)
|
280
|
+
|
281
|
+
reactor.should_not be_running
|
282
|
+
|
283
|
+
reactor.add_handler(:foo){ 'Foo' }
|
284
|
+
reactor.add_handler(:bar){ 'Bar' }
|
285
|
+
reactor.add_handler(:baz){ 'Baz' }
|
286
|
+
reactor.add_handler(:fubar){ raise StandardError.new('Boom!') }
|
287
|
+
|
288
|
+
reactor.stop_on_signal('USR1')
|
289
|
+
|
290
|
+
demux.should_receive(:respond).with(:ok, 'Foo')
|
291
|
+
demux.send(:foo)
|
292
|
+
|
293
|
+
@thread = Thread.new do
|
294
|
+
reactor.run
|
295
|
+
end
|
296
|
+
@thread.abort_on_exception = true
|
297
|
+
sleep(0.1)
|
298
|
+
|
299
|
+
reactor.should be_running
|
300
|
+
|
301
|
+
demux.should_receive(:respond).with(:ok, 'Bar')
|
302
|
+
demux.should_receive(:respond).with(:ok, 'Baz')
|
303
|
+
demux.should_receive(:respond).with(:noop, anything())
|
304
|
+
demux.should_receive(:respond).with(:ex, anything())
|
305
|
+
|
306
|
+
demux.send(:bar)
|
307
|
+
demux.send(:baz)
|
308
|
+
demux.send(:bogus)
|
309
|
+
demux.send(:fubar)
|
310
|
+
|
311
|
+
reactor.should be_running
|
312
|
+
|
313
|
+
Process.kill('USR1', Process.pid)
|
314
|
+
sleep(0.1)
|
315
|
+
|
316
|
+
demux.should_not_receive(:respond).with(:foo, anything())
|
317
|
+
demux.send(:foo)
|
318
|
+
reactor.should_not be_running
|
319
|
+
end
|
320
|
+
end
|
321
|
+
|
322
|
+
specify 'asynchronous demultiplexing' do
|
323
|
+
|
324
|
+
if Functional::PLATFORM.mri? && ! Functional::PLATFORM.windows?
|
325
|
+
|
326
|
+
demux = async_demux
|
327
|
+
reactor = Concurrent::Reactor.new(demux)
|
328
|
+
|
329
|
+
reactor.should_not be_running
|
330
|
+
|
331
|
+
reactor.add_handler(:foo){ 'Foo' }
|
332
|
+
reactor.add_handler(:bar){ 'Bar' }
|
333
|
+
reactor.add_handler(:baz){ 'Baz' }
|
334
|
+
reactor.add_handler(:fubar){ raise StandardError.new('Boom!') }
|
335
|
+
|
336
|
+
reactor.stop_on_signal('USR2')
|
337
|
+
|
338
|
+
demux.send(:foo).first.should eq :stopped
|
339
|
+
|
340
|
+
@thread = Thread.new do
|
341
|
+
reactor.run
|
342
|
+
end
|
343
|
+
@thread.abort_on_exception = true
|
344
|
+
sleep(0.1)
|
345
|
+
|
346
|
+
reactor.should be_running
|
347
|
+
|
348
|
+
demux.send(:foo).should eq [:ok, 'Foo']
|
349
|
+
demux.send(:bar).should eq [:ok, 'Bar']
|
350
|
+
demux.send(:baz).should eq [:ok, 'Baz']
|
351
|
+
demux.send(:bogus).first.should eq :noop
|
352
|
+
demux.send(:fubar).first.should eq :ex
|
353
|
+
|
354
|
+
reactor.should be_running
|
355
|
+
|
356
|
+
Process.kill('USR2', Process.pid)
|
357
|
+
sleep(0.1)
|
358
|
+
|
359
|
+
demux.send(:foo).first.should eq :stopped
|
360
|
+
reactor.should_not be_running
|
361
|
+
end
|
362
|
+
end
|
363
|
+
end
|
364
|
+
end
|
@@ -0,0 +1,258 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
describe Supervisor do
|
6
|
+
|
7
|
+
let(:worker_class) do
|
8
|
+
Class.new {
|
9
|
+
behavior(:runnable)
|
10
|
+
def run() return true; end
|
11
|
+
def stop() return true; end
|
12
|
+
def running?() return true; end
|
13
|
+
}
|
14
|
+
end
|
15
|
+
|
16
|
+
let(:worker){ worker_class.new }
|
17
|
+
|
18
|
+
subject{ Supervisor.new }
|
19
|
+
|
20
|
+
after(:each) do
|
21
|
+
subject.stop
|
22
|
+
end
|
23
|
+
|
24
|
+
context '#initialize' do
|
25
|
+
|
26
|
+
it 'sets the initial length to zero' do
|
27
|
+
supervisor = Supervisor.new
|
28
|
+
supervisor.length.should == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'sets the initial length to one when a worker is provided' do
|
32
|
+
supervisor = Supervisor.new(worker: worker)
|
33
|
+
supervisor.length.should == 1
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'sets the initial state to stopped' do
|
37
|
+
supervisor = Supervisor.new
|
38
|
+
supervisor.should_not be_running
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'sets the monitor interval when given' do
|
42
|
+
supervisor = Supervisor.new
|
43
|
+
supervisor.monitor_interval.should == Supervisor::DEFAULT_MONITOR_INTERVAL
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'sets the monitor interval to the default when not given' do
|
47
|
+
supervisor = Supervisor.new(monitor_interval: 5)
|
48
|
+
supervisor.monitor_interval.should == 5
|
49
|
+
|
50
|
+
supervisor = Supervisor.new(monitor: 10)
|
51
|
+
supervisor.monitor_interval.should == 10
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'run' do
|
56
|
+
|
57
|
+
it 'runs the monitor' do
|
58
|
+
subject.should_receive(:monitor).with(no_args()).at_least(1).times
|
59
|
+
t = Thread.new{ subject.run }
|
60
|
+
sleep(0.1)
|
61
|
+
subject.stop
|
62
|
+
Thread.kill(t) unless t.nil?
|
63
|
+
end
|
64
|
+
|
65
|
+
it 'calls #run on all workers' do
|
66
|
+
supervisor = Supervisor.new(worker: worker)
|
67
|
+
# must stub AFTER adding or else #add_worker will reject
|
68
|
+
worker.should_receive(:run).with(no_args())
|
69
|
+
t = Thread.new{ supervisor.run }
|
70
|
+
sleep(0.1)
|
71
|
+
supervisor.stop
|
72
|
+
Thread.kill(t)
|
73
|
+
end
|
74
|
+
|
75
|
+
it 'sets the state to running' do
|
76
|
+
t = Thread.new{ subject.run }
|
77
|
+
sleep(0.1)
|
78
|
+
subject.should be_running
|
79
|
+
subject.stop
|
80
|
+
Thread.kill(t)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'raises an exception when already running' do
|
84
|
+
@thread = nil
|
85
|
+
subject.run!
|
86
|
+
lambda {
|
87
|
+
@thread = Thread.new{ subject.run }
|
88
|
+
@thread.abort_on_exception = true
|
89
|
+
sleep(0.1)
|
90
|
+
}.should raise_error(StandardError)
|
91
|
+
subject.stop
|
92
|
+
Thread.kill(@thread) unless @thread.nil?
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context '#run!' do
|
97
|
+
|
98
|
+
it 'runs the monitor thread' do
|
99
|
+
Thread.should_receive(:new).with(no_args())
|
100
|
+
subject.run!
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'calls #run on all workers' do
|
104
|
+
supervisor = Supervisor.new(worker: worker)
|
105
|
+
# must stub AFTER adding or else #add_worker will reject
|
106
|
+
worker.should_receive(:run).with(no_args())
|
107
|
+
supervisor.run!
|
108
|
+
sleep(0.1)
|
109
|
+
end
|
110
|
+
|
111
|
+
it 'sets the state to running' do
|
112
|
+
subject.run!
|
113
|
+
subject.should be_running
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'raises an exception when already running' do
|
117
|
+
subject.run!
|
118
|
+
lambda {
|
119
|
+
subject.run!
|
120
|
+
}.should raise_error(StandardError)
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
context '#stop' do
|
125
|
+
|
126
|
+
it 'stops the monitor thread' do
|
127
|
+
Thread.should_receive(:kill).with(anything())
|
128
|
+
subject.run!
|
129
|
+
sleep(0.1)
|
130
|
+
subject.stop
|
131
|
+
end
|
132
|
+
|
133
|
+
it 'calls #stop on all workers' do
|
134
|
+
workers = (1..3).collect{ worker_class.new }
|
135
|
+
workers.each{|worker| subject.add_worker(worker)}
|
136
|
+
# must stub AFTER adding or else #add_worker will reject
|
137
|
+
workers.each{|worker| worker.should_receive(:stop).with(no_args())}
|
138
|
+
subject.run!
|
139
|
+
sleep(0.1)
|
140
|
+
subject.stop
|
141
|
+
end
|
142
|
+
|
143
|
+
it 'sets the state to stopped' do
|
144
|
+
subject.run!
|
145
|
+
subject.stop
|
146
|
+
subject.should_not be_running
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'returns true immediately when already stopped' do
|
150
|
+
subject.stop.should be_true
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
context '#running?' do
|
155
|
+
|
156
|
+
it 'returns true when running' do
|
157
|
+
subject.run!
|
158
|
+
subject.should be_running
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'returns false when stopped' do
|
162
|
+
subject.run!
|
163
|
+
subject.stop
|
164
|
+
subject.should_not be_running
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
context '#length' do
|
169
|
+
|
170
|
+
it 'returns a count of attached workers' do
|
171
|
+
workers = (1..3).collect{ worker.dup }
|
172
|
+
workers.each{|worker| subject.add_worker(worker)}
|
173
|
+
subject.length.should == 3
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
context '#add_worker' do
|
178
|
+
|
179
|
+
it 'adds the worker when stopped' do
|
180
|
+
subject.add_worker(worker)
|
181
|
+
subject.length.should == 1
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'rejects the worker when running' do
|
185
|
+
subject.run!
|
186
|
+
subject.add_worker(worker)
|
187
|
+
subject.length.should == 0
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'rejects a worker without the :runnable behavior' do
|
191
|
+
subject.add_worker('bogus worker')
|
192
|
+
subject.length.should == 0
|
193
|
+
end
|
194
|
+
|
195
|
+
it 'returns true when a worker is accepted' do
|
196
|
+
subject.add_worker(worker).should be_true
|
197
|
+
end
|
198
|
+
|
199
|
+
it 'returns false when a worker is not accepted' do
|
200
|
+
subject.add_worker('bogus worker').should be_false
|
201
|
+
end
|
202
|
+
end
|
203
|
+
|
204
|
+
context 'supervision' do
|
205
|
+
|
206
|
+
it 'reruns any worker that stops' do
|
207
|
+
worker = Class.new(worker_class){
|
208
|
+
def run() sleep(0.2); end
|
209
|
+
}.new
|
210
|
+
|
211
|
+
supervisor = Supervisor.new(worker: worker, monitor: 0.1)
|
212
|
+
supervisor.add_worker(worker)
|
213
|
+
# must stub AFTER adding or else #add_worker will reject
|
214
|
+
worker.should_receive(:run).with(no_args()).at_least(2).times
|
215
|
+
supervisor.run!
|
216
|
+
sleep(1)
|
217
|
+
supervisor.stop
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'reruns any dead threads' do
|
221
|
+
worker = Class.new(worker_class){
|
222
|
+
def run() raise StandardError; end
|
223
|
+
}.new
|
224
|
+
|
225
|
+
supervisor = Supervisor.new(worker: worker, monitor: 0.1)
|
226
|
+
supervisor.add_worker(worker)
|
227
|
+
# must stub AFTER adding or else #add_worker will reject
|
228
|
+
worker.should_receive(:run).with(no_args()).at_least(2).times
|
229
|
+
supervisor.run!
|
230
|
+
sleep(1)
|
231
|
+
supervisor.stop
|
232
|
+
end
|
233
|
+
end
|
234
|
+
|
235
|
+
context 'supervisor tree' do
|
236
|
+
|
237
|
+
specify do
|
238
|
+
s1 = Supervisor.new(monitor: 0.1)
|
239
|
+
s2 = Supervisor.new(monitor: 0.1)
|
240
|
+
s3 = Supervisor.new(monitor: 0.1)
|
241
|
+
|
242
|
+
workers = (1..3).collect{ worker_class.new }
|
243
|
+
workers.each{|worker| s3.add_worker(worker)}
|
244
|
+
# must stub AFTER adding or else #add_worker will reject
|
245
|
+
workers.each{|worker| worker.should_receive(:run).with(no_args())}
|
246
|
+
workers.each{|worker| worker.should_receive(:stop).with(no_args())}
|
247
|
+
|
248
|
+
s1.add_worker(s2)
|
249
|
+
s2.add_worker(s3)
|
250
|
+
|
251
|
+
s1.run!
|
252
|
+
sleep(0.1)
|
253
|
+
s1.stop
|
254
|
+
sleep(0.1)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
end
|