concurrent-ruby 0.1.0 → 0.1.1.pre.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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