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.
Files changed (50) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +10 -33
  3. data/lib/concurrent.rb +5 -11
  4. data/lib/concurrent/{channel.rb → actor.rb} +14 -18
  5. data/lib/concurrent/agent.rb +5 -4
  6. data/lib/concurrent/cached_thread_pool.rb +116 -25
  7. data/lib/concurrent/cached_thread_pool/worker.rb +91 -0
  8. data/lib/concurrent/event.rb +13 -14
  9. data/lib/concurrent/event_machine_defer_proxy.rb +0 -1
  10. data/lib/concurrent/executor.rb +0 -1
  11. data/lib/concurrent/fixed_thread_pool.rb +111 -14
  12. data/lib/concurrent/fixed_thread_pool/worker.rb +54 -0
  13. data/lib/concurrent/future.rb +0 -2
  14. data/lib/concurrent/global_thread_pool.rb +21 -3
  15. data/lib/concurrent/goroutine.rb +1 -5
  16. data/lib/concurrent/obligation.rb +0 -19
  17. data/lib/concurrent/promise.rb +2 -5
  18. data/lib/concurrent/runnable.rb +2 -8
  19. data/lib/concurrent/supervisor.rb +9 -4
  20. data/lib/concurrent/utilities.rb +24 -0
  21. data/lib/concurrent/version.rb +1 -1
  22. data/md/agent.md +3 -3
  23. data/md/future.md +4 -4
  24. data/md/promise.md +15 -25
  25. data/md/thread_pool.md +9 -8
  26. data/spec/concurrent/actor_spec.rb +377 -0
  27. data/spec/concurrent/agent_spec.rb +2 -1
  28. data/spec/concurrent/cached_thread_pool_spec.rb +19 -29
  29. data/spec/concurrent/event_machine_defer_proxy_spec.rb +1 -1
  30. data/spec/concurrent/event_spec.rb +1 -1
  31. data/spec/concurrent/executor_spec.rb +0 -8
  32. data/spec/concurrent/fixed_thread_pool_spec.rb +27 -16
  33. data/spec/concurrent/future_spec.rb +0 -13
  34. data/spec/concurrent/global_thread_pool_spec.rb +73 -0
  35. data/spec/concurrent/goroutine_spec.rb +0 -15
  36. data/spec/concurrent/obligation_shared.rb +1 -38
  37. data/spec/concurrent/promise_spec.rb +28 -47
  38. data/spec/concurrent/supervisor_spec.rb +1 -2
  39. data/spec/concurrent/thread_pool_shared.rb +28 -7
  40. data/spec/concurrent/utilities_spec.rb +50 -0
  41. data/spec/spec_helper.rb +0 -1
  42. data/spec/support/functions.rb +17 -0
  43. metadata +12 -27
  44. data/lib/concurrent/functions.rb +0 -105
  45. data/lib/concurrent/null_thread_pool.rb +0 -25
  46. data/lib/concurrent/thread_pool.rb +0 -149
  47. data/md/reactor.md +0 -32
  48. data/spec/concurrent/channel_spec.rb +0 -446
  49. data/spec/concurrent/functions_spec.rb +0 -197
  50. 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
@@ -1,3 +1,3 @@
1
1
  module Concurrent
2
- VERSION = '0.3.0.pre.1'
2
+ VERSION = '0.3.0.pre.2'
3
3
  end
@@ -42,7 +42,7 @@ score.value #=> 110
42
42
 
43
43
  score << proc{|current| current * 2 }
44
44
  sleep(0.1)
45
- deref score #=> 220
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 = agent(0).validate{|value| value <= 1024 }.
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 = agent(0)
84
+ score = Concurrent::Agent.new(0)
85
85
  score.add_observer(bingo)
86
86
 
87
87
  score << proc{|current| sleep(0.1); current += 30 }
@@ -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
- deref count #=> 10
41
+ count.value #=> 10
42
42
  ```
43
43
 
44
44
  A rejected example:
45
45
 
46
46
  ```ruby
47
- count = future{ sleep(10); raise StandardError.new("Boom!") }
47
+ count = Concurrent::Future.new{ sleep(10); raise StandardError.new("Boom!") }
48
48
  count.state #=> :pending
49
- pending?(count) #=> true
49
+ count.pending? #=> true
50
50
 
51
51
  deref(count) #=> nil (after blocking)
52
- rejected?(count) #=> true
52
+ count.rejected? #=> true
53
53
  count.reason #=> #<StandardError: Boom!>
54
54
  ```
55
55
 
@@ -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 = promise(10){|x| x * 2}.then{|result| result - 10 }
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 = promise(10){|x| x * 2}.
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 = promise{ "Hello, world!" }
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 = promise{ "Hello, world!" }
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 = promise{ raise StandardError.new("Here comes the Boom!") }
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 = [ promise{ Thread.pass; raise StandardError } ]
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 = promise{ raise StandardError }
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
- promise{ "dangerous operation..." }.rescue{|ex| puts "Bam!" }
131
+ Concurrent::Promise.new{ "dangerous operation..." }.rescue{|ex| puts "Bam!" }
141
132
 
142
133
  # -or- (for the Java/C# crowd)
143
- promise{ "dangerous operation..." }.catch{|ex| puts "Boom!" }
134
+ Concurrent::Promise.new{ "dangerous operation..." }.catch{|ex| puts "Boom!" }
144
135
 
145
136
  # -or- (for the hipsters)
146
- promise{ "dangerous operation..." }.on_error{|ex| puts "Pow!" }
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
- promise{ "dangerous operation..." }.rescue(ArgumentError){|ex| puts "Bam!" }
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
- promise{ "dangerous operation..." }.
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
- promise{ raise NoMethodError }.
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
- promise{ Thread.pass; raise StandardError }.
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!' }.
@@ -87,9 +87,7 @@ From the docs:
87
87
  ### Examples
88
88
 
89
89
  ```ruby
90
- require 'functional/cached_thread_pool'
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 `CachedThreadPool`. This means it consumes no resources
129
- unless concurrency functions are called. Most of the time this pool can simply be left alone.
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
- `functional-ruby` is fully compatible with EventMachine. Simple require `eventmachine`
166
- *before* requiring `functional-ruby` then replace the global thread pool with an instance
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 'functional/concurrency'
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