concurrent-ruby 0.1.1.pre.3 → 0.1.1.pre.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -21
  3. data/README.md +275 -279
  4. data/lib/concurrent.rb +27 -28
  5. data/lib/concurrent/agent.rb +114 -108
  6. data/lib/concurrent/cached_thread_pool.rb +129 -130
  7. data/lib/concurrent/defer.rb +65 -67
  8. data/lib/concurrent/event.rb +60 -60
  9. data/lib/concurrent/event_machine_defer_proxy.rb +23 -23
  10. data/lib/concurrent/executor.rb +93 -95
  11. data/lib/concurrent/fixed_thread_pool.rb +95 -89
  12. data/lib/concurrent/functions.rb +120 -120
  13. data/lib/concurrent/future.rb +42 -47
  14. data/lib/concurrent/global_thread_pool.rb +16 -16
  15. data/lib/concurrent/goroutine.rb +29 -29
  16. data/lib/concurrent/null_thread_pool.rb +22 -22
  17. data/lib/concurrent/obligation.rb +67 -67
  18. data/lib/concurrent/promise.rb +174 -166
  19. data/lib/concurrent/reactor.rb +161 -162
  20. data/lib/concurrent/reactor/drb_async_demux.rb +74 -74
  21. data/lib/concurrent/reactor/tcp_sync_demux.rb +98 -98
  22. data/lib/concurrent/thread_pool.rb +76 -69
  23. data/lib/concurrent/utilities.rb +32 -34
  24. data/lib/concurrent/version.rb +3 -3
  25. data/lib/concurrent_ruby.rb +1 -1
  26. data/md/agent.md +123 -123
  27. data/md/defer.md +174 -174
  28. data/md/event.md +32 -32
  29. data/md/executor.md +176 -176
  30. data/md/future.md +83 -83
  31. data/md/goroutine.md +52 -52
  32. data/md/obligation.md +32 -32
  33. data/md/promise.md +227 -227
  34. data/md/thread_pool.md +224 -224
  35. data/spec/concurrent/agent_spec.rb +386 -380
  36. data/spec/concurrent/cached_thread_pool_spec.rb +125 -125
  37. data/spec/concurrent/defer_spec.rb +195 -195
  38. data/spec/concurrent/event_machine_defer_proxy_spec.rb +256 -253
  39. data/spec/concurrent/event_spec.rb +134 -134
  40. data/spec/concurrent/executor_spec.rb +184 -184
  41. data/spec/concurrent/fixed_thread_pool_spec.rb +83 -84
  42. data/spec/concurrent/functions_spec.rb +217 -217
  43. data/spec/concurrent/future_spec.rb +108 -108
  44. data/spec/concurrent/global_thread_pool_spec.rb +38 -38
  45. data/spec/concurrent/goroutine_spec.rb +67 -67
  46. data/spec/concurrent/null_thread_pool_spec.rb +54 -54
  47. data/spec/concurrent/obligation_shared.rb +135 -121
  48. data/spec/concurrent/promise_spec.rb +312 -305
  49. data/spec/concurrent/reactor/drb_async_demux_spec.rb +12 -12
  50. data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +12 -12
  51. data/spec/concurrent/reactor_spec.rb +351 -10
  52. data/spec/concurrent/thread_pool_shared.rb +209 -210
  53. data/spec/concurrent/utilities_spec.rb +74 -74
  54. data/spec/spec_helper.rb +44 -30
  55. metadata +11 -22
  56. data/lib/concurrent/smart_mutex.rb +0 -66
  57. data/spec/concurrent/smart_mutex_spec.rb +0 -234
