concurrent-ruby 0.1.0 → 0.1.1.pre.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/LICENSE +21 -21
  2. data/README.md +279 -224
  3. data/lib/concurrent.rb +27 -20
  4. data/lib/concurrent/agent.rb +106 -130
  5. data/lib/concurrent/cached_thread_pool.rb +130 -122
  6. data/lib/concurrent/defer.rb +67 -69
  7. data/lib/concurrent/drb_async_demux.rb +72 -0
  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 +87 -0
  11. data/lib/concurrent/fixed_thread_pool.rb +89 -89
  12. data/lib/concurrent/functions.rb +120 -0
  13. data/lib/concurrent/future.rb +52 -42
  14. data/lib/concurrent/global_thread_pool.rb +3 -3
  15. data/lib/concurrent/goroutine.rb +29 -25
  16. data/lib/concurrent/obligation.rb +67 -121
  17. data/lib/concurrent/promise.rb +172 -194
  18. data/lib/concurrent/reactor.rb +162 -0
  19. data/lib/concurrent/smart_mutex.rb +66 -0
  20. data/lib/concurrent/tcp_sync_demux.rb +96 -0
  21. data/lib/concurrent/thread_pool.rb +65 -61
  22. data/lib/concurrent/utilities.rb +34 -0
  23. data/lib/concurrent/version.rb +3 -3
  24. data/lib/concurrent_ruby.rb +1 -1
  25. data/md/agent.md +123 -123
  26. data/md/defer.md +174 -174
  27. data/md/event.md +32 -32
  28. data/md/executor.md +176 -0
  29. data/md/future.md +83 -83
  30. data/md/goroutine.md +52 -52
  31. data/md/obligation.md +32 -32
  32. data/md/promise.md +225 -225
  33. data/md/thread_pool.md +197 -197
  34. data/spec/concurrent/agent_spec.rb +376 -405
  35. data/spec/concurrent/cached_thread_pool_spec.rb +112 -112
  36. data/spec/concurrent/defer_spec.rb +209 -199
  37. data/spec/concurrent/event_machine_defer_proxy_spec.rb +250 -246
  38. data/spec/concurrent/event_spec.rb +134 -134
  39. data/spec/concurrent/executor_spec.rb +146 -0
  40. data/spec/concurrent/fixed_thread_pool_spec.rb +84 -84
  41. data/spec/concurrent/functions_spec.rb +57 -0
  42. data/spec/concurrent/future_spec.rb +125 -115
  43. data/spec/concurrent/goroutine_spec.rb +67 -52
  44. data/spec/concurrent/obligation_shared.rb +121 -121
  45. data/spec/concurrent/promise_spec.rb +299 -310
  46. data/spec/concurrent/smart_mutex_spec.rb +234 -0
  47. data/spec/concurrent/thread_pool_shared.rb +209 -209
  48. data/spec/concurrent/utilities_spec.rb +74 -0
  49. data/spec/spec_helper.rb +21 -19
  50. metadata +38 -14
  51. checksums.yaml +0 -7
