concurrent-ruby 0.4.1 → 0.5.0.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 +31 -33
- data/lib/concurrent.rb +11 -3
- data/lib/concurrent/actor.rb +29 -29
- data/lib/concurrent/agent.rb +98 -16
- data/lib/concurrent/atomic.rb +125 -0
- data/lib/concurrent/channel.rb +36 -1
- data/lib/concurrent/condition.rb +67 -0
- data/lib/concurrent/copy_on_notify_observer_set.rb +80 -0
- data/lib/concurrent/copy_on_write_observer_set.rb +94 -0
- data/lib/concurrent/count_down_latch.rb +60 -0
- data/lib/concurrent/dataflow.rb +85 -0
- data/lib/concurrent/dereferenceable.rb +69 -31
- data/lib/concurrent/event.rb +27 -21
- data/lib/concurrent/future.rb +103 -43
- data/lib/concurrent/ivar.rb +78 -0
- data/lib/concurrent/mvar.rb +154 -0
- data/lib/concurrent/obligation.rb +94 -9
- data/lib/concurrent/postable.rb +11 -9
- data/lib/concurrent/promise.rb +101 -127
- data/lib/concurrent/safe_task_executor.rb +28 -0
- data/lib/concurrent/scheduled_task.rb +60 -54
- data/lib/concurrent/stoppable.rb +2 -2
- data/lib/concurrent/supervisor.rb +36 -29
- data/lib/concurrent/thread_local_var.rb +117 -0
- data/lib/concurrent/timer_task.rb +28 -30
- data/lib/concurrent/utilities.rb +1 -1
- data/lib/concurrent/version.rb +1 -1
- data/spec/concurrent/agent_spec.rb +121 -230
- data/spec/concurrent/atomic_spec.rb +201 -0
- data/spec/concurrent/condition_spec.rb +171 -0
- data/spec/concurrent/copy_on_notify_observer_set_spec.rb +10 -0
- data/spec/concurrent/copy_on_write_observer_set_spec.rb +10 -0
- data/spec/concurrent/count_down_latch_spec.rb +125 -0
- data/spec/concurrent/dataflow_spec.rb +160 -0
- data/spec/concurrent/dereferenceable_shared.rb +145 -0
- data/spec/concurrent/event_spec.rb +44 -9
- data/spec/concurrent/fixed_thread_pool_spec.rb +0 -1
- data/spec/concurrent/future_spec.rb +184 -69
- data/spec/concurrent/ivar_spec.rb +192 -0
- data/spec/concurrent/mvar_spec.rb +380 -0
- data/spec/concurrent/obligation_spec.rb +193 -0
- data/spec/concurrent/observer_set_shared.rb +233 -0
- data/spec/concurrent/postable_shared.rb +3 -7
- data/spec/concurrent/promise_spec.rb +270 -192
- data/spec/concurrent/safe_task_executor_spec.rb +58 -0
- data/spec/concurrent/scheduled_task_spec.rb +142 -38
- data/spec/concurrent/thread_local_var_spec.rb +113 -0
- data/spec/concurrent/thread_pool_shared.rb +2 -3
- data/spec/concurrent/timer_task_spec.rb +31 -1
- data/spec/spec_helper.rb +2 -3
- data/spec/support/functions.rb +4 -0
- data/spec/support/less_than_or_equal_to_matcher.rb +5 -0
- metadata +50 -30
- data/lib/concurrent/contract.rb +0 -21
- data/lib/concurrent/event_machine_defer_proxy.rb +0 -22
- data/md/actor.md +0 -404
- data/md/agent.md +0 -142
- data/md/channel.md +0 -40
- data/md/dereferenceable.md +0 -49
- data/md/future.md +0 -125
- data/md/obligation.md +0 -32
- data/md/promise.md +0 -217
- data/md/scheduled_task.md +0 -156
- data/md/supervisor.md +0 -246
- data/md/thread_pool.md +0 -225
- data/md/timer_task.md +0 -191
- data/spec/concurrent/contract_spec.rb +0 -34
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +0 -240
data/md/timer_task.md
DELETED
@@ -1,191 +0,0 @@
|
|
1
|
-
# To Gobbler's Knob. It's Groundhog Day!
|
2
|
-
|
3
|
-
A very common currency pattern is to run a thread that performs a task at regular
|
4
|
-
intervals. The thread that peforms the task sleeps for the given interval then
|
5
|
-
wakes up and performs the task. Lather, rinse, repeat... This pattern causes two
|
6
|
-
problems. First, it is difficult to test the business logic of the task becuse the
|
7
|
-
task itself is tightly coupled with the concurrency logic. Second, an exception in
|
8
|
-
raised while performing the task can cause the entire thread to abend. In a
|
9
|
-
long-running application where the task thread is intended to run for days/weeks/years
|
10
|
-
a crashed task thread can pose a significant problem. `TimerTask` alleviates both problems.
|
11
|
-
|
12
|
-
When a `TimerTask` is launched it starts a thread for monitoring the execution interval.
|
13
|
-
The `TimerTask` thread does not perform the task, however. Instead, the TimerTask
|
14
|
-
launches the task on a separate thread. Should the task experience an unrecoverable
|
15
|
-
crash only the task thread will crash. This makes the `TimerTask` very fault tolerant
|
16
|
-
Additionally, the `TimerTask` thread can respond to the success or failure of the task,
|
17
|
-
performing logging or ancillary operations. `TimerTask` can also be configured with a
|
18
|
-
timeout value allowing it to kill a task that runs too long.
|
19
|
-
|
20
|
-
One other advantage of `TimerTask` is it forces the bsiness logic to be completely decoupled
|
21
|
-
from the concurrency logic. The business logic can be tested separately then passed to the
|
22
|
-
`TimerTask` for scheduling and running.
|
23
|
-
|
24
|
-
Unlike other abstraction in this library, `TimerTask` does not run on the global thread pool.
|
25
|
-
In my experience the types of tasks that will benefit from `TimerTask` tend to also be long
|
26
|
-
running. For this reason they get their own thread every time the task is executed.
|
27
|
-
|
28
|
-
This class is based on the Java class
|
29
|
-
[of the same name](http://docs.oracle.com/javase/7/docs/api/java/util/TimerTask.html).
|
30
|
-
|
31
|
-
## Observation
|
32
|
-
|
33
|
-
`TimerTask` supports notification through the Ruby standard library
|
34
|
-
[Observable](http://ruby-doc.org/stdlib-2.0/libdoc/observer/rdoc/Observable.html)
|
35
|
-
module. On execution the `TimerTask` will notify the observers with thress arguments:
|
36
|
-
time of execution, the result of the block (or nil on failure), and any raised
|
37
|
-
exceptions (or nil on success). If the timeout interval is exceeded the observer
|
38
|
-
will receive a `Concurrent::TimeoutError` object as the third argument.
|
39
|
-
|
40
|
-
## Examples
|
41
|
-
|
42
|
-
A basic example:
|
43
|
-
|
44
|
-
```ruby
|
45
|
-
require 'concurrent'
|
46
|
-
|
47
|
-
task = Concurrent::TimerTask.new{ puts 'Boom!' }
|
48
|
-
task.run!
|
49
|
-
|
50
|
-
task.execution_interval #=> 60 (default)
|
51
|
-
task.timeout_interval #=> 30 (default)
|
52
|
-
|
53
|
-
# wait 60 seconds...
|
54
|
-
#=> 'Boom!'
|
55
|
-
|
56
|
-
task.stop #=> true
|
57
|
-
```
|
58
|
-
|
59
|
-
Both the execution_interval and the timeout_interval can be configured:
|
60
|
-
|
61
|
-
```ruby
|
62
|
-
task = Concurrent::TimerTask.new(execution_interval: 5, timeout_interval: 5) do
|
63
|
-
puts 'Boom!'
|
64
|
-
end
|
65
|
-
|
66
|
-
task.execution_interval #=> 5
|
67
|
-
task.timeout_interval #=> 5
|
68
|
-
```
|
69
|
-
|
70
|
-
By default an `TimerTask` will wait for `:execution_interval` seconds before running the block.
|
71
|
-
To run the block immediately set the `:run_now` option to `true`:
|
72
|
-
|
73
|
-
```ruby
|
74
|
-
task = Concurrent::TimerTask.new(run_now: true){ puts 'Boom!' }
|
75
|
-
task.run!
|
76
|
-
|
77
|
-
#=> 'Boom!'
|
78
|
-
```
|
79
|
-
|
80
|
-
The `TimerTask` class includes the `Dereferenceable` mixin module so the result of
|
81
|
-
the last execution is always available via the `#value` method. Derefencing options
|
82
|
-
can be passed to the `TimerTask` during construction or at any later time using the
|
83
|
-
`#set_deref_options` method.
|
84
|
-
|
85
|
-
```ruby
|
86
|
-
task = Concurrent::TimerTask.new(
|
87
|
-
dup_on_deref: true,
|
88
|
-
execution_interval: 5
|
89
|
-
){ Time.now }
|
90
|
-
|
91
|
-
task.run!
|
92
|
-
Time.now #=> 2013-11-07 18:06:50 -0500
|
93
|
-
sleep(10)
|
94
|
-
task.value #=> 2013-11-07 18:06:55 -0500
|
95
|
-
```
|
96
|
-
|
97
|
-
A simple example with observation:
|
98
|
-
|
99
|
-
```ruby
|
100
|
-
class TaskObserver
|
101
|
-
def update(time, result, ex)
|
102
|
-
if result
|
103
|
-
print "(#{time}) Execution successfully returned #{result}\n"
|
104
|
-
elsif ex.is_a?(Concurrent::TimeoutError)
|
105
|
-
print "(#{time}) Execution timed out\n"
|
106
|
-
else
|
107
|
-
print "(#{time}) Execution failed with error #{ex}\n"
|
108
|
-
end
|
109
|
-
end
|
110
|
-
end
|
111
|
-
|
112
|
-
task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ 42 }
|
113
|
-
task.add_observer(TaskObserver.new)
|
114
|
-
task.run!
|
115
|
-
|
116
|
-
#=> (2013-10-13 19:08:58 -0400) Execution successfully returned 42
|
117
|
-
#=> (2013-10-13 19:08:59 -0400) Execution successfully returned 42
|
118
|
-
#=> (2013-10-13 19:09:00 -0400) Execution successfully returned 42
|
119
|
-
task.stop
|
120
|
-
|
121
|
-
task = Concurrent::TimerTask.new(execution_interval: 1, timeout_interval: 1){ sleep }
|
122
|
-
task.add_observer(TaskObserver.new)
|
123
|
-
task.run!
|
124
|
-
|
125
|
-
#=> (2013-10-13 19:07:25 -0400) Execution timed out
|
126
|
-
#=> (2013-10-13 19:07:27 -0400) Execution timed out
|
127
|
-
#=> (2013-10-13 19:07:29 -0400) Execution timed out
|
128
|
-
task.stop
|
129
|
-
|
130
|
-
task = Concurrent::TimerTask.new(execution_interval: 1){ raise StandardError }
|
131
|
-
task.add_observer(TaskObserver.new)
|
132
|
-
task.run!
|
133
|
-
|
134
|
-
#=> (2013-10-13 19:09:37 -0400) Execution failed with error StandardError
|
135
|
-
#=> (2013-10-13 19:09:38 -0400) Execution failed with error StandardError
|
136
|
-
#=> (2013-10-13 19:09:39 -0400) Execution failed with error StandardError
|
137
|
-
task.stop
|
138
|
-
```
|
139
|
-
|
140
|
-
In some cases it may be necessary for a `TimerTask` to affect its own execution cycle.
|
141
|
-
To facilitate this a reference to the task object is passed into the block as a block
|
142
|
-
argument every time the task is executed.
|
143
|
-
|
144
|
-
```ruby
|
145
|
-
timer_task = Concurrent::TimerTask.new(execution_interval: 1) do |task|
|
146
|
-
task.execution_interval.times{ print 'Boom! ' }
|
147
|
-
print "\n"
|
148
|
-
task.execution_interval += 1
|
149
|
-
if task.execution_interval > 5
|
150
|
-
puts 'Stopping...'
|
151
|
-
task.stop
|
152
|
-
end
|
153
|
-
end
|
154
|
-
|
155
|
-
timer_task.run # blocking call - this task will stop itself
|
156
|
-
#=> Boom!
|
157
|
-
#=> Boom! Boom!
|
158
|
-
#=> Boom! Boom! Boom!
|
159
|
-
#=> Boom! Boom! Boom! Boom!
|
160
|
-
#=> Boom! Boom! Boom! Boom! Boom!
|
161
|
-
#=> Stopping...
|
162
|
-
```
|
163
|
-
|
164
|
-
## Copyright
|
165
|
-
|
166
|
-
*Concurrent Ruby* is Copyright © 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
|
167
|
-
It is free software and may be redistributed under the terms specified in the LICENSE file.
|
168
|
-
|
169
|
-
## License
|
170
|
-
|
171
|
-
Released under the MIT license.
|
172
|
-
|
173
|
-
http://www.opensource.org/licenses/mit-license.php
|
174
|
-
|
175
|
-
> Permission is hereby granted, free of charge, to any person obtaining a copy
|
176
|
-
> of this software and associated documentation files (the "Software"), to deal
|
177
|
-
> in the Software without restriction, including without limitation the rights
|
178
|
-
> to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
179
|
-
> copies of the Software, and to permit persons to whom the Software is
|
180
|
-
> furnished to do so, subject to the following conditions:
|
181
|
-
>
|
182
|
-
> The above copyright notice and this permission notice shall be included in
|
183
|
-
> all copies or substantial portions of the Software.
|
184
|
-
>
|
185
|
-
> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
186
|
-
> IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
187
|
-
> FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
188
|
-
> AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
189
|
-
> LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
190
|
-
> OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
191
|
-
> THE SOFTWARE.
|
@@ -1,34 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
require_relative 'obligation_shared'
|
3
|
-
|
4
|
-
module Concurrent
|
5
|
-
|
6
|
-
describe Contract do
|
7
|
-
|
8
|
-
let!(:fulfilled_value) { 10 }
|
9
|
-
let(:rejected_reason) { StandardError.new('Boom!') }
|
10
|
-
|
11
|
-
let(:pending_subject) do
|
12
|
-
@contract = Contract.new
|
13
|
-
Thread.new do
|
14
|
-
sleep(3)
|
15
|
-
@contract.complete(fulfilled_value, nil)
|
16
|
-
end
|
17
|
-
@contract
|
18
|
-
end
|
19
|
-
|
20
|
-
let(:fulfilled_subject) do
|
21
|
-
contract = Contract.new
|
22
|
-
contract.complete(fulfilled_value, nil)
|
23
|
-
contract
|
24
|
-
end
|
25
|
-
|
26
|
-
let(:rejected_subject) do
|
27
|
-
contract = Contract.new
|
28
|
-
contract.complete(nil, rejected_reason)
|
29
|
-
contract
|
30
|
-
end
|
31
|
-
|
32
|
-
it_should_behave_like :obligation
|
33
|
-
end
|
34
|
-
end
|
@@ -1,240 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
require 'concurrent/agent'
|
4
|
-
require 'concurrent/future'
|
5
|
-
require 'concurrent/promise'
|
6
|
-
|
7
|
-
if mri?
|
8
|
-
|
9
|
-
module Concurrent
|
10
|
-
|
11
|
-
describe EventMachineDeferProxy do
|
12
|
-
|
13
|
-
subject { EventMachineDeferProxy.new }
|
14
|
-
|
15
|
-
after(:all) do
|
16
|
-
$GLOBAL_THREAD_POOL = FixedThreadPool.new(1)
|
17
|
-
end
|
18
|
-
|
19
|
-
context '#post' do
|
20
|
-
|
21
|
-
it 'proxies a call without arguments' do
|
22
|
-
@expected = false
|
23
|
-
EventMachine.run do
|
24
|
-
subject.post{ @expected = true }
|
25
|
-
sleep(0.1)
|
26
|
-
EventMachine.stop
|
27
|
-
end
|
28
|
-
@expected.should eq true
|
29
|
-
end
|
30
|
-
|
31
|
-
it 'proxies a call with arguments' do
|
32
|
-
@expected = []
|
33
|
-
EventMachine.run do
|
34
|
-
subject.post(1,2,3){|*args| @expected = args }
|
35
|
-
sleep(0.1)
|
36
|
-
EventMachine.stop
|
37
|
-
end
|
38
|
-
@expected.should eq [1,2,3]
|
39
|
-
end
|
40
|
-
|
41
|
-
it 'aliases #<<' do
|
42
|
-
@expected = false
|
43
|
-
EventMachine.run do
|
44
|
-
subject << proc{ @expected = true }
|
45
|
-
sleep(0.1)
|
46
|
-
EventMachine.stop
|
47
|
-
end
|
48
|
-
@expected.should eq true
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
context 'operation' do
|
53
|
-
|
54
|
-
context Agent do
|
55
|
-
|
56
|
-
subject { Agent.new(0) }
|
57
|
-
|
58
|
-
before(:each) do
|
59
|
-
Agent.thread_pool = EventMachineDeferProxy.new
|
60
|
-
end
|
61
|
-
|
62
|
-
it 'supports fulfillment' do
|
63
|
-
|
64
|
-
EventMachine.run do
|
65
|
-
|
66
|
-
@expected = []
|
67
|
-
subject.post{ @expected << 1 }
|
68
|
-
subject.post{ @expected << 2 }
|
69
|
-
subject.post{ @expected << 3 }
|
70
|
-
sleep(0.1)
|
71
|
-
@expected.should include(1)
|
72
|
-
@expected.should include(2)
|
73
|
-
@expected.should include(3)
|
74
|
-
|
75
|
-
EventMachine.stop
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
it 'supports validation' do
|
80
|
-
|
81
|
-
EventMachine.run do
|
82
|
-
|
83
|
-
@expected = nil
|
84
|
-
subject.validate{ @expected = 10; true }
|
85
|
-
subject.post{ nil }
|
86
|
-
sleep(0.1)
|
87
|
-
@expected.should eq 10
|
88
|
-
|
89
|
-
EventMachine.stop
|
90
|
-
end
|
91
|
-
end
|
92
|
-
|
93
|
-
it 'supports rejection' do
|
94
|
-
|
95
|
-
EventMachine.run do
|
96
|
-
|
97
|
-
@expected = nil
|
98
|
-
subject.
|
99
|
-
on_error(StandardError){|ex| @expected = 1 }.
|
100
|
-
on_error(StandardError){|ex| @expected = 2 }.
|
101
|
-
on_error(StandardError){|ex| @expected = 3 }
|
102
|
-
subject.post{ raise StandardError }
|
103
|
-
sleep(0.1)
|
104
|
-
@expected.should eq 1
|
105
|
-
|
106
|
-
EventMachine.stop
|
107
|
-
end
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
context Future do
|
112
|
-
|
113
|
-
before(:each) do
|
114
|
-
Future.thread_pool = EventMachineDeferProxy.new
|
115
|
-
end
|
116
|
-
|
117
|
-
it 'supports fulfillment' do
|
118
|
-
|
119
|
-
EventMachine.run do
|
120
|
-
|
121
|
-
@a = @b = @c = nil
|
122
|
-
f = Future.new(1, 2, 3) do |a, b, c|
|
123
|
-
@a, @b, @c = a, b, c
|
124
|
-
end
|
125
|
-
sleep(0.1)
|
126
|
-
[@a, @b, @c].should eq [1, 2, 3]
|
127
|
-
|
128
|
-
sleep(0.1)
|
129
|
-
EventMachine.stop
|
130
|
-
end
|
131
|
-
end
|
132
|
-
end
|
133
|
-
|
134
|
-
context Promise do
|
135
|
-
|
136
|
-
before(:each) do
|
137
|
-
Promise.thread_pool = EventMachineDeferProxy.new
|
138
|
-
end
|
139
|
-
|
140
|
-
context 'fulfillment' do
|
141
|
-
|
142
|
-
it 'passes all arguments to the first promise in the chain' do
|
143
|
-
|
144
|
-
EventMachine.run do
|
145
|
-
|
146
|
-
@a = @b = @c = nil
|
147
|
-
p = Promise.new(1, 2, 3) do |a, b, c|
|
148
|
-
@a, @b, @c = a, b, c
|
149
|
-
end
|
150
|
-
sleep(0.1)
|
151
|
-
[@a, @b, @c].should eq [1, 2, 3]
|
152
|
-
|
153
|
-
sleep(0.1)
|
154
|
-
EventMachine.stop
|
155
|
-
end
|
156
|
-
end
|
157
|
-
|
158
|
-
it 'passes the result of each block to all its children' do
|
159
|
-
|
160
|
-
EventMachine.run do
|
161
|
-
@expected = nil
|
162
|
-
Promise.new(10){|a| a * 2 }.then{|result| @expected = result}
|
163
|
-
sleep(0.1)
|
164
|
-
@expected.should eq 20
|
165
|
-
|
166
|
-
sleep(0.1)
|
167
|
-
EventMachine.stop
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
it 'sets the promise value to the result if its block' do
|
172
|
-
|
173
|
-
EventMachine.run do
|
174
|
-
|
175
|
-
p = Promise.new(10){|a| a * 2 }.then{|result| result * 2}
|
176
|
-
sleep(0.1)
|
177
|
-
p.value.should eq 40
|
178
|
-
|
179
|
-
sleep(0.1)
|
180
|
-
EventMachine.stop
|
181
|
-
end
|
182
|
-
end
|
183
|
-
end
|
184
|
-
|
185
|
-
context 'rejection' do
|
186
|
-
|
187
|
-
it 'sets the promise reason and error on exception' do
|
188
|
-
|
189
|
-
EventMachine.run do
|
190
|
-
|
191
|
-
p = Promise.new{ raise StandardError.new('Boom!') }
|
192
|
-
sleep(0.1)
|
193
|
-
p.reason.should be_a(Exception)
|
194
|
-
p.reason.should.to_s =~ /Boom!/
|
195
|
-
p.should be_rejected
|
196
|
-
|
197
|
-
sleep(0.1)
|
198
|
-
EventMachine.stop
|
199
|
-
end
|
200
|
-
end
|
201
|
-
|
202
|
-
it 'calls the first exception block with a matching class' do
|
203
|
-
|
204
|
-
EventMachine.run do
|
205
|
-
|
206
|
-
@expected = nil
|
207
|
-
Promise.new{ raise StandardError }.
|
208
|
-
on_error(StandardError){|ex| @expected = 1 }.
|
209
|
-
on_error(StandardError){|ex| @expected = 2 }.
|
210
|
-
on_error(StandardError){|ex| @expected = 3 }
|
211
|
-
sleep(0.1)
|
212
|
-
@expected.should eq 1
|
213
|
-
|
214
|
-
sleep(0.1)
|
215
|
-
EventMachine.stop
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
it 'passes the exception object to the matched block' do
|
220
|
-
|
221
|
-
EventMachine.run do
|
222
|
-
|
223
|
-
@expected = nil
|
224
|
-
Promise.new{ raise StandardError }.
|
225
|
-
on_error(ArgumentError){|ex| @expected = ex }.
|
226
|
-
on_error(LoadError){|ex| @expected = ex }.
|
227
|
-
on_error(Exception){|ex| @expected = ex }
|
228
|
-
sleep(0.1)
|
229
|
-
@expected.should be_a(StandardError)
|
230
|
-
|
231
|
-
sleep(0.1)
|
232
|
-
EventMachine.stop
|
233
|
-
end
|
234
|
-
end
|
235
|
-
end
|
236
|
-
end
|
237
|
-
end
|
238
|
-
end
|
239
|
-
end
|
240
|
-
end
|