data/md/thread_pool.md CHANGED
@@ -1,224 +1,224 @@
1
- # We're Going to Need a Bigger Boat
2
-
3
- Thread pools are neither a new idea nor an implementation of the actor pattern. Nevertheless, thread
4
- pools are still an extremely relevant concurrency tool. Every time a thread is created then
5
- subsequently destroyed there is overhead. Creating a pool of reusable worker threads then repeatedly'
6
- dipping into the pool can have huge performace benefits for a long-running application like a service.
7
- Ruby's blocks provide an excellent mechanism for passing a generic work request to a thread, making
8
- Ruby an excellent candidate language for thread pools.
9
-
10
- The inspiration for thread pools in this library is Java's `java.util.concurrent` implementation of
11
- [thread pools](java.util.concurrent). The `java.util.concurrent` library is a well-designed, stable,
12
- scalable, and battle-tested concurrency library. It provides three different implementations of thread
13
- pools. One of those implementations is simply a special case of the first and doesn't offer much
14
- advantage in Ruby, so only the first two (`FixedThreadPool` and `CachedThreadPool`) are implemented here.
15
-
16
- Thread pools share common `behavior` defined by `:thread_pool`. The most imortant method is `post`
17
- (aliased with the left-shift operator `<<`). The `post` method sends a block to the pool for future
18
- processing.
19
-
20
- A running thread pool can be shutdown in an orderly or disruptive manner. Once a thread pool has been
21
- shutdown in cannot be started again. The `shutdown` method can be used to initiate an orderly shutdown
22
- of the thread pool. All new `post` calls will reject the given block and immediately return `false`.
23
- Threads in the pool will continue to process all in-progress work and will process all tasks still in
24
- the queue. The `kill` method can be used to immediately shutdown the pool. All new `post` calls will
25
- reject the given block and immediately return `false`. Ruby's `Thread.kill` will be called on all threads
26
- in the pool, aborting all in-progress work. Tasks in the queue will be discarded.
27
-
28
- A client thread can choose to block and wait for pool shutdown to complete. This is useful when shutting
29
- down an application and ensuring the app doesn't exit before pool processing is complete. The method
30
- `wait_for_termination` will block for a maximum of the given number of seconds then return `true` if
31
- shutdown completed successfully or `false`. When the timeout value is `nil` the call will block
32
- indefinitely. Calling `wait_for_termination` on a stopped thread pool will immediately return `true`.
33
-
34
- Predicate methods are provided to describe the current state of the thread pool. Provided methods are
35
- `running?`, `shutdown?`, and `killed?`. The `shutdown` method will return true regardless of whether
36
- the pool was shutdown wil `shutdown` or `kill`.
37
-
38
- ## FixedThreadPool
39
-
40
- From the docs:
41
-
42
- > Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue.
43
- > At any point, at most `nThreads` threads will be active processing tasks. If additional tasks are submitted
44
- > when all threads are active, they will wait in the queue until a thread is available. If any thread terminates
45
- > due to a failure during execution prior to shutdown, a new one will take its place if needed to execute
46
- > subsequent tasks. The threads in the pool will exist until it is explicitly `shutdown`.
47
-
48
- ### Examples
49
-
50
- ```ruby
51
- require 'concurrent'
52
-
53
- pool = Concurrent::FixedThreadPool.new(5)
54
-
55
- pool.size #=> 5
56
- pool.running? #=> true
57
- pool.status #=> ["sleep", "sleep", "sleep", "sleep", "sleep"]
58
-
59
- pool.post(1,2,3){|*args| sleep(10) }
60
- pool << proc{ sleep(10) }
61
- pool.size #=> 5
62
-
63
- sleep(11)
64
- pool.status #=> ["sleep", "sleep", "sleep", "sleep", "sleep"]
65
-
66
- pool.shutdown #=> :shuttingdown
67
- pool.status #=> []
68
- pool.wait_for_termination
69
-
70
- pool.size #=> 0
71
- pool.status #=> []
72
- pool.shutdown? #=> true
73
- ```
74
-
75
- ## CachedThreadPool
76
-
77
- From the docs:
78
-
79
- > Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when
80
- > they are available. These pools will typically improve the performance of programs that execute many short-lived
81
- > asynchronous tasks. Calls to [`post`] will reuse previously constructed threads if available. If no existing
82
- > thread is available, a new thread will be created and added to the pool. Threads that have not been used for
83
- > sixty seconds are terminated and removed from the cache. Thus, a pool that remains idle for long enough will
84
- > not consume any resources. Note that pools with similar properties but different details (for example,
85
- > timeout parameters) may be created using [`CachedThreadPool`] constructors.
86
-
87
- ### Examples
88
-
89
- ```ruby
90
- require 'functional/cached_thread_pool'
91
- # or
92
- require 'functional/concurrency'
93
-
94
- pool = Concurrent::CachedThreadPool.new
95
-
96
- pool.size #=> 0
97
- pool.running? #=> true
98
- pool.status #=> []
99
-
100
- pool.post(1,2,3){|*args| sleep(10) }
101
- pool << proc{ sleep(10) }
102
- pool.size #=> 2
103
- pool.status #=> [[:working, nil, "sleep"], [:working, nil, "sleep"]]
104
-
105
- sleep(11)
106
- pool.status #=> [[:idle, 23, "sleep"], [:idle, 23, "sleep"]]
107
-
108
- sleep(60)
109
- pool.size #=> 0
110
- pool.status #=> []
111
-
112
- pool.shutdown #=> :shuttingdown
113
- pool.status #=> []
114
- pool.wait_for_termination
115
-
116
- pool.size #=> 0
117
- pool.status #=> []
118
- pool.shutdown? #=> true
119
- ```
120
-
121
- ## Global Thread Pool
122
-
123
- For efficiency, of the aforementioned concurrency methods (agents, futures, promises, and
124
- goroutines) run against a global thread pool. This pool can be directly accessed through the
125
- `$GLOBAL_THREAD_POOL` global variable. Generally, this pool should not be directly accessed.
126
- Use the other concurrency features instead.
127
-
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.
130
-
131
- ### Changing the Global Thread Pool
132
-
133
- It is possible to change the global thread pool. Simply assign a new pool to the `$GLOBAL_THREAD_POOL`
134
- variable:
135
-
136
- ```ruby
137
- $GLOBAL_THREAD_POOL = Concurrent::FixedThreadPool.new(10)
138
- ```
139
-
140
- Ideally this should be done at application startup, before any concurrency functions are called.
141
- If the circumstances warrant the global thread pool can be changed at runtime. Just make sure to
142
- shutdown the old global thread pool so that no tasks are lost:
143
-
144
- ```ruby
145
- $GLOBAL_THREAD_POOL = Concurrent::FixedThreadPool.new(10)
146
-
147
- # do stuff...
148
-
149
- old_global_pool = $GLOBAL_THREAD_POOL
150
- $GLOBAL_THREAD_POOL = Concurrent::FixedThreadPool.new(10)
151
- old_global_pool.shutdown
152
- ```
153
-
154
- ### NullThreadPool
155
-
156
- If for some reason an appliction would be better served by *not* having a global thread pool, the
157
- `NullThreadPool` is provided. The `NullThreadPool` is compatible with the global thread pool but
158
- it is not an actual thread pool. Instead it spawns a new thread on every call to the `post` method.
159
-
160
- ### EventMachine
161
-
162
- The [EventMachine](http://rubyeventmachine.com/) library (source [online](https://github.com/eventmachine/eventmachine))
163
- is an awesome library for creating evented applications. EventMachine provides its own thread pool
164
- 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
167
- of `EventMachineDeferProxy`:
168
-
169
- ```ruby
170
- require 'eventmachine' # do this FIRST
171
- require 'functional/concurrency'
172
-
173
- $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
174
- ```
175
-
176
- ## Per-class Thread Pools
177
-
178
- Many of the classes in this library use the global thread pool rather than creating new threads.
179
- Classes such as `Agent`, `Defer`, and others follow this pattern. There may be cases where a
180
- program would be better suited for one or more of these classes used a different thread pool.
181
- All classes that use the global thread pool support a class-level `thread_pool` attribute accessor.
182
- This property defaults to the global thread pool but can be changed at any time. Once changed, all
183
- new instances of that class will use the new thread pool.
184
-
185
- ```ruby
186
- Concurrent::Agent.thread_pool == $GLOBAL_THREAD_POOL #=> true
187
-
188
- $GLOBAL_THREAD_POOL = Concurrent::FixedThreadPool.new(10) #=> #<Concurrent::FixedThreadPool:0x007fe31130f1f0 ...
189
-
190
- Concurrent::Agent.thread_pool == $GLOBAL_THREAD_POOL #=> false
191
-
192
- Concurrent::Defer.thread_pool = Concurrent::CachedThreadPool.new #=> #<Concurrent::CachedThreadPool:0x007fef1c6b6b48 ...
193
- Concurrent::Defer.thread_pool == Concurrent::Agent.thread_pool #=> false
194
- Concurrent::Defer.thread_pool == $GLOBAL_THREAD_POOL #=> false
195
- ```
196
-
197
- ## Copyright
198
-
199
- *Concurrent Ruby* is Copyright &copy; 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
200
- It is free software and may be redistributed under the terms specified in the LICENSE file.
201
-
202
- ## License
203
-
204
- Released under the MIT license.
205
-
206
- http://www.opensource.org/licenses/mit-license.php
207
-
208
- > Permission is hereby granted, free of charge, to any person obtaining a copy
209
- > of this software and associated documentation files (the "Software"), to deal
210
- > in the Software without restriction, including without limitation the rights
211
- > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
212
- > copies of the Software, and to permit persons to whom the Software is
213
- > furnished to do so, subject to the following conditions:
214
- >
215
- > The above copyright notice and this permission notice shall be included in
216
- > all copies or substantial portions of the Software.
217
- >
218
- > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
219
- > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
220
- > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
221
- > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
222
- > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
223
- > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
224
- > THE SOFTWARE.
1
+ # We're Going to Need a Bigger Boat
2
+
3
+ Thread pools are neither a new idea nor an implementation of the actor pattern. Nevertheless, thread
4
+ pools are still an extremely relevant concurrency tool. Every time a thread is created then
5
+ subsequently destroyed there is overhead. Creating a pool of reusable worker threads then repeatedly'
6
+ dipping into the pool can have huge performace benefits for a long-running application like a service.
7
+ Ruby's blocks provide an excellent mechanism for passing a generic work request to a thread, making
8
+ Ruby an excellent candidate language for thread pools.
9
+
10
+ The inspiration for thread pools in this library is Java's `java.util.concurrent` implementation of
11
+ [thread pools](java.util.concurrent). The `java.util.concurrent` library is a well-designed, stable,
12
+ scalable, and battle-tested concurrency library. It provides three different implementations of thread
13
+ pools. One of those implementations is simply a special case of the first and doesn't offer much
14
+ advantage in Ruby, so only the first two (`FixedThreadPool` and `CachedThreadPool`) are implemented here.
15
+
16
+ Thread pools share common `behavior` defined by `:thread_pool`. The most imortant method is `post`
17
+ (aliased with the left-shift operator `<<`). The `post` method sends a block to the pool for future
18
+ processing.
19
+
20
+ A running thread pool can be shutdown in an orderly or disruptive manner. Once a thread pool has been
21
+ shutdown in cannot be started again. The `shutdown` method can be used to initiate an orderly shutdown
22
+ of the thread pool. All new `post` calls will reject the given block and immediately return `false`.
23
+ Threads in the pool will continue to process all in-progress work and will process all tasks still in
24
+ the queue. The `kill` method can be used to immediately shutdown the pool. All new `post` calls will
25
+ reject the given block and immediately return `false`. Ruby's `Thread.kill` will be called on all threads
26
+ in the pool, aborting all in-progress work. Tasks in the queue will be discarded.
27
+
28
+ A client thread can choose to block and wait for pool shutdown to complete. This is useful when shutting
29
+ down an application and ensuring the app doesn't exit before pool processing is complete. The method
30
+ `wait_for_termination` will block for a maximum of the given number of seconds then return `true` if
31
+ shutdown completed successfully or `false`. When the timeout value is `nil` the call will block
32
+ indefinitely. Calling `wait_for_termination` on a stopped thread pool will immediately return `true`.
33
+
34
+ Predicate methods are provided to describe the current state of the thread pool. Provided methods are
35
+ `running?`, `shutdown?`, and `killed?`. The `shutdown` method will return true regardless of whether
36
+ the pool was shutdown wil `shutdown` or `kill`.
37
+
38
+ ## FixedThreadPool
39
+
40
+ From the docs:
41
+
42
+ > Creates a thread pool that reuses a fixed number of threads operating off a shared unbounded queue.
43
+ > At any point, at most `nThreads` threads will be active processing tasks. If additional tasks are submitted
44
+ > when all threads are active, they will wait in the queue until a thread is available. If any thread terminates
45
+ > due to a failure during execution prior to shutdown, a new one will take its place if needed to execute
46
+ > subsequent tasks. The threads in the pool will exist until it is explicitly `shutdown`.
47
+
48
+ ### Examples
49
+
50
+ ```ruby
51
+ require 'concurrent'
52
+
53
+ pool = Concurrent::FixedThreadPool.new(5)
54
+
55
+ pool.size #=> 5
56
+ pool.running? #=> true
57
+ pool.status #=> ["sleep", "sleep", "sleep", "sleep", "sleep"]
58
+
59
+ pool.post(1,2,3){|*args| sleep(10) }
60
+ pool << proc{ sleep(10) }
61
+ pool.size #=> 5
62
+
63
+ sleep(11)
64
+ pool.status #=> ["sleep", "sleep", "sleep", "sleep", "sleep"]
65
+
66
+ pool.shutdown #=> :shuttingdown
67
+ pool.status #=> []
68
+ pool.wait_for_termination
69
+
70
+ pool.size #=> 0
71
+ pool.status #=> []
72
+ pool.shutdown? #=> true
73
+ ```
74
+
75
+ ## CachedThreadPool
76
+
77
+ From the docs:
78
+
79
+ > Creates a thread pool that creates new threads as needed, but will reuse previously constructed threads when
80
+ > they are available. These pools will typically improve the performance of programs that execute many short-lived
81
+ > asynchronous tasks. Calls to [`post`] will reuse previously constructed threads if available. If no existing
82
+ > thread is available, a new thread will be created and added to the pool. Threads that have not been used for
83
+ > sixty seconds are terminated and removed from the cache. Thus, a pool that remains idle for long enough will
84
+ > not consume any resources. Note that pools with similar properties but different details (for example,
85
+ > timeout parameters) may be created using [`CachedThreadPool`] constructors.
86
+
87
+ ### Examples
88
+
89
+ ```ruby
90
+ require 'functional/cached_thread_pool'
91
+ # or
92
+ require 'functional/concurrency'
93
+
94
+ pool = Concurrent::CachedThreadPool.new
95
+
96
+ pool.size #=> 0
97
+ pool.running? #=> true
98
+ pool.status #=> []
99
+
100
+ pool.post(1,2,3){|*args| sleep(10) }
101
+ pool << proc{ sleep(10) }
102
+ pool.size #=> 2
103
+ pool.status #=> [[:working, nil, "sleep"], [:working, nil, "sleep"]]
104
+
105
+ sleep(11)
106
+ pool.status #=> [[:idle, 23, "sleep"], [:idle, 23, "sleep"]]
107
+
108
+ sleep(60)
109
+ pool.size #=> 0
110
+ pool.status #=> []
111
+
112
+ pool.shutdown #=> :shuttingdown
113
+ pool.status #=> []
114
+ pool.wait_for_termination
115
+
116
+ pool.size #=> 0
117
+ pool.status #=> []
118
+ pool.shutdown? #=> true
119
+ ```
120
+
121
+ ## Global Thread Pool
122
+
123
+ For efficiency, of the aforementioned concurrency methods (agents, futures, promises, and
124
+ goroutines) run against a global thread pool. This pool can be directly accessed through the
125
+ `$GLOBAL_THREAD_POOL` global variable. Generally, this pool should not be directly accessed.
126
+ Use the other concurrency features instead.
127
+
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.
130
+
131
+ ### Changing the Global Thread Pool
132
+
133
+ It is possible to change the global thread pool. Simply assign a new pool to the `$GLOBAL_THREAD_POOL`
134
+ variable:
135
+
136
+ ```ruby
137
+ $GLOBAL_THREAD_POOL = Concurrent::FixedThreadPool.new(10)
138
+ ```
139
+
140
+ Ideally this should be done at application startup, before any concurrency functions are called.
141
+ If the circumstances warrant the global thread pool can be changed at runtime. Just make sure to
142
+ shutdown the old global thread pool so that no tasks are lost:
143
+
144
+ ```ruby
145
+ $GLOBAL_THREAD_POOL = Concurrent::FixedThreadPool.new(10)
146
+
147
+ # do stuff...
148
+
149
+ old_global_pool = $GLOBAL_THREAD_POOL
150
+ $GLOBAL_THREAD_POOL = Concurrent::FixedThreadPool.new(10)
151
+ old_global_pool.shutdown
152
+ ```
153
+
154
+ ### NullThreadPool
155
+
156
+ If for some reason an appliction would be better served by *not* having a global thread pool, the
157
+ `NullThreadPool` is provided. The `NullThreadPool` is compatible with the global thread pool but
158
+ it is not an actual thread pool. Instead it spawns a new thread on every call to the `post` method.
159
+
160
+ ### EventMachine
161
+
162
+ The [EventMachine](http://rubyeventmachine.com/) library (source [online](https://github.com/eventmachine/eventmachine))
163
+ is an awesome library for creating evented applications. EventMachine provides its own thread pool
164
+ 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
167
+ of `EventMachineDeferProxy`:
168
+
169
+ ```ruby
170
+ require 'eventmachine' # do this FIRST
171
+ require 'functional/concurrency'
172
+
173
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
174
+ ```
175
+
176
+ ## Per-class Thread Pools
177
+
178
+ Many of the classes in this library use the global thread pool rather than creating new threads.
179
+ Classes such as `Agent`, `Defer`, and others follow this pattern. There may be cases where a
180
+ program would be better suited for one or more of these classes used a different thread pool.
181
+ All classes that use the global thread pool support a class-level `thread_pool` attribute accessor.
182
+ This property defaults to the global thread pool but can be changed at any time. Once changed, all
183
+ new instances of that class will use the new thread pool.
184
+
185
+ ```ruby
186
+ Concurrent::Agent.thread_pool == $GLOBAL_THREAD_POOL #=> true
187
+
188
+ $GLOBAL_THREAD_POOL = Concurrent::FixedThreadPool.new(10) #=> #<Concurrent::FixedThreadPool:0x007fe31130f1f0 ...
189
+
190
+ Concurrent::Agent.thread_pool == $GLOBAL_THREAD_POOL #=> false
191
+
192
+ Concurrent::Defer.thread_pool = Concurrent::CachedThreadPool.new #=> #<Concurrent::CachedThreadPool:0x007fef1c6b6b48 ...
193
+ Concurrent::Defer.thread_pool == Concurrent::Agent.thread_pool #=> false
194
+ Concurrent::Defer.thread_pool == $GLOBAL_THREAD_POOL #=> false
195
+ ```
196
+
197
+ ## Copyright
198
+
199
+ *Concurrent Ruby* is Copyright &copy; 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
200
+ It is free software and may be redistributed under the terms specified in the LICENSE file.
201
+
202
+ ## License
203
+
204
+ Released under the MIT license.
205
+
206
+ http://www.opensource.org/licenses/mit-license.php
207
+
208
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
209
+ > of this software and associated documentation files (the "Software"), to deal
210
+ > in the Software without restriction, including without limitation the rights
211
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
212
+ > copies of the Software, and to permit persons to whom the Software is
213
+ > furnished to do so, subject to the following conditions:
214
+ >
215
+ > The above copyright notice and this permission notice shall be included in
216
+ > all copies or substantial portions of the Software.
217
+ >
218
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
219
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
220
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
221
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
222
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
223
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
224
+ > THE SOFTWARE.
@@ -1,380 +1,386 @@
1
- require 'spec_helper'
2
-
3
- module Concurrent
4
-
5
- describe Agent do
6
-
7
- subject { Agent.new(0) }
8
-
9
- let(:observer) do
10
- Class.new do
11
- attr_reader :value
12
- define_method(:update) do |time, value|
13
- @value = value
14
- end
15
- end.new
16
- end
17
-
18
- before(:each) do
19
- Agent.thread_pool = FixedThreadPool.new(1)
20
- end
21
-
22
- context '#initialize' do
23
-
24
- it 'sets the value to the given initial state' do
25
- Agent.new(10).value.should eq 10
26
- end
27
-
28
- it 'sets the timeout to the given value' do
29
- Agent.new(0, 5).timeout.should eq 5
30
- end
31
-
32
- it 'sets the timeout to the default when nil' do
33
- Agent.new(0).timeout.should eq Agent::TIMEOUT
34
- end
35
-
36
- it 'sets the length to zero' do
37
- Agent.new(10).length.should eq 0
38
- end
39
-
40
- it 'spawns the worker thread' do
41
- Agent.thread_pool.should_receive(:post).once.with(any_args())
42
- Agent.new(0)
43
- end
44
- end
45
-
46
- context '#rescue' do
47
-
48
- it 'returns self when a block is given' do
49
- a1 = subject
50
- a2 = a1.rescue{}
51
- a1.object_id.should eq a2.object_id
52
- end
53
-
54
- it 'returns self when no block is given' do
55
- a1 = subject
56
- a2 = a1.rescue
57
- a1.object_id.should eq a2.object_id
58
- end
59
-
60
- it 'accepts an exception class as the first parameter' do
61
- lambda {
62
- subject.rescue(StandardError){}
63
- }.should_not raise_error
64
- end
65
-
66
- it 'ignores rescuers without a block' do
67
- subject.rescue
68
- subject.instance_variable_get(:@rescuers).should be_empty
69
- end
70
- end
71
-
72
- context '#validate' do
73
-
74
- it 'returns self when a block is given' do
75
- a1 = subject
76
- a2 = a1.validate{}
77
- a1.object_id.should eq a2.object_id
78
- end
79
-
80
- it 'returns self when no block is given' do
81
- a1 = subject
82
- a2 = a1.validate
83
- a1.object_id.should eq a2.object_id
84
- end
85
-
86
- it 'ignores validators without a block' do
87
- subject.validate
88
- subject.instance_variable_get(:@validator).should be_nil
89
- end
90
- end
91
-
92
- context '#post' do
93
-
94
- it 'adds the given block to the queue' do
95
- before = subject.length
96
- subject.post{ nil }
97
- subject.post{ nil }
98
- subject.length.should eq before+2
99
- end
100
-
101
- it 'does not add to the queue when no block is given' do
102
- before = subject.length
103
- subject.post
104
- subject.post{ nil }
105
- subject.length.should eq before+1
106
- end
107
- end
108
-
109
- context '#length' do
110
-
111
- it 'should be zero for a new agent' do
112
- subject.length.should eq 0
113
- end
114
-
115
- it 'should increase by one for each #post' do
116
- subject.post{ sleep }
117
- subject.post{ sleep }
118
- subject.post{ sleep }
119
- subject.length.should eq 3
120
- end
121
-
122
- it 'should decrease by one each time a handler is run' do
123
- subject.post{ nil }
124
- subject.post{ sleep }
125
- subject.post{ sleep }
126
- sleep(0.1)
127
- subject.length.should eq 1
128
- end
129
- end
130
-
131
- context 'fulfillment' do
132
-
133
- it 'process each block in the queue' do
134
- @expected = []
135
- subject.post{ @expected << 1 }
136
- subject.post{ @expected << 2 }
137
- subject.post{ @expected << 3 }
138
- sleep(0.1)
139
- @expected.should eq [1,2,3]
140
- end
141
-
142
- it 'passes the current value to the handler' do
143
- @expected = nil
144
- Agent.new(10).post{|i| @expected = i }
145
- sleep(0.1)
146
- @expected.should eq 10
147
- end
148
-
149
- it 'sets the value to the handler return value on success' do
150
- subject.post{ 100 }
151
- sleep(0.1)
152
- subject.value.should eq 100
153
- end
154
-
155
- it 'rejects the handler after timeout reached' do
156
- agent = Agent.new(0, 0.1)
157
- agent.post{ sleep(1); 10 }
158
- agent.value.should eq 0
159
- end
160
- end
161
-
162
- context 'validation' do
163
-
164
- it 'processes the validator when present' do
165
- @expected = nil
166
- subject.validate{ @expected = 10; true }
167
- subject.post{ nil }
168
- sleep(0.1)
169
- @expected.should eq 10
170
- end
171
-
172
- it 'passes the new value to the validator' do
173
- @expected = nil
174
- subject.validate{|v| @expected = v; true }
175
- subject.post{ 10 }
176
- sleep(0.1)
177
- @expected.should eq 10
178
- end
179
-
180
- it 'sets the new value when the validator returns true' do
181
- agent = Agent.new(0).validate{ true }
182
- agent.post{ 10 }
183
- sleep(0.1)
184
- agent.value.should eq 10
185
- end
186
-
187
- it 'does not change the value when the validator returns false' do
188
- agent = Agent.new(0).validate{ false }
189
- agent.post{ 10 }
190
- sleep(0.1)
191
- agent.value.should eq 0
192
- end
193
-
194
- it 'does not change the value when the validator raises an exception' do
195
- agent = Agent.new(0).validate{ raise StandardError }
196
- agent.post{ 10 }
197
- sleep(0.1)
198
- agent.value.should eq 0
199
- end
200
- end
201
-
202
- context 'rejection' do
203
-
204
- it 'calls the first exception block with a matching class' do
205
- @expected = nil
206
- subject.
207
- rescue(StandardError){|ex| @expected = 1 }.
208
- rescue(StandardError){|ex| @expected = 2 }.
209
- rescue(StandardError){|ex| @expected = 3 }
210
- subject.post{ raise StandardError }
211
- sleep(0.1)
212
- @expected.should eq 1
213
- end
214
-
215
- it 'matches all with a rescue with no class given' do
216
- @expected = nil
217
- subject.
218
- rescue(LoadError){|ex| @expected = 1 }.
219
- rescue{|ex| @expected = 2 }.
220
- rescue(StandardError){|ex| @expected = 3 }
221
- subject.post{ raise NoMethodError }
222
- sleep(0.1)
223
- @expected.should eq 2
224
- end
225
-
226
- it 'searches associated rescue handlers in order' do
227
- @expected = nil
228
- subject.
229
- rescue(ArgumentError){|ex| @expected = 1 }.
230
- rescue(LoadError){|ex| @expected = 2 }.
231
- rescue(Exception){|ex| @expected = 3 }
232
- subject.post{ raise ArgumentError }
233
- sleep(0.1)
234
- @expected.should eq 1
235
-
236
- @expected = nil
237
- subject.
238
- rescue(ArgumentError){|ex| @expected = 1 }.
239
- rescue(LoadError){|ex| @expected = 2 }.
240
- rescue(Exception){|ex| @expected = 3 }
241
- subject.post{ raise LoadError }
242
- sleep(0.1)
243
- @expected.should eq 2
244
-
245
- @expected = nil
246
- subject.
247
- rescue(ArgumentError){|ex| @expected = 1 }.
248
- rescue(LoadError){|ex| @expected = 2 }.
249
- rescue(Exception){|ex| @expected = 3 }
250
- subject.post{ raise StandardError }
251
- sleep(0.1)
252
- @expected.should eq 3
253
- end
254
-
255
- it 'passes the exception object to the matched block' do
256
- @expected = nil
257
- subject.
258
- rescue(ArgumentError){|ex| @expected = ex }.
259
- rescue(LoadError){|ex| @expected = ex }.
260
- rescue(Exception){|ex| @expected = ex }
261
- subject.post{ raise StandardError }
262
- sleep(0.1)
263
- @expected.should be_a(StandardError)
264
- end
265
-
266
- it 'ignores rescuers without a block' do
267
- @expected = nil
268
- subject.
269
- rescue(StandardError).
270
- rescue(StandardError){|ex| @expected = ex }.
271
- rescue(Exception){|ex| @expected = ex }
272
- subject.post{ raise StandardError }
273
- sleep(0.1)
274
- @expected.should be_a(StandardError)
275
- end
276
-
277
- it 'supresses the exception if no rescue matches' do
278
- lambda {
279
- subject.
280
- rescue(ArgumentError){|ex| @expected = ex }.
281
- rescue(StandardError){|ex| @expected = ex }.
282
- rescue(Exception){|ex| @expected = ex }
283
- subject.post{ raise StandardError }
284
- sleep(0.1)
285
- }.should_not raise_error
286
- end
287
-
288
- it 'supresses exceptions thrown from rescue handlers' do
289
- lambda {
290
- subject.rescue(Exception){ raise StandardError }
291
- subject.post{ raise ArgumentError }
292
- sleep(0.1)
293
- }.should_not raise_error
294
- end
295
- end
296
-
297
- context 'observation' do
298
-
299
- it 'notifies all observers when the value changes' do
300
- agent = Agent.new(0)
301
- agent.add_observer(observer)
302
- agent.post{ 10 }
303
- sleep(0.1)
304
- observer.value.should eq 10
305
- end
306
-
307
- it 'does not notify observers when validation fails' do
308
- agent = Agent.new(0)
309
- agent.validate{ false }
310
- agent.add_observer(observer)
311
- agent.post{ 10 }
312
- sleep(0.1)
313
- observer.value.should be_nil
314
- end
315
-
316
- it 'does not notify observers when the handler raises an exception' do
317
- agent = Agent.new(0)
318
- agent.add_observer(observer)
319
- agent.post{ raise StandardError }
320
- sleep(0.1)
321
- observer.value.should be_nil
322
- end
323
- end
324
-
325
- context 'aliases' do
326
-
327
- it 'aliases #deref for #value' do
328
- Agent.new(10).deref.should eq 10
329
- end
330
-
331
- it 'aliases #validates for :validate' do
332
- @expected = nil
333
- subject.validates{|v| @expected = v }
334
- subject.post{ 10 }
335
- sleep(0.1)
336
- @expected.should eq 10
337
- end
338
-
339
- it 'aliases #validate_with for :validate' do
340
- @expected = nil
341
- subject.validate_with{|v| @expected = v }
342
- subject.post{ 10 }
343
- sleep(0.1)
344
- @expected.should eq 10
345
- end
346
-
347
- it 'aliases #validates_with for :validate' do
348
- @expected = nil
349
- subject.validates_with{|v| @expected = v }
350
- subject.post{ 10 }
351
- sleep(0.1)
352
- @expected.should eq 10
353
- end
354
-
355
- it 'aliases #catch for #rescue' do
356
- @expected = nil
357
- subject.catch{ @expected = true }
358
- subject.post{ raise StandardError }
359
- sleep(0.1)
360
- @expected.should be_true
361
- end
362
-
363
- it 'aliases #on_error for #rescue' do
364
- @expected = nil
365
- subject.on_error{ @expected = true }
366
- subject.post{ raise StandardError }
367
- sleep(0.1)
368
- @expected.should be_true
369
- end
370
-
371
- it 'aliases #add_watch for #add_observer' do
372
- agent = Agent.new(0)
373
- agent.add_watch(observer)
374
- agent.post{ 10 }
375
- sleep(0.1)
376
- observer.value.should eq 10
377
- end
378
- end
379
- end
380
- end
1
+ require 'spec_helper'
2
+
3
+ module Concurrent
4
+
5
+ describe Agent do
6
+
7
+ subject { Agent.new(0) }
8
+
9
+ let(:observer) do
10
+ Class.new do
11
+ attr_reader :value
12
+ define_method(:update) do |time, value|
13
+ @value = value
14
+ end
15
+ end.new
16
+ end
17
+
18
+ before(:each) do
19
+ Agent.thread_pool = FixedThreadPool.new(1)
20
+ end
21
+
22
+ context '#initialize' do
23
+
24
+ it 'sets the value to the given initial state' do
25
+ Agent.new(10).value.should eq 10
26
+ end
27
+
28
+ it 'sets the timeout to the given value' do
29
+ Agent.new(0, 5).timeout.should eq 5
30
+ end
31
+
32
+ it 'sets the timeout to the default when nil' do
33
+ Agent.new(0).timeout.should eq Agent::TIMEOUT
34
+ end
35
+
36
+ it 'sets the length to zero' do
37
+ Agent.new(10).length.should eq 0
38
+ end
39
+
40
+ it 'spawns the worker thread' do
41
+ Agent.thread_pool.should_receive(:post).once.with(any_args())
42
+ Agent.new(0)
43
+ end
44
+ end
45
+
46
+ context '#rescue' do
47
+
48
+ it 'returns self when a block is given' do
49
+ a1 = subject
50
+ a2 = a1.rescue{}
51
+ a1.object_id.should eq a2.object_id
52
+ end
53
+
54
+ it 'returns self when no block is given' do
55
+ a1 = subject
56
+ a2 = a1.rescue
57
+ a1.object_id.should eq a2.object_id
58
+ end
59
+
60
+ it 'accepts an exception class as the first parameter' do
61
+ lambda {
62
+ subject.rescue(StandardError){}
63
+ }.should_not raise_error
64
+ end
65
+
66
+ it 'ignores rescuers without a block' do
67
+ subject.rescue
68
+ subject.instance_variable_get(:@rescuers).should be_empty
69
+ end
70
+ end
71
+
72
+ context '#validate' do
73
+
74
+ it 'returns self when a block is given' do
75
+ a1 = subject
76
+ a2 = a1.validate{}
77
+ a1.object_id.should eq a2.object_id
78
+ end
79
+
80
+ it 'returns self when no block is given' do
81
+ a1 = subject
82
+ a2 = a1.validate
83
+ a1.object_id.should eq a2.object_id
84
+ end
85
+
86
+ it 'ignores validators without a block' do
87
+ subject.validate
88
+ subject.instance_variable_get(:@validator).should be_nil
89
+ end
90
+ end
91
+
92
+ context '#post' do
93
+
94
+ it 'adds the given block to the queue' do
95
+ subject.post{ sleep(100) }
96
+ sleep(0.1)
97
+ before = subject.length
98
+ subject.post{ nil }
99
+ subject.post{ nil }
100
+ subject.length.should eq before+2
101
+ end
102
+
103
+ it 'does not add to the queue when no block is given' do
104
+ subject.post{ sleep(100) }
105
+ sleep(0.1)
106
+ before = subject.length
107
+ subject.post
108
+ subject.post{ nil }
109
+ subject.length.should eq before+1
110
+ end
111
+ end
112
+
113
+ context '#length' do
114
+
115
+ it 'should be zero for a new agent' do
116
+ subject.length.should eq 0
117
+ end
118
+
119
+ it 'should increase by one for each #post' do
120
+ subject.post{ sleep(100) }
121
+ sleep(0.1)
122
+ subject.post{ sleep }
123
+ subject.post{ sleep }
124
+ subject.post{ sleep }
125
+ subject.length.should eq 3
126
+ end
127
+
128
+ it 'should decrease by one each time a handler is run' do
129
+ subject.post{ nil }
130
+ subject.post{ sleep }
131
+ subject.post{ sleep }
132
+ sleep(0.1)
133
+ subject.length.should eq 1
134
+ end
135
+ end
136
+
137
+ context 'fulfillment' do
138
+
139
+ it 'process each block in the queue' do
140
+ @expected = []
141
+ subject.post{ @expected << 1 }
142
+ subject.post{ @expected << 2 }
143
+ subject.post{ @expected << 3 }
144
+ sleep(0.1)
145
+ @expected.should eq [1,2,3]
146
+ end
147
+
148
+ it 'passes the current value to the handler' do
149
+ @expected = nil
150
+ Agent.new(10).post{|i| @expected = i }
151
+ sleep(0.1)
152
+ @expected.should eq 10
153
+ end
154
+
155
+ it 'sets the value to the handler return value on success' do
156
+ subject.post{ 100 }
157
+ sleep(0.1)
158
+ subject.value.should eq 100
159
+ end
160
+
161
+ it 'rejects the handler after timeout reached' do
162
+ agent = Agent.new(0, 0.1)
163
+ agent.post{ sleep(1); 10 }
164
+ agent.value.should eq 0
165
+ end
166
+ end
167
+
168
+ context 'validation' do
169
+
170
+ it 'processes the validator when present' do
171
+ @expected = nil
172
+ subject.validate{ @expected = 10; true }
173
+ subject.post{ nil }
174
+ sleep(0.1)
175
+ @expected.should eq 10
176
+ end
177
+
178
+ it 'passes the new value to the validator' do
179
+ @expected = nil
180
+ subject.validate{|v| @expected = v; true }
181
+ subject.post{ 10 }
182
+ sleep(0.1)
183
+ @expected.should eq 10
184
+ end
185
+
186
+ it 'sets the new value when the validator returns true' do
187
+ agent = Agent.new(0).validate{ true }
188
+ agent.post{ 10 }
189
+ sleep(0.1)
190
+ agent.value.should eq 10
191
+ end
192
+
193
+ it 'does not change the value when the validator returns false' do
194
+ agent = Agent.new(0).validate{ false }
195
+ agent.post{ 10 }
196
+ sleep(0.1)
197
+ agent.value.should eq 0
198
+ end
199
+
200
+ it 'does not change the value when the validator raises an exception' do
201
+ agent = Agent.new(0).validate{ raise StandardError }
202
+ agent.post{ 10 }
203
+ sleep(0.1)
204
+ agent.value.should eq 0
205
+ end
206
+ end
207
+
208
+ context 'rejection' do
209
+
210
+ it 'calls the first exception block with a matching class' do
211
+ @expected = nil
212
+ subject.
213
+ rescue(StandardError){|ex| @expected = 1 }.
214
+ rescue(StandardError){|ex| @expected = 2 }.
215
+ rescue(StandardError){|ex| @expected = 3 }
216
+ subject.post{ raise StandardError }
217
+ sleep(0.1)
218
+ @expected.should eq 1
219
+ end
220
+
221
+ it 'matches all with a rescue with no class given' do
222
+ @expected = nil
223
+ subject.
224
+ rescue(LoadError){|ex| @expected = 1 }.
225
+ rescue{|ex| @expected = 2 }.
226
+ rescue(StandardError){|ex| @expected = 3 }
227
+ subject.post{ raise NoMethodError }
228
+ sleep(0.1)
229
+ @expected.should eq 2
230
+ end
231
+
232
+ it 'searches associated rescue handlers in order' do
233
+ @expected = nil
234
+ subject.
235
+ rescue(ArgumentError){|ex| @expected = 1 }.
236
+ rescue(LoadError){|ex| @expected = 2 }.
237
+ rescue(Exception){|ex| @expected = 3 }
238
+ subject.post{ raise ArgumentError }
239
+ sleep(0.1)
240
+ @expected.should eq 1
241
+
242
+ @expected = nil
243
+ subject.
244
+ rescue(ArgumentError){|ex| @expected = 1 }.
245
+ rescue(LoadError){|ex| @expected = 2 }.
246
+ rescue(Exception){|ex| @expected = 3 }
247
+ subject.post{ raise LoadError }
248
+ sleep(0.1)
249
+ @expected.should eq 2
250
+
251
+ @expected = nil
252
+ subject.
253
+ rescue(ArgumentError){|ex| @expected = 1 }.
254
+ rescue(LoadError){|ex| @expected = 2 }.
255
+ rescue(Exception){|ex| @expected = 3 }
256
+ subject.post{ raise StandardError }
257
+ sleep(0.1)
258
+ @expected.should eq 3
259
+ end
260
+
261
+ it 'passes the exception object to the matched block' do
262
+ @expected = nil
263
+ subject.
264
+ rescue(ArgumentError){|ex| @expected = ex }.
265
+ rescue(LoadError){|ex| @expected = ex }.
266
+ rescue(Exception){|ex| @expected = ex }
267
+ subject.post{ raise StandardError }
268
+ sleep(0.1)
269
+ @expected.should be_a(StandardError)
270
+ end
271
+
272
+ it 'ignores rescuers without a block' do
273
+ @expected = nil
274
+ subject.
275
+ rescue(StandardError).
276
+ rescue(StandardError){|ex| @expected = ex }.
277
+ rescue(Exception){|ex| @expected = ex }
278
+ subject.post{ raise StandardError }
279
+ sleep(0.1)
280
+ @expected.should be_a(StandardError)
281
+ end
282
+
283
+ it 'supresses the exception if no rescue matches' do
284
+ lambda {
285
+ subject.
286
+ rescue(ArgumentError){|ex| @expected = ex }.
287
+ rescue(StandardError){|ex| @expected = ex }.
288
+ rescue(Exception){|ex| @expected = ex }
289
+ subject.post{ raise StandardError }
290
+ sleep(0.1)
291
+ }.should_not raise_error
292
+ end
293
+
294
+ it 'supresses exceptions thrown from rescue handlers' do
295
+ lambda {
296
+ subject.rescue(Exception){ raise StandardError }
297
+ subject.post{ raise ArgumentError }
298
+ sleep(0.1)
299
+ }.should_not raise_error
300
+ end
301
+ end
302
+
303
+ context 'observation' do
304
+
305
+ it 'notifies all observers when the value changes' do
306
+ agent = Agent.new(0)
307
+ agent.add_observer(observer)
308
+ agent.post{ 10 }
309
+ sleep(0.1)
310
+ observer.value.should eq 10
311
+ end
312
+
313
+ it 'does not notify observers when validation fails' do
314
+ agent = Agent.new(0)
315
+ agent.validate{ false }
316
+ agent.add_observer(observer)
317
+ agent.post{ 10 }
318
+ sleep(0.1)
319
+ observer.value.should be_nil
320
+ end
321
+
322
+ it 'does not notify observers when the handler raises an exception' do
323
+ agent = Agent.new(0)
324
+ agent.add_observer(observer)
325
+ agent.post{ raise StandardError }
326
+ sleep(0.1)
327
+ observer.value.should be_nil
328
+ end
329
+ end
330
+
331
+ context 'aliases' do
332
+
333
+ it 'aliases #deref for #value' do
334
+ Agent.new(10).deref.should eq 10
335
+ end
336
+
337
+ it 'aliases #validates for :validate' do
338
+ @expected = nil
339
+ subject.validates{|v| @expected = v }
340
+ subject.post{ 10 }
341
+ sleep(0.1)
342
+ @expected.should eq 10
343
+ end
344
+
345
+ it 'aliases #validate_with for :validate' do
346
+ @expected = nil
347
+ subject.validate_with{|v| @expected = v }
348
+ subject.post{ 10 }
349
+ sleep(0.1)
350
+ @expected.should eq 10
351
+ end
352
+
353
+ it 'aliases #validates_with for :validate' do
354
+ @expected = nil
355
+ subject.validates_with{|v| @expected = v }
356
+ subject.post{ 10 }
357
+ sleep(0.1)
358
+ @expected.should eq 10
359
+ end
360
+
361
+ it 'aliases #catch for #rescue' do
362
+ @expected = nil
363
+ subject.catch{ @expected = true }
364
+ subject.post{ raise StandardError }
365
+ sleep(0.1)
366
+ @expected.should be_true
367
+ end
368
+
369
+ it 'aliases #on_error for #rescue' do
370
+ @expected = nil
371
+ subject.on_error{ @expected = true }
372
+ subject.post{ raise StandardError }
373
+ sleep(0.1)
374
+ @expected.should be_true
375
+ end
376
+
377
+ it 'aliases #add_watch for #add_observer' do
378
+ agent = Agent.new(0)
379
+ agent.add_watch(observer)
380
+ agent.post{ 10 }
381
+ sleep(0.1)
382
+ observer.value.should eq 10
383
+ end
384
+ end
385
+ end
386
+ end