concurrent-ruby 0.3.0.pre.1 → 0.3.0.pre.2
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 +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
|