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.
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