concurrent-ruby 0.2.1 → 0.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -21
- data/README.md +276 -275
- data/lib/concurrent.rb +28 -28
- data/lib/concurrent/agent.rb +114 -114
- data/lib/concurrent/cached_thread_pool.rb +131 -131
- data/lib/concurrent/defer.rb +65 -65
- data/lib/concurrent/event.rb +60 -60
- data/lib/concurrent/event_machine_defer_proxy.rb +23 -23
- data/lib/concurrent/executor.rb +96 -96
- data/lib/concurrent/fixed_thread_pool.rb +99 -99
- data/lib/concurrent/functions.rb +120 -120
- data/lib/concurrent/future.rb +42 -42
- data/lib/concurrent/global_thread_pool.rb +24 -16
- data/lib/concurrent/goroutine.rb +29 -29
- data/lib/concurrent/null_thread_pool.rb +22 -22
- data/lib/concurrent/obligation.rb +67 -67
- data/lib/concurrent/promise.rb +174 -174
- data/lib/concurrent/reactor.rb +166 -166
- data/lib/concurrent/reactor/drb_async_demux.rb +83 -83
- data/lib/concurrent/reactor/tcp_sync_demux.rb +131 -131
- data/lib/concurrent/supervisor.rb +105 -105
- data/lib/concurrent/thread_pool.rb +76 -76
- data/lib/concurrent/utilities.rb +32 -32
- data/lib/concurrent/version.rb +3 -3
- data/lib/concurrent_ruby.rb +1 -1
- data/md/agent.md +123 -123
- data/md/defer.md +174 -174
- data/md/event.md +32 -32
- data/md/executor.md +187 -187
- data/md/future.md +83 -83
- data/md/goroutine.md +52 -52
- data/md/obligation.md +32 -32
- data/md/promise.md +227 -227
- data/md/thread_pool.md +224 -224
- data/spec/concurrent/agent_spec.rb +390 -386
- data/spec/concurrent/cached_thread_pool_spec.rb +125 -125
- data/spec/concurrent/defer_spec.rb +199 -195
- data/spec/concurrent/event_machine_defer_proxy_spec.rb +256 -256
- data/spec/concurrent/event_spec.rb +134 -134
- data/spec/concurrent/executor_spec.rb +200 -200
- data/spec/concurrent/fixed_thread_pool_spec.rb +83 -83
- data/spec/concurrent/functions_spec.rb +217 -217
- data/spec/concurrent/future_spec.rb +112 -108
- data/spec/concurrent/global_thread_pool_spec.rb +11 -38
- data/spec/concurrent/goroutine_spec.rb +67 -67
- data/spec/concurrent/null_thread_pool_spec.rb +57 -57
- data/spec/concurrent/obligation_shared.rb +132 -132
- data/spec/concurrent/promise_spec.rb +316 -312
- data/spec/concurrent/reactor/drb_async_demux_spec.rb +196 -196
- data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +410 -410
- data/spec/concurrent/reactor_spec.rb +364 -364
- data/spec/concurrent/supervisor_spec.rb +269 -269
- data/spec/concurrent/thread_pool_shared.rb +204 -204
- data/spec/concurrent/uses_global_thread_pool_shared.rb +64 -0
- data/spec/concurrent/utilities_spec.rb +74 -74
- data/spec/spec_helper.rb +32 -32
- metadata +17 -19
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 © 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 © 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,386 +1,390 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
it 'sets the
|
29
|
-
Agent.new(
|
30
|
-
end
|
31
|
-
|
32
|
-
it 'sets the timeout to the
|
33
|
-
Agent.new(0).timeout.should eq
|
34
|
-
end
|
35
|
-
|
36
|
-
it 'sets the
|
37
|
-
Agent.new(
|
38
|
-
end
|
39
|
-
|
40
|
-
it '
|
41
|
-
Agent.
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
a1
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
a1
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
subject.post{
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
subject.
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
subject.post{
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
it 'should
|
120
|
-
subject.
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
subject.post{ sleep }
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
subject.
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
subject.
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
@expected
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
@expected
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
@expected
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
@expected
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
@expected
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
agent
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
agent
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
subject.
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
@expected
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
subject.
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
@expected
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
subject.
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
subject.
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
subject.
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
@expected
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
subject.
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
@expected
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
subject.
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
subject.
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
|
337
|
-
it 'aliases #
|
338
|
-
|
339
|
-
|
340
|
-
|
341
|
-
|
342
|
-
@expected
|
343
|
-
|
344
|
-
|
345
|
-
|
346
|
-
@expected
|
347
|
-
|
348
|
-
|
349
|
-
|
350
|
-
@expected
|
351
|
-
|
352
|
-
|
353
|
-
|
354
|
-
@expected
|
355
|
-
|
356
|
-
|
357
|
-
|
358
|
-
@expected
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
@expected
|
363
|
-
|
364
|
-
|
365
|
-
|
366
|
-
@expected
|
367
|
-
|
368
|
-
|
369
|
-
|
370
|
-
@expected
|
371
|
-
|
372
|
-
|
373
|
-
|
374
|
-
@expected
|
375
|
-
|
376
|
-
|
377
|
-
|
378
|
-
|
379
|
-
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
1
|
+
require 'spec_helper'
|
2
|
+
require_relative 'uses_global_thread_pool_shared'
|
3
|
+
|
4
|
+
module Concurrent
|
5
|
+
|
6
|
+
describe Agent do
|
7
|
+
|
8
|
+
let!(:thread_pool_user){ Agent }
|
9
|
+
it_should_behave_like Concurrent::UsesGlobalThreadPool
|
10
|
+
|
11
|
+
subject { Agent.new(0) }
|
12
|
+
|
13
|
+
let(:observer) do
|
14
|
+
Class.new do
|
15
|
+
attr_reader :value
|
16
|
+
define_method(:update) do |time, value|
|
17
|
+
@value = value
|
18
|
+
end
|
19
|
+
end.new
|
20
|
+
end
|
21
|
+
|
22
|
+
before(:each) do
|
23
|
+
Agent.thread_pool = FixedThreadPool.new(1)
|
24
|
+
end
|
25
|
+
|
26
|
+
context '#initialize' do
|
27
|
+
|
28
|
+
it 'sets the value to the given initial state' do
|
29
|
+
Agent.new(10).value.should eq 10
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'sets the timeout to the given value' do
|
33
|
+
Agent.new(0, 5).timeout.should eq 5
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'sets the timeout to the default when nil' do
|
37
|
+
Agent.new(0).timeout.should eq Agent::TIMEOUT
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'sets the length to zero' do
|
41
|
+
Agent.new(10).length.should eq 0
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'spawns the worker thread' do
|
45
|
+
Agent.thread_pool.should_receive(:post).once.with(any_args())
|
46
|
+
Agent.new(0)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context '#rescue' do
|
51
|
+
|
52
|
+
it 'returns self when a block is given' do
|
53
|
+
a1 = subject
|
54
|
+
a2 = a1.rescue{}
|
55
|
+
a1.object_id.should eq a2.object_id
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'returns self when no block is given' do
|
59
|
+
a1 = subject
|
60
|
+
a2 = a1.rescue
|
61
|
+
a1.object_id.should eq a2.object_id
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'accepts an exception class as the first parameter' do
|
65
|
+
lambda {
|
66
|
+
subject.rescue(StandardError){}
|
67
|
+
}.should_not raise_error
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'ignores rescuers without a block' do
|
71
|
+
subject.rescue
|
72
|
+
subject.instance_variable_get(:@rescuers).should be_empty
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context '#validate' do
|
77
|
+
|
78
|
+
it 'returns self when a block is given' do
|
79
|
+
a1 = subject
|
80
|
+
a2 = a1.validate{}
|
81
|
+
a1.object_id.should eq a2.object_id
|
82
|
+
end
|
83
|
+
|
84
|
+
it 'returns self when no block is given' do
|
85
|
+
a1 = subject
|
86
|
+
a2 = a1.validate
|
87
|
+
a1.object_id.should eq a2.object_id
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'ignores validators without a block' do
|
91
|
+
subject.validate
|
92
|
+
subject.instance_variable_get(:@validator).should be_nil
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context '#post' do
|
97
|
+
|
98
|
+
it 'adds the given block to the queue' do
|
99
|
+
subject.post{ sleep(100) }
|
100
|
+
sleep(0.1)
|
101
|
+
before = subject.length
|
102
|
+
subject.post{ nil }
|
103
|
+
subject.post{ nil }
|
104
|
+
subject.length.should eq before+2
|
105
|
+
end
|
106
|
+
|
107
|
+
it 'does not add to the queue when no block is given' do
|
108
|
+
subject.post{ sleep(100) }
|
109
|
+
sleep(0.1)
|
110
|
+
before = subject.length
|
111
|
+
subject.post
|
112
|
+
subject.post{ nil }
|
113
|
+
subject.length.should eq before+1
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
context '#length' do
|
118
|
+
|
119
|
+
it 'should be zero for a new agent' do
|
120
|
+
subject.length.should eq 0
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'should increase by one for each #post' do
|
124
|
+
subject.post{ sleep(100) }
|
125
|
+
sleep(0.1)
|
126
|
+
subject.post{ sleep }
|
127
|
+
subject.post{ sleep }
|
128
|
+
subject.post{ sleep }
|
129
|
+
subject.length.should eq 3
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'should decrease by one each time a handler is run' do
|
133
|
+
subject.post{ nil }
|
134
|
+
subject.post{ sleep }
|
135
|
+
subject.post{ sleep }
|
136
|
+
sleep(0.1)
|
137
|
+
subject.length.should eq 1
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context 'fulfillment' do
|
142
|
+
|
143
|
+
it 'process each block in the queue' do
|
144
|
+
@expected = []
|
145
|
+
subject.post{ @expected << 1 }
|
146
|
+
subject.post{ @expected << 2 }
|
147
|
+
subject.post{ @expected << 3 }
|
148
|
+
sleep(0.1)
|
149
|
+
@expected.should eq [1,2,3]
|
150
|
+
end
|
151
|
+
|
152
|
+
it 'passes the current value to the handler' do
|
153
|
+
@expected = nil
|
154
|
+
Agent.new(10).post{|i| @expected = i }
|
155
|
+
sleep(0.1)
|
156
|
+
@expected.should eq 10
|
157
|
+
end
|
158
|
+
|
159
|
+
it 'sets the value to the handler return value on success' do
|
160
|
+
subject.post{ 100 }
|
161
|
+
sleep(0.1)
|
162
|
+
subject.value.should eq 100
|
163
|
+
end
|
164
|
+
|
165
|
+
it 'rejects the handler after timeout reached' do
|
166
|
+
agent = Agent.new(0, 0.1)
|
167
|
+
agent.post{ sleep(1); 10 }
|
168
|
+
agent.value.should eq 0
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
context 'validation' do
|
173
|
+
|
174
|
+
it 'processes the validator when present' do
|
175
|
+
@expected = nil
|
176
|
+
subject.validate{ @expected = 10; true }
|
177
|
+
subject.post{ nil }
|
178
|
+
sleep(0.1)
|
179
|
+
@expected.should eq 10
|
180
|
+
end
|
181
|
+
|
182
|
+
it 'passes the new value to the validator' do
|
183
|
+
@expected = nil
|
184
|
+
subject.validate{|v| @expected = v; true }
|
185
|
+
subject.post{ 10 }
|
186
|
+
sleep(0.1)
|
187
|
+
@expected.should eq 10
|
188
|
+
end
|
189
|
+
|
190
|
+
it 'sets the new value when the validator returns true' do
|
191
|
+
agent = Agent.new(0).validate{ true }
|
192
|
+
agent.post{ 10 }
|
193
|
+
sleep(0.1)
|
194
|
+
agent.value.should eq 10
|
195
|
+
end
|
196
|
+
|
197
|
+
it 'does not change the value when the validator returns false' do
|
198
|
+
agent = Agent.new(0).validate{ false }
|
199
|
+
agent.post{ 10 }
|
200
|
+
sleep(0.1)
|
201
|
+
agent.value.should eq 0
|
202
|
+
end
|
203
|
+
|
204
|
+
it 'does not change the value when the validator raises an exception' do
|
205
|
+
agent = Agent.new(0).validate{ raise StandardError }
|
206
|
+
agent.post{ 10 }
|
207
|
+
sleep(0.1)
|
208
|
+
agent.value.should eq 0
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
context 'rejection' do
|
213
|
+
|
214
|
+
it 'calls the first exception block with a matching class' do
|
215
|
+
@expected = nil
|
216
|
+
subject.
|
217
|
+
rescue(StandardError){|ex| @expected = 1 }.
|
218
|
+
rescue(StandardError){|ex| @expected = 2 }.
|
219
|
+
rescue(StandardError){|ex| @expected = 3 }
|
220
|
+
subject.post{ raise StandardError }
|
221
|
+
sleep(0.1)
|
222
|
+
@expected.should eq 1
|
223
|
+
end
|
224
|
+
|
225
|
+
it 'matches all with a rescue with no class given' do
|
226
|
+
@expected = nil
|
227
|
+
subject.
|
228
|
+
rescue(LoadError){|ex| @expected = 1 }.
|
229
|
+
rescue{|ex| @expected = 2 }.
|
230
|
+
rescue(StandardError){|ex| @expected = 3 }
|
231
|
+
subject.post{ raise NoMethodError }
|
232
|
+
sleep(0.1)
|
233
|
+
@expected.should eq 2
|
234
|
+
end
|
235
|
+
|
236
|
+
it 'searches associated rescue handlers in order' do
|
237
|
+
@expected = nil
|
238
|
+
subject.
|
239
|
+
rescue(ArgumentError){|ex| @expected = 1 }.
|
240
|
+
rescue(LoadError){|ex| @expected = 2 }.
|
241
|
+
rescue(Exception){|ex| @expected = 3 }
|
242
|
+
subject.post{ raise ArgumentError }
|
243
|
+
sleep(0.1)
|
244
|
+
@expected.should eq 1
|
245
|
+
|
246
|
+
@expected = nil
|
247
|
+
subject.
|
248
|
+
rescue(ArgumentError){|ex| @expected = 1 }.
|
249
|
+
rescue(LoadError){|ex| @expected = 2 }.
|
250
|
+
rescue(Exception){|ex| @expected = 3 }
|
251
|
+
subject.post{ raise LoadError }
|
252
|
+
sleep(0.1)
|
253
|
+
@expected.should eq 2
|
254
|
+
|
255
|
+
@expected = nil
|
256
|
+
subject.
|
257
|
+
rescue(ArgumentError){|ex| @expected = 1 }.
|
258
|
+
rescue(LoadError){|ex| @expected = 2 }.
|
259
|
+
rescue(Exception){|ex| @expected = 3 }
|
260
|
+
subject.post{ raise StandardError }
|
261
|
+
sleep(0.1)
|
262
|
+
@expected.should eq 3
|
263
|
+
end
|
264
|
+
|
265
|
+
it 'passes the exception object to the matched block' do
|
266
|
+
@expected = nil
|
267
|
+
subject.
|
268
|
+
rescue(ArgumentError){|ex| @expected = ex }.
|
269
|
+
rescue(LoadError){|ex| @expected = ex }.
|
270
|
+
rescue(Exception){|ex| @expected = ex }
|
271
|
+
subject.post{ raise StandardError }
|
272
|
+
sleep(0.1)
|
273
|
+
@expected.should be_a(StandardError)
|
274
|
+
end
|
275
|
+
|
276
|
+
it 'ignores rescuers without a block' do
|
277
|
+
@expected = nil
|
278
|
+
subject.
|
279
|
+
rescue(StandardError).
|
280
|
+
rescue(StandardError){|ex| @expected = ex }.
|
281
|
+
rescue(Exception){|ex| @expected = ex }
|
282
|
+
subject.post{ raise StandardError }
|
283
|
+
sleep(0.1)
|
284
|
+
@expected.should be_a(StandardError)
|
285
|
+
end
|
286
|
+
|
287
|
+
it 'supresses the exception if no rescue matches' do
|
288
|
+
lambda {
|
289
|
+
subject.
|
290
|
+
rescue(ArgumentError){|ex| @expected = ex }.
|
291
|
+
rescue(StandardError){|ex| @expected = ex }.
|
292
|
+
rescue(Exception){|ex| @expected = ex }
|
293
|
+
subject.post{ raise StandardError }
|
294
|
+
sleep(0.1)
|
295
|
+
}.should_not raise_error
|
296
|
+
end
|
297
|
+
|
298
|
+
it 'supresses exceptions thrown from rescue handlers' do
|
299
|
+
lambda {
|
300
|
+
subject.rescue(Exception){ raise StandardError }
|
301
|
+
subject.post{ raise ArgumentError }
|
302
|
+
sleep(0.1)
|
303
|
+
}.should_not raise_error
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
context 'observation' do
|
308
|
+
|
309
|
+
it 'notifies all observers when the value changes' do
|
310
|
+
agent = Agent.new(0)
|
311
|
+
agent.add_observer(observer)
|
312
|
+
agent.post{ 10 }
|
313
|
+
sleep(0.1)
|
314
|
+
observer.value.should eq 10
|
315
|
+
end
|
316
|
+
|
317
|
+
it 'does not notify observers when validation fails' do
|
318
|
+
agent = Agent.new(0)
|
319
|
+
agent.validate{ false }
|
320
|
+
agent.add_observer(observer)
|
321
|
+
agent.post{ 10 }
|
322
|
+
sleep(0.1)
|
323
|
+
observer.value.should be_nil
|
324
|
+
end
|
325
|
+
|
326
|
+
it 'does not notify observers when the handler raises an exception' do
|
327
|
+
agent = Agent.new(0)
|
328
|
+
agent.add_observer(observer)
|
329
|
+
agent.post{ raise StandardError }
|
330
|
+
sleep(0.1)
|
331
|
+
observer.value.should be_nil
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
context 'aliases' do
|
336
|
+
|
337
|
+
it 'aliases #deref for #value' do
|
338
|
+
Agent.new(10).deref.should eq 10
|
339
|
+
end
|
340
|
+
|
341
|
+
it 'aliases #validates for :validate' do
|
342
|
+
@expected = nil
|
343
|
+
subject.validates{|v| @expected = v }
|
344
|
+
subject.post{ 10 }
|
345
|
+
sleep(0.1)
|
346
|
+
@expected.should eq 10
|
347
|
+
end
|
348
|
+
|
349
|
+
it 'aliases #validate_with for :validate' do
|
350
|
+
@expected = nil
|
351
|
+
subject.validate_with{|v| @expected = v }
|
352
|
+
subject.post{ 10 }
|
353
|
+
sleep(0.1)
|
354
|
+
@expected.should eq 10
|
355
|
+
end
|
356
|
+
|
357
|
+
it 'aliases #validates_with for :validate' do
|
358
|
+
@expected = nil
|
359
|
+
subject.validates_with{|v| @expected = v }
|
360
|
+
subject.post{ 10 }
|
361
|
+
sleep(0.1)
|
362
|
+
@expected.should eq 10
|
363
|
+
end
|
364
|
+
|
365
|
+
it 'aliases #catch for #rescue' do
|
366
|
+
@expected = nil
|
367
|
+
subject.catch{ @expected = true }
|
368
|
+
subject.post{ raise StandardError }
|
369
|
+
sleep(0.1)
|
370
|
+
@expected.should be_true
|
371
|
+
end
|
372
|
+
|
373
|
+
it 'aliases #on_error for #rescue' do
|
374
|
+
@expected = nil
|
375
|
+
subject.on_error{ @expected = true }
|
376
|
+
subject.post{ raise StandardError }
|
377
|
+
sleep(0.1)
|
378
|
+
@expected.should be_true
|
379
|
+
end
|
380
|
+
|
381
|
+
it 'aliases #add_watch for #add_observer' do
|
382
|
+
agent = Agent.new(0)
|
383
|
+
agent.add_watch(observer)
|
384
|
+
agent.post{ 10 }
|
385
|
+
sleep(0.1)
|
386
|
+
observer.value.should eq 10
|
387
|
+
end
|
388
|
+
end
|
389
|
+
end
|
390
|
+
end
|