@@ -1,197 +1,197 @@
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
- ### EventMachine
155
-
156
- The [EventMachine](http://rubyeventmachine.com/) library (source [online](https://github.com/eventmachine/eventmachine))
157
- is an awesome library for creating evented applications. EventMachine provides its own thread pool
158
- and the authors recommend using their pool rather than using Ruby's `Thread`. No sweat,
159
- `functional-ruby` is fully compatible with EventMachine. Simple require `eventmachine`
160
- *before* requiring `functional-ruby` then replace the global thread pool with an instance
161
- of `EventMachineDeferProxy`:
162
-
163
- ```ruby
164
- require 'eventmachine' # do this FIRST
165
- require 'functional/concurrency'
166
-
167
- $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
168
- ```
169
-
170
- ## Copyright
171
-
172
- *Concurrent Ruby* is Copyright &copy; 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
173
- It is free software and may be redistributed under the terms specified in the LICENSE file.
174
-
175
- ## License
176
-
177
- Released under the MIT license.
178
-
179
- http://www.opensource.org/licenses/mit-license.php
180
-
181
- > Permission is hereby granted, free of charge, to any person obtaining a copy
182
- > of this software and associated documentation files (the "Software"), to deal
183
- > in the Software without restriction, including without limitation the rights
184
- > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
185
- > copies of the Software, and to permit persons to whom the Software is
186
- > furnished to do so, subject to the following conditions:
187
- >
188
- > The above copyright notice and this permission notice shall be included in
189
- > all copies or substantial portions of the Software.
190
- >
191
- > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
192
- > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
193
- > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
194
- > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
195
- > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
196
- > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
197
- > 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
+ ### EventMachine
155
+
156
+ The [EventMachine](http://rubyeventmachine.com/) library (source [online](https://github.com/eventmachine/eventmachine))
157
+ is an awesome library for creating evented applications. EventMachine provides its own thread pool
158
+ and the authors recommend using their pool rather than using Ruby's `Thread`. No sweat,
159
+ `functional-ruby` is fully compatible with EventMachine. Simple require `eventmachine`
160
+ *before* requiring `functional-ruby` then replace the global thread pool with an instance
161
+ of `EventMachineDeferProxy`:
162
+
163
+ ```ruby
164
+ require 'eventmachine' # do this FIRST
165
+ require 'functional/concurrency'
166
+
167
+ $GLOBAL_THREAD_POOL = EventMachineDeferProxy.new
168
+ ```
169
+
170
+ ## Copyright
171
+
172
+ *Concurrent Ruby* is Copyright &copy; 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
173
+ It is free software and may be redistributed under the terms specified in the LICENSE file.
174
+
175
+ ## License
176
+
177
+ Released under the MIT license.
178
+
179
+ http://www.opensource.org/licenses/mit-license.php
180
+
181
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
182
+ > of this software and associated documentation files (the "Software"), to deal
183
+ > in the Software without restriction, including without limitation the rights
184
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
185
+ > copies of the Software, and to permit persons to whom the Software is
186
+ > furnished to do so, subject to the following conditions:
187
+ >
188
+ > The above copyright notice and this permission notice shall be included in
189
+ > all copies or substantial portions of the Software.
190
+ >
191
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
192
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
193
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
194
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
195
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
196
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
197
+ > THE SOFTWARE.
@@ -1,405 +1,376 @@
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
- $GLOBAL_THREAD_POOL = CachedThreadPool.new
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
- $GLOBAL_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
-
379
- it 'aliases #<< for Agent#post' do
380
- subject << proc{ 100 }
381
- sleep(0.1)
382
- subject.value.should eq 100
383
-
384
- subject << lambda{ 100 }
385
- sleep(0.1)
386
- subject.value.should eq 100
387
- end
388
-
389
- it 'aliases Kernel#agent for Agent.new' do
390
- agent(10).should be_a(Agent)
391
- end
392
-
393
- it 'aliases Kernel#deref for #deref' do
394
- deref(Agent.new(10)).should eq 10
395
- deref(Agent.new(10), 10).should eq 10
396
- end
397
-
398
- it 'aliases Kernel:post for Agent#post' do
399
- post(subject){ 100 }
400
- sleep(0.1)
401
- subject.value.should eq 100
402
- end
403
- end
404
- end
405
- 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
+ context '#initialize' do
19
+
20
+ it 'sets the value to the given initial state' do
21
+ Agent.new(10).value.should eq 10
22
+ end
23
+
24
+ it 'sets the timeout to the given value' do
25
+ Agent.new(0, 5).timeout.should eq 5
26
+ end
27
+
28
+ it 'sets the timeout to the default when nil' do
29
+ Agent.new(0).timeout.should eq Agent::TIMEOUT
30
+ end
31
+
32
+ it 'sets the length to zero' do
33
+ Agent.new(10).length.should eq 0
34
+ end
35
+
36
+ it 'spawns the worker thread' do
37
+ Thread.should_receive(:new).once.with(any_args())
38
+ Agent.new(0)
39
+ end
40
+ end
41
+
42
+ context '#rescue' do
43
+
44
+ it 'returns self when a block is given' do
45
+ a1 = subject
46
+ a2 = a1.rescue{}
47
+ a1.object_id.should eq a2.object_id
48
+ end
49
+
50
+ it 'returns self when no block is given' do
51
+ a1 = subject
52
+ a2 = a1.rescue
53
+ a1.object_id.should eq a2.object_id
54
+ end
55
+
56
+ it 'accepts an exception class as the first parameter' do
57
+ lambda {
58
+ subject.rescue(StandardError){}
59
+ }.should_not raise_error
60
+ end
61
+
62
+ it 'ignores rescuers without a block' do
63
+ subject.rescue
64
+ subject.instance_variable_get(:@rescuers).should be_empty
65
+ end
66
+ end
67
+
68
+ context '#validate' do
69
+
70
+ it 'returns self when a block is given' do
71
+ a1 = subject
72
+ a2 = a1.validate{}
73
+ a1.object_id.should eq a2.object_id
74
+ end
75
+
76
+ it 'returns self when no block is given' do
77
+ a1 = subject
78
+ a2 = a1.validate
79
+ a1.object_id.should eq a2.object_id
80
+ end
81
+
82
+ it 'ignores validators without a block' do
83
+ subject.validate
84
+ subject.instance_variable_get(:@validator).should be_nil
85
+ end
86
+ end
87
+
88
+ context '#post' do
89
+
90
+ it 'adds the given block to the queue' do
91
+ before = subject.length
92
+ subject.post{ nil }
93
+ subject.post{ nil }
94
+ subject.length.should eq before+2
95
+ end
96
+
97
+ it 'does not add to the queue when no block is given' do
98
+ before = subject.length
99
+ subject.post
100
+ subject.post{ nil }
101
+ subject.length.should eq before+1
102
+ end
103
+ end
104
+
105
+ context '#length' do
106
+
107
+ it 'should be zero for a new agent' do
108
+ subject.length.should eq 0
109
+ end
110
+
111
+ it 'should increase by one for each #post' do
112
+ subject.post{ sleep }
113
+ subject.post{ sleep }
114
+ subject.post{ sleep }
115
+ subject.length.should eq 3
116
+ end
117
+
118
+ it 'should decrease by one each time a handler is run' do
119
+ subject.post{ nil }
120
+ subject.post{ sleep }
121
+ subject.post{ sleep }
122
+ sleep(0.1)
123
+ subject.length.should eq 1
124
+ end
125
+ end
126
+
127
+ context 'fulfillment' do
128
+
129
+ it 'process each block in the queue' do
130
+ @expected = []
131
+ subject.post{ @expected << 1 }
132
+ subject.post{ @expected << 2 }
133
+ subject.post{ @expected << 3 }
134
+ sleep(0.1)
135
+ @expected.should eq [1,2,3]
136
+ end
137
+
138
+ it 'passes the current value to the handler' do
139
+ @expected = nil
140
+ Agent.new(10).post{|i| @expected = i }
141
+ sleep(0.1)
142
+ @expected.should eq 10
143
+ end
144
+
145
+ it 'sets the value to the handler return value on success' do
146
+ subject.post{ 100 }
147
+ sleep(0.1)
148
+ subject.value.should eq 100
149
+ end
150
+
151
+ it 'rejects the handler after timeout reached' do
152
+ agent = Agent.new(0, 0.1)
153
+ agent.post{ sleep(1); 10 }
154
+ agent.value.should eq 0
155
+ end
156
+ end
157
+
158
+ context 'validation' do
159
+
160
+ it 'processes the validator when present' do
161
+ @expected = nil
162
+ subject.validate{ @expected = 10; true }
163
+ subject.post{ nil }
164
+ sleep(0.1)
165
+ @expected.should eq 10
166
+ end
167
+
168
+ it 'passes the new value to the validator' do
169
+ @expected = nil
170
+ subject.validate{|v| @expected = v; true }
171
+ subject.post{ 10 }
172
+ sleep(0.1)
173
+ @expected.should eq 10
174
+ end
175
+
176
+ it 'sets the new value when the validator returns true' do
177
+ agent = Agent.new(0).validate{ true }
178
+ agent.post{ 10 }
179
+ sleep(0.1)
180
+ agent.value.should eq 10
181
+ end
182
+
183
+ it 'does not change the value when the validator returns false' do
184
+ agent = Agent.new(0).validate{ false }
185
+ agent.post{ 10 }
186
+ sleep(0.1)
187
+ agent.value.should eq 0
188
+ end
189
+
190
+ it 'does not change the value when the validator raises an exception' do
191
+ agent = Agent.new(0).validate{ raise StandardError }
192
+ agent.post{ 10 }
193
+ sleep(0.1)
194
+ agent.value.should eq 0
195
+ end
196
+ end
197
+
198
+ context 'rejection' do
199
+
200
+ it 'calls the first exception block with a matching class' do
201
+ @expected = nil
202
+ subject.
203
+ rescue(StandardError){|ex| @expected = 1 }.
204
+ rescue(StandardError){|ex| @expected = 2 }.
205
+ rescue(StandardError){|ex| @expected = 3 }
206
+ subject.post{ raise StandardError }
207
+ sleep(0.1)
208
+ @expected.should eq 1
209
+ end
210
+
211
+ it 'matches all with a rescue with no class given' do
212
+ @expected = nil
213
+ subject.
214
+ rescue(LoadError){|ex| @expected = 1 }.
215
+ rescue{|ex| @expected = 2 }.
216
+ rescue(StandardError){|ex| @expected = 3 }
217
+ subject.post{ raise NoMethodError }
218
+ sleep(0.1)
219
+ @expected.should eq 2
220
+ end
221
+
222
+ it 'searches associated rescue handlers in order' do
223
+ @expected = nil
224
+ subject.
225
+ rescue(ArgumentError){|ex| @expected = 1 }.
226
+ rescue(LoadError){|ex| @expected = 2 }.
227
+ rescue(Exception){|ex| @expected = 3 }
228
+ subject.post{ raise ArgumentError }
229
+ sleep(0.1)
230
+ @expected.should eq 1
231
+
232
+ @expected = nil
233
+ subject.
234
+ rescue(ArgumentError){|ex| @expected = 1 }.
235
+ rescue(LoadError){|ex| @expected = 2 }.
236
+ rescue(Exception){|ex| @expected = 3 }
237
+ subject.post{ raise LoadError }
238
+ sleep(0.1)
239
+ @expected.should eq 2
240
+
241
+ @expected = nil
242
+ subject.
243
+ rescue(ArgumentError){|ex| @expected = 1 }.
244
+ rescue(LoadError){|ex| @expected = 2 }.
245
+ rescue(Exception){|ex| @expected = 3 }
246
+ subject.post{ raise StandardError }
247
+ sleep(0.1)
248
+ @expected.should eq 3
249
+ end
250
+
251
+ it 'passes the exception object to the matched block' do
252
+ @expected = nil
253
+ subject.
254
+ rescue(ArgumentError){|ex| @expected = ex }.
255
+ rescue(LoadError){|ex| @expected = ex }.
256
+ rescue(Exception){|ex| @expected = ex }
257
+ subject.post{ raise StandardError }
258
+ sleep(0.1)
259
+ @expected.should be_a(StandardError)
260
+ end
261
+
262
+ it 'ignores rescuers without a block' do
263
+ @expected = nil
264
+ subject.
265
+ rescue(StandardError).
266
+ rescue(StandardError){|ex| @expected = ex }.
267
+ rescue(Exception){|ex| @expected = ex }
268
+ subject.post{ raise StandardError }
269
+ sleep(0.1)
270
+ @expected.should be_a(StandardError)
271
+ end
272
+
273
+ it 'supresses the exception if no rescue matches' do
274
+ lambda {
275
+ subject.
276
+ rescue(ArgumentError){|ex| @expected = ex }.
277
+ rescue(StandardError){|ex| @expected = ex }.
278
+ rescue(Exception){|ex| @expected = ex }
279
+ subject.post{ raise StandardError }
280
+ sleep(0.1)
281
+ }.should_not raise_error
282
+ end
283
+
284
+ it 'supresses exceptions thrown from rescue handlers' do
285
+ lambda {
286
+ subject.rescue(Exception){ raise StandardError }
287
+ subject.post{ raise ArgumentError }
288
+ sleep(0.1)
289
+ }.should_not raise_error
290
+ end
291
+ end
292
+
293
+ context 'observation' do
294
+
295
+ it 'notifies all observers when the value changes' do
296
+ agent = Agent.new(0)
297
+ agent.add_observer(observer)
298
+ agent.post{ 10 }
299
+ sleep(0.1)
300
+ observer.value.should eq 10
301
+ end
302
+
303
+ it 'does not notify observers when validation fails' do
304
+ agent = Agent.new(0)
305
+ agent.validate{ false }
306
+ agent.add_observer(observer)
307
+ agent.post{ 10 }
308
+ sleep(0.1)
309
+ observer.value.should be_nil
310
+ end
311
+
312
+ it 'does not notify observers when the handler raises an exception' do
313
+ agent = Agent.new(0)
314
+ agent.add_observer(observer)
315
+ agent.post{ raise StandardError }
316
+ sleep(0.1)
317
+ observer.value.should be_nil
318
+ end
319
+ end
320
+
321
+ context 'aliases' do
322
+
323
+ it 'aliases #deref for #value' do
324
+ Agent.new(10).deref.should eq 10
325
+ end
326
+
327
+ it 'aliases #validates for :validate' do
328
+ @expected = nil
329
+ subject.validates{|v| @expected = v }
330
+ subject.post{ 10 }
331
+ sleep(0.1)
332
+ @expected.should eq 10
333
+ end
334
+
335
+ it 'aliases #validate_with for :validate' do
336
+ @expected = nil
337
+ subject.validate_with{|v| @expected = v }
338
+ subject.post{ 10 }
339
+ sleep(0.1)
340
+ @expected.should eq 10
341
+ end
342
+
343
+ it 'aliases #validates_with for :validate' do
344
+ @expected = nil
345
+ subject.validates_with{|v| @expected = v }
346
+ subject.post{ 10 }
347
+ sleep(0.1)
348
+ @expected.should eq 10
349
+ end
350
+
351
+ it 'aliases #catch for #rescue' do
352
+ @expected = nil
353
+ subject.catch{ @expected = true }
354
+ subject.post{ raise StandardError }
355
+ sleep(0.1)
356
+ @expected.should be_true
357
+ end
358
+
359
+ it 'aliases #on_error for #rescue' do
360
+ @expected = nil
361
+ subject.on_error{ @expected = true }
362
+ subject.post{ raise StandardError }
363
+ sleep(0.1)
364
+ @expected.should be_true
365
+ end
366
+
367
+ it 'aliases #add_watch for #add_observer' do
368
+ agent = Agent.new(0)
369
+ agent.add_watch(observer)
370
+ agent.post{ 10 }
371
+ sleep(0.1)
372
+ observer.value.should eq 10
373
+ end
374
+ end
375
+ end
376
+ end