concurrent-ruby 0.2.2 → 0.3.0.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. checksums.yaml +4 -4
  2. data/README.md +45 -42
  3. data/lib/concurrent.rb +5 -6
  4. data/lib/concurrent/agent.rb +29 -33
  5. data/lib/concurrent/cached_thread_pool.rb +26 -105
  6. data/lib/concurrent/channel.rb +94 -0
  7. data/lib/concurrent/event.rb +8 -17
  8. data/lib/concurrent/executor.rb +68 -72
  9. data/lib/concurrent/fixed_thread_pool.rb +15 -83
  10. data/lib/concurrent/functions.rb +7 -22
  11. data/lib/concurrent/future.rb +29 -9
  12. data/lib/concurrent/null_thread_pool.rb +5 -2
  13. data/lib/concurrent/obligation.rb +6 -16
  14. data/lib/concurrent/promise.rb +9 -10
  15. data/lib/concurrent/runnable.rb +103 -0
  16. data/lib/concurrent/supervisor.rb +271 -44
  17. data/lib/concurrent/thread_pool.rb +112 -39
  18. data/lib/concurrent/version.rb +1 -1
  19. data/md/executor.md +9 -3
  20. data/md/goroutine.md +11 -9
  21. data/md/reactor.md +32 -0
  22. data/md/supervisor.md +43 -0
  23. data/spec/concurrent/agent_spec.rb +128 -51
  24. data/spec/concurrent/cached_thread_pool_spec.rb +33 -47
  25. data/spec/concurrent/channel_spec.rb +446 -0
  26. data/spec/concurrent/event_machine_defer_proxy_spec.rb +3 -1
  27. data/spec/concurrent/event_spec.rb +0 -19
  28. data/spec/concurrent/executor_spec.rb +167 -119
  29. data/spec/concurrent/fixed_thread_pool_spec.rb +40 -30
  30. data/spec/concurrent/functions_spec.rb +0 -20
  31. data/spec/concurrent/future_spec.rb +88 -0
  32. data/spec/concurrent/null_thread_pool_spec.rb +23 -2
  33. data/spec/concurrent/obligation_shared.rb +0 -5
  34. data/spec/concurrent/promise_spec.rb +9 -10
  35. data/spec/concurrent/runnable_shared.rb +62 -0
  36. data/spec/concurrent/runnable_spec.rb +233 -0
  37. data/spec/concurrent/supervisor_spec.rb +912 -47
  38. data/spec/concurrent/thread_pool_shared.rb +18 -31
  39. data/spec/spec_helper.rb +10 -3
  40. metadata +17 -23
  41. data/lib/concurrent/defer.rb +0 -65
  42. data/lib/concurrent/reactor.rb +0 -166
  43. data/lib/concurrent/reactor/drb_async_demux.rb +0 -83
  44. data/lib/concurrent/reactor/tcp_sync_demux.rb +0 -131
  45. data/lib/concurrent/utilities.rb +0 -32
  46. data/md/defer.md +0 -174
  47. data/spec/concurrent/defer_spec.rb +0 -199
  48. data/spec/concurrent/reactor/drb_async_demux_spec.rb +0 -196
  49. data/spec/concurrent/reactor/tcp_sync_demux_spec.rb +0 -410
  50. data/spec/concurrent/reactor_spec.rb +0 -364
  51. data/spec/concurrent/utilities_spec.rb +0 -74
@@ -1,19 +1,7 @@
1
- require 'functional/behavior'
1
+ require 'thread'
2
+ require 'functional'
2
3
 
3
4
  require 'concurrent/event'
4
- require 'concurrent/utilities'
5
-
6
- behavior_info(:thread_pool,
7
- running?: 0,
8
- shutdown?: 0,
9
- killed?: 0,
10
- shutdown: 0,
11
- kill: 0,
12
- size: 0,
13
- wait_for_termination: -1,
14
- post: -1,
15
- :<< => 1,
16
- status: 0)
17
5
 
