concurrent-ruby 0.3.0 → 0.3.1.pre.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,34 @@
1
+ # I'm late! For a very important date!
2
+
3
+ TBD
4
+
5
+ [ScheduledExecutorService](http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/ScheduledExecutorService.html)
6
+
7
+ ## Copyright
8
+
9
+ *Concurrent Ruby* is Copyright © 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
10
+ It is free software and may be redistributed under the terms specified in the LICENSE file.
11
+
12
+ ## License
13
+
14
+ Released under the MIT license.
15
+
16
+ http://www.opensource.org/licenses/mit-license.php
17
+
18
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
19
+ > of this software and associated documentation files (the "Software"), to deal
20
+ > in the Software without restriction, including without limitation the rights
21
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
22
+ > copies of the Software, and to permit persons to whom the Software is
23
+ > furnished to do so, subject to the following conditions:
24
+ >
25
+ > The above copyright notice and this permission notice shall be included in
26
+ > all copies or substantial portions of the Software.
27
+ >
28
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
29
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
30
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
31
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
32
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
33
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
34
+ > THE SOFTWARE.
@@ -1,12 +1,215 @@
1
1
  # You don't need to get no supervisor! You the supervisor today!
2
2
 
3
- TBD...
3
+ One of Erlang's claim to fame is its fault tolerance. Erlang systems have been known
4
+ to exhibit near-mythical levels of uptime. One of the main reasons is the pervaisve
5
+ design philosophy of "let it fail." When errors occur most Erlang systems simply let
6
+ the failing component fail completely. The system then restarts the failed component.
7
+ This "let it fail" resilience isn't an intrinsic capability of either the language
8
+ or the virtual machine. It's a deliberate design philosophy. One of the key enablers
9
+ of this philosophy is the [Supervisor](http://www.erlang.org/doc/man/supervisor.html)
10
+ of the OTP (standard library).
4
11
 
5
- The `Sypervisor` is the yin to to the
6
- [Executor's](https://github.com/jdantonio/concurrent-ruby/blob/master/md/executor.md)
7
- yang. Where the `Supervisor` is intended to manage long-running threads that operate
8
- continuously, the `Executor` is intended to manage fairly short operations that
9
- occur repeatedly at regular intervals.
12
+ The Supervisor module answers the question "Who watches the watchmen?" A single
13
+ Supervisor can manage any number of workers (children). The Supervisor assumes
14
+ responsibility for starting the children, stopping them, and restarting them if
15
+ they fail. Several classes in this library, including `Actor` and `TimerTask` are
16
+ designed to work with `Supervisor`. Additionally, `Supervisor`s can supervise others
17
+ `Supervisor`s (see *Supervision Trees* below).
18
+
19
+ The `Concurrent::Supervisor` class is a faithful and nearly complete implementaion
20
+ of Erlang's Supervisor module.
21
+
22
+ ## Basic Supervisor Behavior
23
+
24
+ At the core a `Supervisor` instance is a very simple object. Simply create a `Supervisor`,
25
+ add at least one worker using the `#add_worker` method, and start the `Supervisor` using
26
+ either `#run` (blocking) or `#run!` (non-blocking). The `Supervisor` will spawn a new thread
27
+ for each child and start the chid on its thread. The `Supervisor` will then continuously
28
+ monitor all its child threads. If any of the children crash the `Supervisor` will restart
29
+ them in accordance with its *restart strategy* (see below). Later, stop the `Supervisor`
30
+ with its `#stop` method and it will gracefully stop all its children.
31
+
32
+ A `Supervisor` will also track the number of times it must restart children withing a
33
+ defined, sliding window of time. If the onfigured threshholds are exceeded (see *Intervals*
34
+ below) then the `Supervisor` will assume there is a catastrophic failure (possibly within
35
+ the `Supervisor` itself) and it will shut itself down. If the `Supervisor` is part of a
36
+ *supervision tree* (see below) then its `Supervisor` will likely restart it.
37
+
38
+ ```ruby
39
+ task = Concurrent::TimerTask.new{ print "[#{Time.now}] Hello world!\n" }
40
+
41
+ supervisor = Concurrent::Supervisor.new
42
+ supervisor.add_worker(task)
43
+
44
+ supervisor.run! # the #run method blocks, #run! does not
45
+ ```
46
+
47
+ ## Workers
48
+
49
+ Any object can be managed by a `Supervisor` so long as the class to be supervised supports
50
+ the required API. A supervised object needs only support three methods:
51
+
52
+ * `#run` is a blocking call that starts the child then blocks until the child is stopped
53
+ * `#running?` is a predicate method indicating whether or not the child is running
54
+ * `#stop` gracefully stops the child if it is running
55
+
56
+ ### Runnable
57
+
58
+ To facilitate the creation of supervisorable classes, the `Runnable` module is provided.
59
+ Simple include `Runnable` in the class and the required API methods will be provided.
60
+ `Runnable` also provides several lifecycle methods that may be overridden by the including
61
+ class. At a minimum the `#on_task` method *must* be overridden. `Runnable` will provide an
62
+ infinite loop that will start when either the `#run` or `#run!` method is called. The subclass
63
+ `#on_task` method will be called once in every iteration. The overridden method should provide
64
+ some sort of blocking behavior otherwise the run loop may monopolize the processor and spike
65
+ the processor utilization.
66
+
67
+ The following optional lifecycle methods are also provided:
68
+
69
+ * `#on_run` is called once when the object is started via the `#run` or `#run!` method but before the `#on_task` method is first called
70
+ * `#on_stop` is called once when the `#stop` method is called, after the last call to `#on_task`
71
+
72
+ ```ruby
73
+ class Echo
74
+ include Concurrent::Runnable
75
+
76
+ def initialize
77
+ @queue = Queue.new
78
+ end
79
+
80
+ def post(message)
81
+ @queue.push(message)
82
+ end
83
+
84
+ protected
85
+
86
+ def on_task
87
+ message = @queue.pop
88
+ print "#{message}\n"
89
+ end
90
+ end
91
+
92
+ echo = Echo.new
93
+ supervisor = Concurrent::Supervisor.new
94
+ supervisor.add_worker(echo)
95
+ supervisor.run!
96
+ ```
97
+
98
+ ## Supervisor Configuration
99
+
100
+ A newly-created `Supervisor` will be configured with a reasonable set of options that should
101
+ suffice for most purposes. In many cases no additional configuration will be required. When
102
+ more granular control is required a `Supervisor` may be given several configuration options
103
+ during initialization. Additionally, a few per-worker configuration options may be passed
104
+ during the call to `#add_worker`. Once a `Supervisor` is created and the workers are added
105
+ no additional configuration is possible.
106
+
107
+ ### Intervals
108
+
109
+ A `Supervisor` monitors its children and conducts triage operations based on several configurable
110
+ intervals:
111
+
112
+ * `:monitor_interval` specifies the number of seconds between health checks of the workers. The
113
+ higher the interval the longer a particular worker may be dead before being restarted. The
114
+ default is 1 second.
115
+ * `:max_restart` specifies the number of times (in total) the `Supevisor` may restart children
116
+ before it assumes there is a catastrophic failure and it shuts itself down. The default is 5
117
+ restarts.
118
+ * `:max_time` if the time interval over which `#max_restart` is tracked. Since `Supervisor` is
119
+ intended to be used in applications that may run forever the `#max_restart` count must be
120
+ timeboxed to prevent erroneous `Supervisor shutdown`. The default is 60 seconds.
121
+
122
+ ### Restart Strategy
123
+
124
+ When a child thread dies the `Supervisor` will restart it, and possibly other children,
125
+ with the expectation that the workers are capable of cleaning themselves up and running
126
+ again. The `Supervisor` will call each targetted worker's `#stop` method, kill the
127
+ worker's thread, spawn a new thread, and call the worker's `#run` method.
128
+
129
+ * `:one_for_one` When this restart strategy is set the `Supervisor` will only restart
130
+ the worker thread that has died. It will not restart any of the other children.
131
+ This is the default restart strategy.
132
+ * `:one_for_all` When this restart strategy is set the `Supervisor` will restart all
133
+ children when any one child dies. All workers will be stopped in the order they were
134
+ originally added to the `Supervisor`. Once all childrean have been stopped they will
135
+ all be started again in the same order.
136
+ * `:rest_for_one` This restart strategy assumes that the order the workers were added
137
+ to the `Supervisor` is meaningful. When one child dies all the downstream children
138
+ (children added to the `Supervisor` after the dead worker) will be restarted. The
139
+ `Supervisor` will begin by calling the `#stop` method on the dead worker and all
140
+ downstream workers. The `Supervisor` will then iterate over all dead workers and
141
+ restart each by creating a new thread then calling the worker's `#run` method.
142
+
143
+ When a restart is initiated under any strategy other than `:one_for_one` the
144
+ `:max_restart` value will only be incremented by one, regardless of how many children
145
+ are restarted.
146
+
147
+ ### Worker Restart Option
148
+
149
+ When a worker dies the default behavior of the `Supervisor` is to restart one or more
150
+ workers according to the restart strategy defined when the `Supervisor` is created
151
+ (see above). This behavior can be modified on a per-worker basis using the `:restart`
152
+ option when calling `#add_worker`. Three worker `:restart` options are supported:
153
+
154
+ * `:permanent` means the worker is intended to run forever and will always be restarted
155
+ (this is the default)
156
+ * `:temporary` workers are expected to stop on their own as a normal part of their operation
157
+ and will only be restarted on an abnormal exit
158
+ * `:transient` workers will never be restarted
159
+
160
+ ### Worker Type
161
+
162
+ Every worker added to a `Supervisor` is of either type `:worker` or `:supervisor`. The defauly
163
+ value is `:worker`. Currently this type makes no functional difference. It is purely informational.
164
+
165
+ ## Supervision Trees
166
+
167
+ One of the most powerful aspects of Erlang's supervisor module is its ability to supervise
168
+ other supervisors. This allows for the creation of deep, robust *supervision trees*.
169
+ Workers can be gouped under multiple bottom-level `Supervisor`s. Each of these `Supervisor`s
170
+ can be configured according to the needs of its workers. These multiple `Supervisor`s can
171
+ be added as children to another `Supervisor`. The root `Supervisor` can then start the
172
+ entire tree via trickel-down (start its children which start their children and so on).
173
+ The root `Supervisor` then monitor its child `Supervisor`s, and so on.
174
+
175
+ Supervision trees are the main reason that a `Supervisor` will shut itself down if its
176
+ `:max_restart`/`:max_time` threshhold is exceeded. An isolated `Supervisor` will simply
177
+ shut down forever. A `Supervisor` that is part of a supervision tree will shut itself
178
+ down and let its parent `Supervisor` manage the restart.
179
+
180
+ ## Examples
181
+
182
+ ```ruby
183
+ QUERIES = %w[YAHOO Microsoft google]
184
+
185
+ class FinanceActor < Concurrent::Actor
186
+ def act(query)
187
+ finance = Finance.new(query)
188
+ print "[#{Time.now}] RECEIVED '#{query}' to #{self} returned #{finance.update.suggested_symbols}\n\n"
189
+ end
190
+ end
191
+
192
+ financial, pool = FinanceActor.pool(5)
193
+
194
+ timer_proc = proc do
195
+ query = QUERIES[rand(QUERIES.length)]
196
+ financial.post(query)
197
+ print "[#{Time.now}] SENT '#{query}' from #{self} to worker pool\n\n"
198
+ end
199
+
200
+ t1 = Concurrent::TimerTask.new(execution_interval: rand(5)+1, &timer_proc)
201
+ t2 = Concurrent::TimerTask.new(execution_interval: rand(5)+1, &timer_proc)
202
+
203
+ overlord = Concurrent::Supervisor.new
204
+
205
+ overlord.add_worker(t1)
206
+ overlord.add_worker(t2)
207
+ pool.each{|actor| overlord.add_worker(actor)}
208
+
209
+ overlord.run!
210
+ ```
211
+
212
+ ## Additional Reading
10
213
 
11
214
  * [Supervisor Module](http://www.erlang.org/doc/man/supervisor.html)
12
215
  * [Supervisor Behaviour](http://www.erlang.org/doc/design_principles/sup_princ.html)
@@ -1,4 +1,4 @@
1
- # Being of Sound Mind
1
+ # To Gobbler's Knob. It's Groundhog Day.
2
2
 
3
3
  A very common currency pattern is to run a thread that performs a task at regular
4
4
  intervals. The thread that peforms the task sleeps for the given interval then
@@ -70,6 +70,192 @@ module Concurrent
70
70
  end
71
71
  end
72
72
 
73
+ context '#post?' do
74
+
75
+ it 'returns nil when not running' do
76
+ subject.post?.should be_false
77
+ end
78
+
79
+ it 'returns an Obligation' do
80
+ actor = actor_class.new
81
+ @thread = Thread.new{ actor.run }
82
+ @thread.join(0.1)
83
+ obligation = actor.post?(nil)
84
+ obligation.should be_a(Obligation)
85
+ actor.stop
86
+ end
87
+
88
+ it 'fulfills the obligation on success' do
89
+ actor = actor_class.new{|msg| @expected = msg }
90
+ @thread = Thread.new{ actor.run }
91
+ @thread.join(0.1)
92
+ obligation = actor.post?(42)
93
+ @thread.join(0.1)
94
+ obligation.should be_fulfilled
95
+ obligation.value.should == 42
96
+ actor.stop
97
+ end
98
+
99
+ it 'rejects the obligation on failure' do
100
+ actor = actor_class.new{|msg| raise StandardError.new('Boom!') }
101
+ @thread = Thread.new{ actor.run }
102
+ @thread.join(0.1)
103
+ obligation = actor.post?(42)
104
+ @thread.join(0.1)
105
+ obligation.should be_rejected
106
+ obligation.reason.should be_a(StandardError)
107
+ actor.stop
108
+ end
109
+ end
110
+
111
+ context '#post!' do
112
+
113
+ it 'raises Concurrent::Runnable::LifecycleError when not running' do
114
+ expect {
115
+ subject.post!(1)
116
+ }.to raise_error(Concurrent::Runnable::LifecycleError)
117
+ end
118
+
119
+ it 'blocks for up to the given number of seconds' do
120
+ actor = actor_class.new{|msg| sleep }
121
+ @thread = Thread.new{ actor.run }
122
+ @thread.join(0.1)
123
+ start = Time.now.to_i
124
+ expect {
125
+ actor.post!(2, nil)
126
+ }.to raise_error
127
+ elapsed = Time.now.to_i - start
128
+ elapsed.should >= 2
129
+ actor.stop
130
+ end
131
+
132
+ it 'raises Concurrent::TimeoutError when seconds is zero' do
133
+ actor = actor_class.new{|msg| 42 }
134
+ @thread = Thread.new{ actor.run }
135
+ @thread.join(0.1)
136
+ expect {
137
+ actor.post!(0, nil)
138
+ }.to raise_error(Concurrent::TimeoutError)
139
+ actor.stop
140
+ end
141
+
142
+ it 'raises Concurrent::TimeoutError on timeout' do
143
+ actor = actor_class.new{|msg| sleep }
144
+ @thread = Thread.new{ actor.run }
145
+ @thread.join(0.1)
146
+ expect {
147
+ actor.post!(1, nil)
148
+ }.to raise_error(Concurrent::TimeoutError)
149
+ actor.stop
150
+ end
151
+
152
+ it 'bubbles the exception on error' do
153
+ actor = actor_class.new{|msg| raise StandardError.new('Boom!') }
154
+ @thread = Thread.new{ actor.run }
155
+ @thread.join(0.1)
156
+ expect {
157
+ actor.post!(1, nil)
158
+ }.to raise_error(StandardError)
159
+ actor.stop
160
+ end
161
+
162
+ it 'returns the result on success' do
163
+ actor = actor_class.new{|msg| 42 }
164
+ @thread = Thread.new{ actor.run }
165
+ @thread.join(0.1)
166
+ expected = actor.post!(1, nil)
167
+ expected.should == 42
168
+ actor.stop
169
+ end
170
+
171
+ it 'attempts to cancel the operation on timeout' do
172
+ @expected = 0
173
+ actor = actor_class.new{|msg| sleep(0.5); @expected += 1 }
174
+ @thread = Thread.new{ actor.run }
175
+ @thread.join(0.1)
176
+ actor.post(nil) # block the actor
177
+ expect {
178
+ actor.post!(0.1, nil)
179
+ }.to raise_error(Concurrent::TimeoutError)
180
+ sleep(1.5)
181
+ @expected.should == 1
182
+ actor.stop
183
+ end
184
+ end
185
+
186
+ context '#forward' do
187
+
188
+ let(:sender_clazz) do
189
+ Class.new(Actor) do
190
+ def act(*message)
191
+ if message.first.is_a?(Exception)
192
+ raise message.first
193
+ else
194
+ return message.first
195
+ end
196
+ end
197
+ end
198
+ end
199
+
200
+ let(:receiver_clazz) do
201
+ Class.new(Actor) do
202
+ attr_reader :result
203
+ def act(*message)
204
+ @result = message.first
205
+ end
206
+ end
207
+ end
208
+
209
+ let(:sender) { sender_clazz.new }
210
+ let(:receiver) { receiver_clazz.new }
211
+
212
+ let(:observer) { double('observer') }
213
+
214
+ before(:each) do
215
+ @sender = Thread.new{ sender.run }
216
+ @receiver = Thread.new{ receiver.run }
217
+ sleep(0.1)
218
+ end
219
+
220
+ after(:each) do
221
+ sender.stop
222
+ receiver.stop
223
+ sleep(0.1)
224
+ @sender.kill unless @sender.nil?
225
+ @receiver.kill unless @receiver.nil?
226
+ end
227
+
228
+ it 'returns false when sender not running' do
229
+ sender_clazz.new.forward(receiver).should be_false
230
+ end
231
+
232
+ it 'forwards the result to the receiver on success' do
233
+ sender.forward(receiver, 42)
234
+ sleep(0.1)
235
+ receiver.result.should eq 42
236
+ end
237
+
238
+ it 'does not forward on exception' do
239
+ sender.forward(receiver, StandardError.new)
240
+ sleep(0.1)
241
+ receiver.result.should be_nil
242
+ end
243
+
244
+ it 'notifies observers on success' do
245
+ observer.should_receive(:update).with(any_args())
246
+ sender.add_observer(observer)
247
+ sender.forward(receiver, 42)
248
+ sleep(0.1)
249
+ end
250
+
251
+ it 'notifies observers on exception' do
252
+ observer.should_not_receive(:update).with(any_args())
253
+ sender.add_observer(observer)
254
+ sender.forward(receiver, StandardError.new)
255
+ sleep(0.1)
256
+ end
257
+ end
258
+
73
259
  context '#run' do
74
260
 
75
261
  it 'empties the queue' do
@@ -107,31 +293,6 @@ module Concurrent
107
293
  end
108
294
  end
109
295
 
110
- context 'message handling' do
111
-
112
- it 'runs the constructor block once for every message' do
113
- @expected = 0
114
- actor = actor_class.new{|msg| @expected += 1 }
115
- @thread = Thread.new{ actor.run }
116
- @thread.join(0.1)
117
- 10.times { actor.post(true) }
118
- @thread.join(0.1)
119
- @expected.should eq 10
120
- actor.stop
121
- end
122
-
123
- it 'passes the message to the block' do
124
- @expected = []
125
- actor = actor_class.new{|msg| @expected << msg }
126
- @thread = Thread.new{ actor.run }
127
- @thread.join(0.1)
128
- 10.times {|i| actor.post(i) }
129
- @thread.join(0.1)
130
- actor.stop
131
- @expected.should eq (0..9).to_a
132
- end
133
- end
134
-
135
296
  context 'exception handling' do
136
297
 
137
298
  it 'supresses exceptions thrown when handling messages' do
@@ -145,12 +306,34 @@ module Concurrent
145
306
  end
146
307
  end
147
308
 
148
- context 'observer notification' do
309
+ context 'observation' do
310
+
311
+ let(:actor_class) do
312
+ Class.new(Actor) do
313
+ def act(*message)
314
+ if message.first.is_a?(Exception)
315
+ raise message.first
316
+ else
317
+ return message.first
318
+ end
319
+ end
320
+ end
321
+ end
322
+
323
+ subject { Class.new(actor_class).new }
149
324
 
150
325
  let(:observer) do
151
326
  Class.new {
152
- attr_reader :notice
153
- def update(*args) @notice = args; end
327
+ attr_reader :time
328
+ attr_reader :message
329
+ attr_reader :value
330
+ attr_reader :reason
331
+ def update(time, message, value, reason)
332
+ @time = time
333
+ @message = message
334
+ @value = value
335
+ @reason = reason
336
+ end
154
337
  }.new
155
338
  end
156
339
 
@@ -159,32 +342,45 @@ module Concurrent
159
342
  subject.add_observer(observer)
160
343
  @thread = Thread.new{ subject.run }
161
344
  @thread.join(0.1)
162
- 10.times { subject.post(true) }
345
+ 10.times { subject.post(42) }
163
346
  @thread.join(0.1)
164
347
  end
165
348
 
166
- it 'does not notify observers when a message raises an exception' do
167
- observer.should_not_receive(:update).with(any_args())
168
- actor = actor_class.new{|msg| raise StandardError }
169
- actor.add_observer(observer)
170
- @thread = Thread.new{ actor.run }
349
+ it 'notifies observers when a message raises an exception' do
350
+ error = StandardError.new
351
+ observer.should_receive(:update).exactly(10).times.with(any_args())
352
+ subject.add_observer(observer)
353
+ @thread = Thread.new{ subject.run }
171
354
  @thread.join(0.1)
172
- 10.times { actor.post(true) }
355
+ 10.times { subject.post(error) }
173
356
  @thread.join(0.1)
174
- actor.stop
175
357
  end
176
358
 
177
- it 'passes the time, message, and result to the observer' do
178
- actor = actor_class.new{|*msg| msg }
179
- actor.add_observer(observer)
180
- @thread = Thread.new{ actor.run }
359
+ it 'passes the time, message, value, and reason to the observer on success' do
360
+ subject.add_observer(observer)
361
+ @thread = Thread.new{ subject.run }
181
362
  @thread.join(0.1)
182
- actor.post(42)
363
+ subject.post(42)
183
364
  @thread.join(0.1)
184
- observer.notice[0].should be_a(Time)
185
- observer.notice[1].should == [42]
186
- observer.notice[2].should == [42]
187
- actor.stop
365
+
366
+ observer.time.should be_a(Time)
367
+ observer.message.should eq [42]
368
+ observer.value.should eq 42
369
+ observer.reason.should be_nil
370
+ end
371
+
372
+ it 'passes the time, message, value, and reason to the observer on exception' do
373
+ error = StandardError.new
374
+ subject.add_observer(observer)
375
+ @thread = Thread.new{ subject.run }
376
+ @thread.join(0.1)
377
+ subject.post(error)
378
+ @thread.join(0.1)
379
+
380
+ observer.time.should be_a(Time)
381
+ observer.message.should eq [error]
382
+ observer.value.should be_nil
383
+ observer.reason.should be_a(Exception)
188
384
  end
189
385
  end
190
386
 
@@ -197,7 +393,7 @@ module Concurrent
197
393
  clazz.pool(0)
198
394
  }.to raise_error(ArgumentError)
199
395
  end
200
-
396
+
201
397
  it 'creates the requested number of actors' do
202
398
  mailbox, actors = clazz.pool(5)
203
399
  actors.size.should == 5
@@ -333,7 +529,7 @@ module Concurrent
333
529
  actor = actor_class.new
334
530
  supervisor = Supervisor.new
335
531
  supervisor.add_worker(actor)
336
-
532
+
337
533
  actor.should_receive(:run).with(no_args())
338
534
  supervisor.run!
339
535
  sleep(0.1)
@@ -364,7 +560,7 @@ module Concurrent
364
560
  actor = actor_class.new
365
561
  supervisor = Supervisor.new
366
562
  supervisor.add_worker(actor)
367
-
563
+
368
564
  supervisor.run!
369
565
  sleep(0.1)
370
566