concurrent-ruby 0.3.0 → 0.3.1.pre.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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