18
6
  behavior_info(:global_thread_pool,
19
7
  post: -1,
@@ -21,44 +9,49 @@ behavior_info(:global_thread_pool,
21
9
 
22
10
  module Concurrent
23
11
 
24
- class ThreadPool
12
+ class AbstractThreadPool
25
13
 
26
- def initialize
27
- @status = :running
28
- @queue = Queue.new
29
- @termination = Event.new
30
- @pool = []
31
- end
14
+ WorkerContext = Struct.new(:status, :idletime, :thread)
32
15
 
33
- def running?
34
- return @status == :running
35
- end
16
+ MIN_POOL_SIZE = 1
17
+ MAX_POOL_SIZE = 256
36
18
 
37
- def shutdown?
38
- return @status == :shutdown
19
+ attr_accessor :max_threads
20
+
21
+ def initialize(opts = {})
22
+ @max_threads = opts[:max_threads] || opts[:max] || MAX_POOL_SIZE
23
+ if @max_threads < MIN_POOL_SIZE || @max_threads > MAX_POOL_SIZE
24
+ raise ArgumentError.new("pool size must be from #{MIN_POOL_SIZE} to #{MAX_POOL_SIZE}")
25
+ end
26
+
27
+ @state = :running
28
+ @mutex ||= Mutex.new
29
+ @terminator ||= Event.new
30
+ @pool ||= []
31
+ @queue ||= Queue.new
32
+ @working = 0
39
33
  end
40
34
 
41
- def killed?
42
- return @status == :killed
35
+ def running?
36
+ return @state == :running
43
37
  end
44
38
 
45
39
  def shutdown
46
- mutex.synchronize do
40
+ @mutex.synchronize do
41
+ @collector.kill if @collector && @collector.status
47
42
  if @pool.empty?
48
- @status = :shutdown
43
+ @state = :shutdown
44
+ @terminator.set
49
45
  else
50
- @status = :shuttingdown
46
+ @state = :shuttingdown
51
47
  @pool.size.times{ @queue << :stop }
52
48
  end
53
49
  end
50
+ Thread.pass
54
51
  end
55
52
 
56
53
  def wait_for_termination(timeout = nil)
57
- if shutdown? || killed?
58
- return true
59
- else
60
- return @termination.wait(timeout)
61
- end
54
+ return @terminator.wait(timeout)
62
55
  end
63
56
 
64
57
  def <<(block)
@@ -66,11 +59,91 @@ module Concurrent
66
59
  return self
67
60
  end
68
61
 
62
+ def kill
63
+ @mutex.synchronize do
64
+ @collector.kill if @collector && @collector.status
65
+ @state = :shuttingdown
66
+ @pool.each{|t| Thread.kill(t.thread) }
67
+ @terminator.set
68
+ end
69
+ Thread.pass
70
+ end
71
+
72
+ def size
73
+ return @mutex.synchronize do
74
+ @state == :running ? @pool.length : 0
75
+ end
76
+ end
77
+ alias_method :length, :size
78
+
79
+ def status
80
+ @mutex.synchronize do
81
+ @pool.collect do |worker|
82
+ [
83
+ worker.status,
84
+ worker.status == :idle ? delta(worker.idletime, timestamp) : nil,
85
+ worker.thread.status
86
+ ]
87
+ end
88
+ end
89
+ end
90
+
69
91
  protected
70
92
 
71
- # @private
72
- def mutex # :nodoc:
73
- @mutex || Mutex.new
93
+ def timestamp
94
+ return Time.now.to_i
95
+ end
96
+
97
+ private
98
+
99
+ def create_worker_thread
100
+ context = WorkerContext.new(:idle, timestamp, nil)
101
+
102
+ context.thread = Thread.new do
103
+ Thread.current.abort_on_exception = false
104
+ loop do
105
+ task = @queue.pop
106
+ if task == :stop
107
+ @mutex.synchronize do
108
+ context.status = :stopping
109
+ end
110
+ break
111
+ else
112
+ @mutex.synchronize do
113
+ context.status = :working
114
+ @working += 1
115
+ end
116
+ task.last.call(*task.first)
117
+ @mutex.synchronize do
118
+ @working -= 1
119
+ context.status = :idle
120
+ context.idletime = timestamp
121
+ end
122
+ end
123
+ end
124
+ @mutex.synchronize do
125
+ @pool.delete(context)
126
+ if @pool.empty? && @state != :running
127
+ @terminator.set
128
+ @state = :shutdown
129
+ end
130
+ end
131
+ end
132
+
133
+ Thread.pass
134
+ run_garbage_collector unless @collector && @collector.alive?
135
+ return context
136
+ end
137
+
138
+ def run_garbage_collector
139
+ @collector = Thread.new do
140
+ Thread.current.abort_on_exception = false
141
+ loop do
142
+ sleep(1)
143
+ @mutex.synchronize { collect_garbage }
144
+ end
145
+ end
146
+ Thread.pass
74
147
  end
75
148
  end
76
149
  end
@@ -1,3 +1,3 @@
1
1
  module Concurrent
2
- VERSION = '0.2.2'
2
+ VERSION = '0.3.0.pre.1'
3
3
  end
@@ -21,10 +21,16 @@ One other advantage of the `Executor` class is that it forces the bsiness logic
21
21
  be completely decoupled from the threading logic. The business logic can be tested
22
22
  separately then passed to the an executor for scheduling and running.
23
23
 
24
+ The `Executor` is the yin to to the
25
+ [Supervisor's](https://github.com/jdantonio/concurrent-ruby/blob/master/md/supervisor.md)
26
+ yang. Where the `Supervisor` is intended to manage long-running threads that operate
27
+ continuously, the `Executor` is intended to manage fairly short operations that
28
+ occur repeatedly at regular intervals.
29
+
24
30
  Unlike some of the others concurrency objects in the library, executors do not
25
- run on the global. In my experience the types of tasks that will benefit from
26
- the `Executor` class tend to also be long running. For this reason they get their
27
- own thread every time the task is executed.
31
+ run on the global thread pool. In my experience the types of tasks that will benefit
32
+ from the `Executor` class tend to also be long running. For this reason they get
33
+ their own thread every time the task is executed.
28
34
 
29
35
  ## ExecutionContext
30
36
 
@@ -11,15 +11,17 @@ for processing.
11
11
  ```ruby
12
12
  require 'concurrent'
13
13
 
14
- @expected = nil
15
-
16
- go(1, 2, 3){|a, b, c| sleep(1); @expected = [c, b, a] }
17
-
18
- sleep(0.1)
19
- @expected #=> nil
20
-
21
- sleep(2)
22
- @expected #=> [3, 2, 1]
14
+ go('foo'){|echo| sleep(0.1); print "#{echo}\n"; sleep(0.1); print "Boom!\n" }
15
+ go('bar'){|echo| sleep(0.1); print "#{echo}\n"; sleep(0.1); print "Pow!\n" }
16
+ go('baz'){|echo| sleep(0.1); print "#{echo}\n"; sleep(0.1); print "Zap!\n" }
17
+ sleep(0.5)
18
+
19
+ #=> foo
20
+ #=> bar
21
+ #=> baz
22
+ #=> Boom!
23
+ #=> Pow!
24
+ #=> Zap!
23
25
  ```
24
26
 
25
27
  ## Copyright
@@ -0,0 +1,32 @@
1
+ # Event
2
+
3
+ TBD...
4
+
5
+ ## Copyright
6
+
7
+ *Concurrent Ruby* is Copyright &copy; 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
8
+ It is free software and may be redistributed under the terms specified in the LICENSE file.
9
+
10
+ ## License
11
+
12
+ Released under the MIT license.
13
+
14
+ http://www.opensource.org/licenses/mit-license.php
15
+
16
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
17
+ > of this software and associated documentation files (the "Software"), to deal
18
+ > in the Software without restriction, including without limitation the rights
19
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
20
+ > copies of the Software, and to permit persons to whom the Software is
21
+ > furnished to do so, subject to the following conditions:
22
+ >
23
+ > The above copyright notice and this permission notice shall be included in
24
+ > all copies or substantial portions of the Software.
25
+ >
26
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
27
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
28
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
29
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
30
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
31
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
32
+ > THE SOFTWARE.
@@ -0,0 +1,43 @@
1
+ # You don't need to get no supervisor! You the supervisor today!
2
+
3
+ TBD...
4
+
5
+ The `Sypervisor` is the yin to to the
6
+ [Executor's](https://github.com/jdantonio/concurrent-ruby/blob/master/md/executor.md)
7
+ yang. Where the `Supervisor` is intended to manage long-running threads that operate
8
+ continuously, the `Executor` is intended to manage fairly short operations that
9
+ occur repeatedly at regular intervals.
10
+
11
+ * [Supervisor Module](http://www.erlang.org/doc/man/supervisor.html)
12
+ * [Supervisor Behaviour](http://www.erlang.org/doc/design_principles/sup_princ.html)
13
+ * [Who Supervises The Supervisors?](http://learnyousomeerlang.com/supervisors)
14
+ * [OTP Design Principles](http://www.erlang.org/doc/design_principles/des_princ.html)
15
+
16
+ ## Copyright
17
+
18
+ *Concurrent Ruby* is Copyright &copy; 2013 [Jerry D'Antonio](https://twitter.com/jerrydantonio).
19
+ It is free software and may be redistributed under the terms specified in the LICENSE file.
20
+
21
+ ## License
22
+
23
+ Released under the MIT license.
24
+
25
+ http://www.opensource.org/licenses/mit-license.php
26
+
27
+ > Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ > of this software and associated documentation files (the "Software"), to deal
29
+ > in the Software without restriction, including without limitation the rights
30
+ > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ > copies of the Software, and to permit persons to whom the Software is
32
+ > furnished to do so, subject to the following conditions:
33
+ >
34
+ > The above copyright notice and this permission notice shall be included in
35
+ > all copies or substantial portions of the Software.
36
+ >
37
+ > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
43
+ > THE SOFTWARE.
@@ -30,21 +30,12 @@ module Concurrent
30
30
  end
31
31
 
32
32
  it 'sets the timeout to the given value' do
33
- Agent.new(0, 5).timeout.should eq 5
33
+ Agent.new(0, timeout: 5).timeout.should eq 5
34
34
  end
35
35
 
36
36
  it 'sets the timeout to the default when nil' do
37
37
  Agent.new(0).timeout.should eq Agent::TIMEOUT
38
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
39
  end
49
40
 
50
41
  context '#rescue' do
@@ -96,45 +87,19 @@ module Concurrent
96
87
  context '#post' do
97
88
 
98
89
  it 'adds the given block to the queue' do
90
+ Agent.thread_pool.should_receive(:post).with(no_args()).exactly(3).times
99
91
  subject.post{ sleep(100) }
100
- sleep(0.1)
101
- before = subject.length
102
92
  subject.post{ nil }
103
93
  subject.post{ nil }
104
- subject.length.should eq before+2
94
+ sleep(0.1)
105
95
  end
106
96
 
107
97
  it 'does not add to the queue when no block is given' do
98
+ Agent.thread_pool.should_receive(:post).with(no_args()).exactly(2).times
108
99
  subject.post{ sleep(100) }
109
- sleep(0.1)
110
- before = subject.length
111
100
  subject.post
112
101
  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
102
  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
103
  end
139
104
  end
140
105
 
@@ -163,7 +128,7 @@ module Concurrent
163
128
  end
164
129
 
165
130
  it 'rejects the handler after timeout reached' do
166
- agent = Agent.new(0, 0.1)
131
+ agent = Agent.new(0, timeout: 0.1)
167
132
  agent.post{ sleep(1); 10 }
168
133
  agent.value.should eq 0
169
134
  end
@@ -238,7 +203,7 @@ module Concurrent
238
203
  subject.
239
204
  rescue(ArgumentError){|ex| @expected = 1 }.
240
205
  rescue(LoadError){|ex| @expected = 2 }.
241
- rescue(Exception){|ex| @expected = 3 }
206
+ rescue(StandardError){|ex| @expected = 3 }
242
207
  subject.post{ raise ArgumentError }
243
208
  sleep(0.1)
244
209
  @expected.should eq 1
@@ -247,7 +212,7 @@ module Concurrent
247
212
  subject.
248
213
  rescue(ArgumentError){|ex| @expected = 1 }.
249
214
  rescue(LoadError){|ex| @expected = 2 }.
250
- rescue(Exception){|ex| @expected = 3 }
215
+ rescue(StandardError){|ex| @expected = 3 }
251
216
  subject.post{ raise LoadError }
252
217
  sleep(0.1)
253
218
  @expected.should eq 2
@@ -256,7 +221,7 @@ module Concurrent
256
221
  subject.
257
222
  rescue(ArgumentError){|ex| @expected = 1 }.
258
223
  rescue(LoadError){|ex| @expected = 2 }.
259
- rescue(Exception){|ex| @expected = 3 }
224
+ rescue(StandardError){|ex| @expected = 3 }
260
225
  subject.post{ raise StandardError }
261
226
  sleep(0.1)
262
227
  @expected.should eq 3
@@ -267,7 +232,7 @@ module Concurrent
267
232
  subject.
268
233
  rescue(ArgumentError){|ex| @expected = ex }.
269
234
  rescue(LoadError){|ex| @expected = ex }.
270
- rescue(Exception){|ex| @expected = ex }
235
+ rescue(StandardError){|ex| @expected = ex }
271
236
  subject.post{ raise StandardError }
272
237
  sleep(0.1)
273
238
  @expected.should be_a(StandardError)
@@ -275,11 +240,9 @@ module Concurrent
275
240
 
276
241
  it 'ignores rescuers without a block' do
277
242
  @expected = nil
278
- subject.
243
+ Promise.new{ raise StandardError }.
279
244
  rescue(StandardError).
280
- rescue(StandardError){|ex| @expected = ex }.
281
- rescue(Exception){|ex| @expected = ex }
282
- subject.post{ raise StandardError }
245
+ rescue(StandardError){|ex| @expected = ex }
283
246
  sleep(0.1)
284
247
  @expected.should be_a(StandardError)
285
248
  end
@@ -288,8 +251,8 @@ module Concurrent
288
251
  lambda {
289
252
  subject.
290
253
  rescue(ArgumentError){|ex| @expected = ex }.
291
- rescue(StandardError){|ex| @expected = ex }.
292
- rescue(Exception){|ex| @expected = ex }
254
+ rescue(NotImplementedError){|ex| @expected = ex }.
255
+ rescue(NoMethodError){|ex| @expected = ex }
293
256
  subject.post{ raise StandardError }
294
257
  sleep(0.1)
295
258
  }.should_not raise_error
@@ -297,13 +260,109 @@ module Concurrent
297
260
 
298
261
  it 'supresses exceptions thrown from rescue handlers' do
299
262
  lambda {
300
- subject.rescue(Exception){ raise StandardError }
263
+ subject.rescue(StandardError){ raise StandardError }
301
264
  subject.post{ raise ArgumentError }
302
265
  sleep(0.1)
303
266
  }.should_not raise_error
304
267
  end
305
268
  end
306
269
 
270
+ context 'dereference' do
271
+
272
+ it 'defaults :dup_on_deref to false' do
273
+ value = 'value'
274
+ value.should_not_receive(:dup).with(any_args())
275
+
276
+ agent = Agent.new(value)
277
+ agent.value.should eq 'value'
278
+
279
+ agent = Agent.new(value, dup_on_deref: false)
280
+ agent.value.should eq 'value'
281
+
282
+ agent = Agent.new(value, dup: false)
283
+ agent.value.should eq 'value'
284
+ end
285
+
286
+ it 'calls #dup when the :dup_on_deref option is true' do
287
+ value = 'value'
288
+
289
+ agent = Agent.new(value, dup_on_deref: true)
290
+ agent.value.object_id.should_not eq value.object_id
291
+ agent.value.should eq 'value'
292
+
293
+ agent = Agent.new(value, dup: true)
294
+ agent.value.object_id.should_not eq value.object_id
295
+ agent.value.should eq 'value'
296
+ end
297
+
298
+ it 'defaults :freeze_on_deref to false' do
299
+ value = 'value'
300
+ value.should_not_receive(:freeze).with(any_args())
301
+
302
+ agent = Agent.new(value)
303
+ agent.value.should eq 'value'
304
+
305
+ agent = Agent.new(value, freeze_on_deref: false)
306
+ agent.value.should eq 'value'
307
+
308
+ agent = Agent.new(value, freeze: false)
309
+ agent.value.should eq 'value'
310
+ end
311
+
312
+ it 'calls #freeze when the :freeze_on_deref option is true' do
313
+ value = 'value'
314
+
315
+ agent = Agent.new(value, freeze_on_deref: true)
316
+ agent.value.should be_frozen
317
+ agent.value.should eq 'value'
318
+
319
+ agent = Agent.new(value, freeze: true)
320
+ agent.value.should be_frozen
321
+ agent.value.should eq 'value'
322
+ end
323
+
324
+ it 'defaults :copy_on_deref to nil' do
325
+ value = 'value'
326
+
327
+ agent = Agent.new(value)
328
+ agent.value.object_id.should == value.object_id
329
+ agent.value.should eq 'value'
330
+
331
+ agent = Agent.new(value, copy_on_deref: nil)
332
+ agent.value.object_id.should == value.object_id
333
+ agent.value.should eq 'value'
334
+
335
+ agent = Agent.new(value, copy: nil)
336
+ agent.value.object_id.should == value.object_id
337
+ agent.value.should eq 'value'
338
+ end
339
+
340
+ it 'calls the block when the :copy_on_deref option is passed a proc' do
341
+ value = 'value'
342
+ copy = proc{|val| 'copy' }
343
+
344
+ agent = Agent.new(value, copy_on_deref: copy)
345
+ agent.value.object_id.should_not == value.object_id
346
+
347
+ agent = Agent.new(value, copy: copy)
348
+ agent.value.object_id.should_not == value.object_id
349
+ end
350
+
351
+ it 'calls the :copy block first followed by #dup followed by #freeze' do
352
+ value = 'value'
353
+ copied = 'copied'
354
+ dup = 'dup'
355
+ frozen = 'frozen'
356
+ copy = proc{|val| copied }
357
+
358
+ copied.should_receive(:dup).with(no_args()).and_return(dup)
359
+ dup.should_receive(:freeze).with(no_args()).and_return(frozen)
360
+
361
+ agent = Agent.new(value, dup_on_deref: true, freeze_on_deref: true, copy_on_deref: copy)
362
+ agent.value.should eq frozen
363
+ end
364
+ end
365
+
307
366
  context 'observation' do
308
367
 
309
368
  it 'notifies all observers when the value changes' do
@@ -386,5 +445,23 @@ module Concurrent
386
445
  observer.value.should eq 10
387
446
  end
388
447
  end
448
+
449
+ context 'stress test' do
450
+
451
+ before(:each) do
452
+ Agent.thread_pool = FixedThreadPool.new(5)
453
+ end
454
+
455
+ specify do
456
+ count = 10_000
457
+ counter = Concurrent::Agent.new(0)
458
+
459
+ count.times do |i|
460
+ counter.post{|value| value + 1 }
461
+ end
462
+
463
+ sleep(0.1) until counter.value == count
464
+ end
465
+ end
389
466
  end
390
467
  end