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.
- checksums.yaml +4 -4
- data/README.md +55 -5
- data/lib/concurrent.rb +3 -0
- data/lib/concurrent/actor.rb +90 -36
- data/lib/concurrent/agent.rb +7 -21
- data/lib/concurrent/contract.rb +20 -0
- data/lib/concurrent/dereferenceable.rb +33 -0
- data/lib/concurrent/future.rb +7 -6
- data/lib/concurrent/obligation.rb +4 -3
- data/lib/concurrent/promise.rb +3 -3
- data/lib/concurrent/scheduled_task.rb +94 -0
- data/lib/concurrent/version.rb +1 -1
- data/md/actor.md +209 -0
- data/md/agent.md +30 -11
- data/md/future.md +55 -13
- data/md/scheduled_task.md +34 -0
- data/md/supervisor.md +209 -6
- data/md/timer_task.md +1 -1
- data/spec/concurrent/actor_spec.rb +244 -48
- data/spec/concurrent/agent_spec.rb +52 -1
- data/spec/concurrent/contract_spec.rb +34 -0
- data/spec/concurrent/future_spec.rb +6 -1
- data/spec/concurrent/obligation_shared.rb +1 -1
- data/spec/concurrent/promise_spec.rb +6 -1
- data/spec/concurrent/runnable_shared.rb +1 -1
- data/spec/concurrent/scheduled_task_spec.rb +259 -0
- metadata +14 -5
@@ -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.
|
data/md/supervisor.md
CHANGED
@@ -1,12 +1,215 @@
|
|
1
1
|
# You don't need to get no supervisor! You the supervisor today!
|
2
2
|
|
3
|
-
|
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
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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)
|
data/md/timer_task.md
CHANGED
@@ -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 '
|
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 :
|
153
|
-
|
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(
|
345
|
+
10.times { subject.post(42) }
|
163
346
|
@thread.join(0.1)
|
164
347
|
end
|
165
348
|
|
166
|
-
it '
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
@thread = Thread.new{
|
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 {
|
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
|
178
|
-
|
179
|
-
|
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
|
-
|
363
|
+
subject.post(42)
|
183
364
|
@thread.join(0.1)
|
184
|
-
|
185
|
-
observer.
|
186
|
-
observer.
|
187
|
-
|
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
|
|