concurrent-ruby 0.3.0.pre.1 → 0.3.0.pre.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +10 -33
- data/lib/concurrent.rb +5 -11
- data/lib/concurrent/{channel.rb → actor.rb} +14 -18
- data/lib/concurrent/agent.rb +5 -4
- data/lib/concurrent/cached_thread_pool.rb +116 -25
- data/lib/concurrent/cached_thread_pool/worker.rb +91 -0
- data/lib/concurrent/event.rb +13 -14
- data/lib/concurrent/event_machine_defer_proxy.rb +0 -1
- data/lib/concurrent/executor.rb +0 -1
- data/lib/concurrent/fixed_thread_pool.rb +111 -14
- data/lib/concurrent/fixed_thread_pool/worker.rb +54 -0
- data/lib/concurrent/future.rb +0 -2
- data/lib/concurrent/global_thread_pool.rb +21 -3
- data/lib/concurrent/goroutine.rb +1 -5
- data/lib/concurrent/obligation.rb +0 -19
- data/lib/concurrent/promise.rb +2 -5
- data/lib/concurrent/runnable.rb +2 -8
- data/lib/concurrent/supervisor.rb +9 -4
- data/lib/concurrent/utilities.rb +24 -0
- data/lib/concurrent/version.rb +1 -1
- data/md/agent.md +3 -3
- data/md/future.md +4 -4
- data/md/promise.md +15 -25
- data/md/thread_pool.md +9 -8
- data/spec/concurrent/actor_spec.rb +377 -0
- data/spec/concurrent/agent_spec.rb +2 -1
- data/spec/concurrent/cached_thread_pool_spec.rb +19 -29
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +1 -1
- data/spec/concurrent/event_spec.rb +1 -1
- data/spec/concurrent/executor_spec.rb +0 -8
- data/spec/concurrent/fixed_thread_pool_spec.rb +27 -16
- data/spec/concurrent/future_spec.rb +0 -13
- data/spec/concurrent/global_thread_pool_spec.rb +73 -0
- data/spec/concurrent/goroutine_spec.rb +0 -15
- data/spec/concurrent/obligation_shared.rb +1 -38
- data/spec/concurrent/promise_spec.rb +28 -47
- data/spec/concurrent/supervisor_spec.rb +1 -2
- data/spec/concurrent/thread_pool_shared.rb +28 -7
- data/spec/concurrent/utilities_spec.rb +50 -0
- data/spec/spec_helper.rb +0 -1
- data/spec/support/functions.rb +17 -0
- metadata +12 -27
- data/lib/concurrent/functions.rb +0 -105
- data/lib/concurrent/null_thread_pool.rb +0 -25
- data/lib/concurrent/thread_pool.rb +0 -149
- data/md/reactor.md +0 -32
- data/spec/concurrent/channel_spec.rb +0 -446
- data/spec/concurrent/functions_spec.rb +0 -197
- data/spec/concurrent/null_thread_pool_spec.rb +0 -78
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
module Concurrent
|
4
|
+
|
5
|
+
TimeoutError = Class.new(StandardError)
|
6
|
+
|
7
|
+
def timeout(seconds)
|
8
|
+
|
9
|
+
thread = Thread.new do
|
10
|
+
Thread.current[:result] = yield
|
11
|
+
end
|
12
|
+
success = thread.join(seconds)
|
13
|
+
|
14
|
+
if success
|
15
|
+
return thread[:result]
|
16
|
+
else
|
17
|
+
raise TimeoutError
|
18
|
+
end
|
19
|
+
ensure
|
20
|
+
Thread.kill(thread) unless thread.nil?
|
21
|
+
end
|
22
|
+
module_function :timeout
|
23
|
+
|
24
|
+
end
|
data/lib/concurrent/version.rb
CHANGED
data/md/agent.md
CHANGED
@@ -42,7 +42,7 @@ score.value #=> 110
|
|
42
42
|
|
43
43
|
score << proc{|current| current * 2 }
|
44
44
|
sleep(0.1)
|
45
|
-
|
45
|
+
score.value #=> 220
|
46
46
|
|
47
47
|
score << proc{|current| current - 50 }
|
48
48
|
sleep(0.1)
|
@@ -52,7 +52,7 @@ score.value #=> 170
|
|
52
52
|
With validation and error handling:
|
53
53
|
|
54
54
|
```ruby
|
55
|
-
score =
|
55
|
+
score = Concurrent::Agent.new(0).validate{|value| value <= 1024 }.
|
56
56
|
rescue(NoMethodError){|ex| puts "Bam!" }.
|
57
57
|
rescue(ArgumentError){|ex| puts "Pow!" }.
|
58
58
|
rescue{|ex| puts "Boom!" }
|
@@ -81,7 +81,7 @@ bingo = Class.new{
|
|
81
81
|
end
|
82
82
|
}.new
|
83
83
|
|
84
|
-
score =
|
84
|
+
score = Concurrent::Agent.new(0)
|
85
85
|
score.add_observer(bingo)
|
86
86
|
|
87
87
|
score << proc{|current| sleep(0.1); current += 30 }
|
data/md/future.md
CHANGED
@@ -38,18 +38,18 @@ count.value(0) #=> nil (does not block)
|
|
38
38
|
count.value #=> 10 (after blocking)
|
39
39
|
count.state #=> :fulfilled
|
40
40
|
count.fulfilled? #=> true
|
41
|
-
|
41
|
+
count.value #=> 10
|
42
42
|
```
|
43
43
|
|
44
44
|
A rejected example:
|
45
45
|
|
46
46
|
```ruby
|
47
|
-
count =
|
47
|
+
count = Concurrent::Future.new{ sleep(10); raise StandardError.new("Boom!") }
|
48
48
|
count.state #=> :pending
|
49
|
-
pending?
|
49
|
+
count.pending? #=> true
|
50
50
|
|
51
51
|
deref(count) #=> nil (after blocking)
|
52
|
-
rejected?
|
52
|
+
count.rejected? #=> true
|
53
53
|
count.reason #=> #<StandardError: Boom!>
|
54
54
|
```
|
55
55
|
|
data/md/promise.md
CHANGED
@@ -44,10 +44,6 @@ Then create one
|
|
44
44
|
p = Promise.new("Jerry", "D'Antonio") do |first, last|
|
45
45
|
"#{last}, #{first}"
|
46
46
|
end
|
47
|
-
|
48
|
-
# -or-
|
49
|
-
|
50
|
-
p = promise(10){|x| x * x * x }
|
51
47
|
```
|
52
48
|
|
53
49
|
Promises can be chained using the `then` method. The `then` method
|
@@ -55,13 +51,13 @@ accepts a block but no arguments. The result of the each promise is
|
|
55
51
|
passed as the block argument to chained promises
|
56
52
|
|
57
53
|
```ruby
|
58
|
-
p =
|
54
|
+
p = Concurrent::Promise.new(10){|x| x * 2}.then{|result| result - 10 }
|
59
55
|
```
|
60
56
|
|
61
57
|
And so on, and so on, and so on...
|
62
58
|
|
63
59
|
```ruby
|
64
|
-
p =
|
60
|
+
p = Concurrent::Promise.new(10){|x| x * 2}.
|
65
61
|
then{|result| result - 10 }.
|
66
62
|
then{|result| result * 3 }.
|
67
63
|
then{|result| result % 5 }
|
@@ -69,9 +65,8 @@ p = promise(10){|x| x * 2}.
|
|
69
65
|
|
70
66
|
Promises are executed asynchronously so a newly-created promise *should* always be in the pending state
|
71
67
|
|
72
|
-
|
73
68
|
```ruby
|
74
|
-
p =
|
69
|
+
p = Concurrent::Promise.new{ "Hello, world!" }
|
75
70
|
p.state #=> :pending
|
76
71
|
p.pending? #=> true
|
77
72
|
```
|
@@ -79,27 +74,24 @@ p.pending? #=> true
|
|
79
74
|
Wait a little bit, and the promise will resolve and provide a value
|
80
75
|
|
81
76
|
```ruby
|
82
|
-
p =
|
77
|
+
p = Concurrent::Promise.new{ "Hello, world!" }
|
83
78
|
sleep(0.1)
|
84
79
|
|
85
80
|
p.state #=> :fulfilled
|
86
81
|
p.fulfilled? #=> true
|
87
|
-
|
88
82
|
p.value #=> "Hello, world!"
|
89
|
-
|
90
83
|
```
|
91
84
|
|
92
85
|
If an exception occurs, the promise will be rejected and will provide
|
93
86
|
a reason for the rejection
|
94
87
|
|
95
88
|
```ruby
|
96
|
-
p =
|
89
|
+
p = Concurrent::Promise.new{ raise StandardError.new("Here comes the Boom!") }
|
97
90
|
sleep(0.1)
|
98
91
|
|
99
92
|
p.state #=> :rejected
|
100
93
|
p.rejected? #=> true
|
101
|
-
|
102
|
-
p.reason=> #=> "#<StandardError: Here comes the Boom!>"
|
94
|
+
p.reason #=> "#<StandardError: Here comes the Boom!>"
|
103
95
|
```
|
104
96
|
|
105
97
|
### Rejection
|
@@ -108,7 +100,7 @@ Much like the economy, rejection exhibits a trickle-down effect. When
|
|
108
100
|
a promise is rejected all its children will be rejected
|
109
101
|
|
110
102
|
```ruby
|
111
|
-
p = [
|
103
|
+
p = [ Concurrent::Promise.new{ Thread.pass; raise StandardError } ]
|
112
104
|
|
113
105
|
10.times{|i| p << p.first.then{ i } }
|
114
106
|
sleep(0.1)
|
@@ -122,7 +114,7 @@ Once a promise is rejected it will not accept any children. Calls
|
|
122
114
|
to `then` will continually return `self`
|
123
115
|
|
124
116
|
```ruby
|
125
|
-
p =
|
117
|
+
p = Concurrent::Promise.new{ raise StandardError }
|
126
118
|
sleep(0.1)
|
127
119
|
|
128
120
|
p.object_id #=> 32960556
|
@@ -135,30 +127,28 @@ p.then{}.object_id #=> 32960556
|
|
135
127
|
Promises support error handling callbacks is a style mimicing Ruby's
|
136
128
|
own exception handling mechanism, namely `rescue`
|
137
129
|
|
138
|
-
|
139
130
|
```ruby
|
140
|
-
|
131
|
+
Concurrent::Promise.new{ "dangerous operation..." }.rescue{|ex| puts "Bam!" }
|
141
132
|
|
142
133
|
# -or- (for the Java/C# crowd)
|
143
|
-
|
134
|
+
Concurrent::Promise.new{ "dangerous operation..." }.catch{|ex| puts "Boom!" }
|
144
135
|
|
145
136
|
# -or- (for the hipsters)
|
146
|
-
|
137
|
+
Concurrent::Promise.new{ "dangerous operation..." }.on_error{|ex| puts "Pow!" }
|
147
138
|
```
|
148
139
|
|
149
140
|
As with Ruby's `rescue` mechanism, a promise's `rescue` method can
|
150
141
|
accept an optional Exception class argument (defaults to `Exception`
|
151
142
|
when not specified)
|
152
143
|
|
153
|
-
|
154
144
|
```ruby
|
155
|
-
|
145
|
+
Concurrent::Promise.new{ "dangerous operation..." }.rescue(ArgumentError){|ex| puts "Bam!" }
|
156
146
|
```
|
157
147
|
|
158
148
|
Calls to `rescue` can also be chained
|
159
149
|
|
160
150
|
```ruby
|
161
|
-
|
151
|
+
Concurrent::Promise.new{ "dangerous operation..." }.
|
162
152
|
rescue(ArgumentError){|ex| puts "Bam!" }.
|
163
153
|
rescue(NoMethodError){|ex| puts "Boom!" }.
|
164
154
|
rescue(StandardError){|ex| puts "Pow!" }
|
@@ -168,7 +158,7 @@ When there are multiple `rescue` handlers the first one to match the thrown
|
|
168
158
|
exception will be triggered
|
169
159
|
|
170
160
|
```ruby
|
171
|
-
|
161
|
+
Concurrent::Promise.new{ raise NoMethodError }.
|
172
162
|
rescue(ArgumentError){|ex| puts "Bam!" }.
|
173
163
|
rescue(NoMethodError){|ex| puts "Boom!" }.
|
174
164
|
rescue(StandardError){|ex| puts "Pow!" }
|
@@ -182,7 +172,7 @@ Trickle-down rejection also applies to rescue handlers. When a promise is reject
|
|
182
172
|
for any reason, its rescue handlers will be triggered. Rejection of the parent counts.
|
183
173
|
|
184
174
|
```ruby
|
185
|
-
|
175
|
+
Concurrent::Promise.new{ Thread.pass; raise StandardError }.
|
186
176
|
then{ true }.rescue{ puts 'Boom!' }.
|
187
177
|
then{ true }.rescue{ puts 'Boom!' }.
|
188
178
|
then{ true }.rescue{ puts 'Boom!' }.
|
data/md/thread_pool.md
CHANGED
@@ -87,9 +87,7 @@ From the docs:
|
|
87
87
|
### Examples
|
88
88
|
|
89
89
|
```ruby
|
90
|
-
require '
|
91
|
-
# or
|
92
|
-
require 'functional/concurrency'
|
90
|
+
require 'concurrent'
|
93
91
|
|
94
92
|
pool = Concurrent::CachedThreadPool.new
|
95
93
|
|
@@ -125,8 +123,11 @@ goroutines) run against a global thread pool. This pool can be directly accessed
|
|
125
123
|
`$GLOBAL_THREAD_POOL` global variable. Generally, this pool should not be directly accessed.
|
126
124
|
Use the other concurrency features instead.
|
127
125
|
|
128
|
-
By default the global thread pool is a `
|
129
|
-
|
126
|
+
By default the global thread pool is a `NullThreadPool`. This isn't a real thread pool at all.
|
127
|
+
It's simply a proxy for creating new threads on every post to the pool. I couldn't decide which
|
128
|
+
of the other threads pools and what configuration would be the most universally appropriate so
|
129
|
+
I punted. If you understand thread pools then you know enough to make your own choice. That's
|
130
|
+
why the global thread pool can be changed.
|
130
131
|
|
131
132
|
### Changing the Global Thread Pool
|
132
133
|
|
@@ -162,13 +163,13 @@ it is not an actual thread pool. Instead it spawns a new thread on every call to
|
|
162
163
|
The [EventMachine](http://rubyeventmachine.com/) library (source [online](https://github.com/eventmachine/eventmachine))
|
163
164
|
is an awesome library for creating evented applications. EventMachine provides its own thread pool
|
164
165
|
and the authors recommend using their pool rather than using Ruby's `Thread`. No sweat,
|
165
|
-
`
|
166
|
-
*before* requiring `
|
166
|
+
`concurrent-ruby` is fully compatible with EventMachine. Simple require `eventmachine`
|
167
|
+
*before* requiring `concurrent-ruby` then replace the global thread pool with an instance
|
167
168
|
of `EventMachineDeferProxy`:
|
168
169
|
|
169
170
|
```ruby
|
170
171
|
require 'eventmachine' # do this FIRST
|
171
|
-
require '
|
172
|
+
require 'concurrent'
|
172
173
|
|
173
174
|
$GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
|
174
175
|
```
|
@@ -0,0 +1,377 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative 'runnable_shared'
|
3
|
+
|
4
|
+
module Concurrent
|
5
|
+
|
6
|
+
describe Actor do
|
7
|
+
|
8
|
+
let(:actor_class) do
|
9
|
+
Class.new(Actor) do
|
10
|
+
attr_reader :last_message
|
11
|
+
def initialize(&block)
|
12
|
+
@task = block
|
13
|
+
super()
|
14
|
+
end
|
15
|
+
def act(*message)
|
16
|
+
@last_message = message
|
17
|
+
@task.call(*message) unless @task.nil?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
subject { Class.new(actor_class).new }
|
23
|
+
|
24
|
+
it_should_behave_like :runnable
|
25
|
+
|
26
|
+
after(:each) do
|
27
|
+
subject.stop
|
28
|
+
@thread.kill unless @thread.nil?
|
29
|
+
sleep(0.1)
|
30
|
+
end
|
31
|
+
|
32
|
+
context '#post' do
|
33
|
+
|
34
|
+
it 'returns false when not running' do
|
35
|
+
subject.post.should be_false
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'pushes a message onto the queue' do
|
39
|
+
@expected = false
|
40
|
+
actor = actor_class.new{|msg| @expected = msg }
|
41
|
+
@thread = Thread.new{ actor.run }
|
42
|
+
@thread.join(0.1)
|
43
|
+
actor.post(true)
|
44
|
+
@thread.join(0.1)
|
45
|
+
@expected.should be_true
|
46
|
+
actor.stop
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'returns the current size of the queue' do
|
50
|
+
actor = actor_class.new{|msg| sleep }
|
51
|
+
@thread = Thread.new{ actor.run }
|
52
|
+
@thread.join(0.1)
|
53
|
+
actor.post(true).should == 1
|
54
|
+
@thread.join(0.1)
|
55
|
+
actor.post(true).should == 1
|
56
|
+
@thread.join(0.1)
|
57
|
+
actor.post(true).should == 2
|
58
|
+
actor.stop
|
59
|
+
end
|
60
|
+
|
61
|
+
it 'is aliased a <<' do
|
62
|
+
@expected = false
|
63
|
+
actor = actor_class.new{|msg| @expected = msg }
|
64
|
+
@thread = Thread.new{ actor.run }
|
65
|
+
@thread.join(0.1)
|
66
|
+
actor << true
|
67
|
+
@thread.join(0.1)
|
68
|
+
@expected.should be_true
|
69
|
+
actor.stop
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
context '#run' do
|
74
|
+
|
75
|
+
it 'empties the queue' do
|
76
|
+
@thread = Thread.new{ subject.run }
|
77
|
+
@thread.join(0.1)
|
78
|
+
q = subject.instance_variable_get(:@queue)
|
79
|
+
q.size.should == 0
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context '#stop' do
|
84
|
+
|
85
|
+
it 'empties the queue' do
|
86
|
+
actor = actor_class.new{|msg| sleep }
|
87
|
+
@thread = Thread.new{ actor.run }
|
88
|
+
10.times { actor.post(true) }
|
89
|
+
@thread.join(0.1)
|
90
|
+
actor.stop
|
91
|
+
@thread.join(0.1)
|
92
|
+
q = actor.instance_variable_get(:@queue)
|
93
|
+
if q.size >= 1
|
94
|
+
q.pop.should == :stop
|
95
|
+
else
|
96
|
+
q.size.should == 0
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'pushes a :stop message onto the queue' do
|
101
|
+
@thread = Thread.new{ subject.run }
|
102
|
+
@thread.join(0.1)
|
103
|
+
q = subject.instance_variable_get(:@queue)
|
104
|
+
q.should_receive(:push).once.with(:stop)
|
105
|
+
subject.stop
|
106
|
+
@thread.join(0.1)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
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
|
+
context 'exception handling' do
|
136
|
+
|
137
|
+
it 'supresses exceptions thrown when handling messages' do
|
138
|
+
actor = actor_class.new{|msg| raise StandardError }
|
139
|
+
@thread = Thread.new{ actor.run }
|
140
|
+
expect {
|
141
|
+
@thread.join(0.1)
|
142
|
+
10.times { actor.post(true) }
|
143
|
+
}.not_to raise_error
|
144
|
+
actor.stop
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
context 'observer notification' do
|
149
|
+
|
150
|
+
let(:observer) do
|
151
|
+
Class.new {
|
152
|
+
attr_reader :notice
|
153
|
+
def update(*args) @notice = args; end
|
154
|
+
}.new
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'notifies observers when a message is successfully handled' do
|
158
|
+
observer.should_receive(:update).exactly(10).times.with(any_args())
|
159
|
+
subject.add_observer(observer)
|
160
|
+
@thread = Thread.new{ subject.run }
|
161
|
+
@thread.join(0.1)
|
162
|
+
10.times { subject.post(true) }
|
163
|
+
@thread.join(0.1)
|
164
|
+
end
|
165
|
+
|
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 }
|
171
|
+
@thread.join(0.1)
|
172
|
+
10.times { actor.post(true) }
|
173
|
+
@thread.join(0.1)
|
174
|
+
actor.stop
|
175
|
+
end
|
176
|
+
|
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 }
|
181
|
+
@thread.join(0.1)
|
182
|
+
actor.post(42)
|
183
|
+
@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
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
context '#pool' do
|
192
|
+
|
193
|
+
let(:clazz){ Class.new(actor_class) }
|
194
|
+
|
195
|
+
it 'raises an exception if the count is zero or less' do
|
196
|
+
expect {
|
197
|
+
clazz.pool(0)
|
198
|
+
}.to raise_error(ArgumentError)
|
199
|
+
end
|
200
|
+
|
201
|
+
it 'creates the requested number of actors' do
|
202
|
+
mailbox, actors = clazz.pool(5)
|
203
|
+
actors.size.should == 5
|
204
|
+
end
|
205
|
+
|
206
|
+
it 'passes the block to each actor' do
|
207
|
+
block = proc{ nil }
|
208
|
+
clazz.should_receive(:new).with(&block)
|
209
|
+
clazz.pool(1, &block)
|
210
|
+
end
|
211
|
+
|
212
|
+
it 'gives all actors the same mailbox' do
|
213
|
+
mailbox, actors = clazz.pool(2)
|
214
|
+
mbox1 = actors.first.instance_variable_get(:@queue)
|
215
|
+
mbox2 = actors.last.instance_variable_get(:@queue)
|
216
|
+
mbox1.should eq mbox2
|
217
|
+
end
|
218
|
+
|
219
|
+
it 'returns a Poolbox as the first retval' do
|
220
|
+
mailbox, actors = clazz.pool(2)
|
221
|
+
mailbox.should be_a(Actor::Poolbox)
|
222
|
+
end
|
223
|
+
|
224
|
+
it 'gives the Poolbox the same mailbox as the actors' do
|
225
|
+
mailbox, actors = clazz.pool(1)
|
226
|
+
mbox1 = mailbox.instance_variable_get(:@queue)
|
227
|
+
mbox2 = actors.first.instance_variable_get(:@queue)
|
228
|
+
mbox1.should eq mbox2
|
229
|
+
end
|
230
|
+
|
231
|
+
it 'returns an array of actors as the second retval' do
|
232
|
+
mailbox, actors = clazz.pool(2)
|
233
|
+
actors.each do |actor|
|
234
|
+
actor.should be_a(clazz)
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
it 'posts to the mailbox with Poolbox#post' do
|
239
|
+
@expected = false
|
240
|
+
mailbox, actors = clazz.pool(1){|msg| @expected = true }
|
241
|
+
@thread = Thread.new{ actors.first.run }
|
242
|
+
sleep(0.1)
|
243
|
+
mailbox.post(42)
|
244
|
+
sleep(0.1)
|
245
|
+
actors.each{|actor| actor.stop }
|
246
|
+
@thread.kill
|
247
|
+
@expected.should be_true
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'posts to the mailbox with Poolbox#<<' do
|
251
|
+
@expected = false
|
252
|
+
mailbox, actors = clazz.pool(1){|msg| @expected = true }
|
253
|
+
@thread = Thread.new{ actors.first.run }
|
254
|
+
sleep(0.1)
|
255
|
+
mailbox << 42
|
256
|
+
sleep(0.1)
|
257
|
+
actors.each{|actor| actor.stop }
|
258
|
+
@thread.kill
|
259
|
+
@expected.should be_true
|
260
|
+
end
|
261
|
+
end
|
262
|
+
|
263
|
+
context 'subclassing' do
|
264
|
+
|
265
|
+
after(:each) do
|
266
|
+
@thread.kill unless @thread.nil?
|
267
|
+
end
|
268
|
+
|
269
|
+
context '#pool' do
|
270
|
+
|
271
|
+
it 'creates actors of the appropriate subclass' do
|
272
|
+
actor = Class.new(actor_class)
|
273
|
+
mailbox, actors = actor.pool(1)
|
274
|
+
actors.first.should be_a(actor)
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
context '#act overloading' do
|
279
|
+
|
280
|
+
it 'raises an exception if #act is not implemented in the subclass' do
|
281
|
+
actor = Class.new(Actor).new
|
282
|
+
@thread = Thread.new{ actor.run }
|
283
|
+
@thread.join(0.1)
|
284
|
+
expect {
|
285
|
+
actor.post(:foo)
|
286
|
+
@thread.join(0.1)
|
287
|
+
}.to raise_error(NotImplementedError)
|
288
|
+
actor.stop
|
289
|
+
end
|
290
|
+
|
291
|
+
it 'uses the subclass #act implementation' do
|
292
|
+
actor = actor_class.new{|*args| @expected = true }
|
293
|
+
@thread = Thread.new{ actor.run }
|
294
|
+
@thread.join(0.1)
|
295
|
+
actor.post(:foo)
|
296
|
+
@thread.join(0.1)
|
297
|
+
actor.last_message.should eq [:foo]
|
298
|
+
actor.stop
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
context '#on_error overloading' do
|
303
|
+
|
304
|
+
let(:bad_actor) do
|
305
|
+
Class.new(actor_class) {
|
306
|
+
attr_reader :last_error
|
307
|
+
def act(*message)
|
308
|
+
raise StandardError
|
309
|
+
end
|
310
|
+
def on_error(*args)
|
311
|
+
@last_error = args
|
312
|
+
end
|
313
|
+
}
|
314
|
+
end
|
315
|
+
|
316
|
+
it 'uses the subclass #on_error implementation' do
|
317
|
+
actor = bad_actor.new
|
318
|
+
@thread = Thread.new{ actor.run }
|
319
|
+
@thread.join(0.1)
|
320
|
+
actor.post(42)
|
321
|
+
@thread.join(0.1)
|
322
|
+
actor.last_error[0].should be_a(Time)
|
323
|
+
actor.last_error[1].should eq [42]
|
324
|
+
actor.last_error[2].should be_a(StandardError)
|
325
|
+
actor.stop
|
326
|
+
end
|
327
|
+
end
|
328
|
+
end
|
329
|
+
|
330
|
+
context 'supervision' do
|
331
|
+
|
332
|
+
it 'can be started by a Supervisor' do
|
333
|
+
actor = actor_class.new
|
334
|
+
supervisor = Supervisor.new
|
335
|
+
supervisor.add_worker(actor)
|
336
|
+
|
337
|
+
actor.should_receive(:run).with(no_args())
|
338
|
+
supervisor.run!
|
339
|
+
sleep(0.1)
|
340
|
+
|
341
|
+
supervisor.stop
|
342
|
+
sleep(0.1)
|
343
|
+
actor.stop
|
344
|
+
end
|
345
|
+
|
346
|
+
it 'can receive messages while under supervision' do
|
347
|
+
@expected = false
|
348
|
+
actor = actor_class.new{|*args| @expected = true}
|
349
|
+
supervisor = Supervisor.new
|
350
|
+
supervisor.add_worker(actor)
|
351
|
+
supervisor.run!
|
352
|
+
sleep(0.1)
|
353
|
+
|
354
|
+
actor.post(42)
|
355
|
+
sleep(0.1)
|
356
|
+
@expected.should be_true
|
357
|
+
|
358
|
+
supervisor.stop
|
359
|
+
sleep(0.1)
|
360
|
+
actor.stop
|
361
|
+
end
|
362
|
+
|
363
|
+
it 'can be stopped by a supervisor' do
|
364
|
+
actor = actor_class.new
|
365
|
+
supervisor = Supervisor.new
|
366
|
+
supervisor.add_worker(actor)
|
367
|
+
|
368
|
+
supervisor.run!
|
369
|
+
sleep(0.1)
|
370
|
+
|
371
|
+
actor.should_receive(:stop).with(no_args())
|
372
|
+
supervisor.stop
|
373
|
+
sleep(0.1)
|
374
|
+
end
|
375
|
+
end
|
376
|
+
end
|
377
|
+
end
|