concurrent-ruby 0.2.2 → 0.3.0.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. 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,4 +1,6 @@
1
1
  require 'spec_helper'
2
+ require 'timecop'
3
+ require_relative 'runnable_shared'
2
4
 
3
5
  module Concurrent
4
6
 
@@ -7,18 +9,56 @@ module Concurrent
7
9
  let(:worker_class) do
8
10
  Class.new {
9
11
  behavior(:runnable)
10
- def run() return true; end
11
- def stop() return true; end
12
+ attr_reader :start_count, :stop_count
13
+ def run() @start_count ||= 0; @start_count += 1; return true; end
14
+ def stop() @stop_count ||= 0; @stop_count += 1; return true; end
12
15
  def running?() return true; end
13
16
  }
14
17
  end
15
18
 
19
+ let(:sleeper_class) do
20
+ Class.new(worker_class) {
21
+ def run() super(); sleep; end
22
+ }
23
+ end
24
+
25
+ let(:stopper_class) do
26
+ Class.new(worker_class) {
27
+ def initialize(sleep_time = 0.2) @sleep_time = sleep_time; end;
28
+ def run() super(); sleep(@sleep_time); end
29
+ }
30
+ end
31
+
32
+ let(:error_class) do
33
+ Class.new(worker_class) {
34
+ def run() super(); raise StandardError; end
35
+ }
36
+ end
37
+
38
+ let(:runner_class) do
39
+ Class.new(worker_class) {
40
+ attr_accessor :stopped
41
+ def run()
42
+ super()
43
+ stopped = false
44
+ loop do
45
+ break if stopped
46
+ Thread.pass
47
+ end
48
+ end
49
+ def stop() super(); stopped = true; end
50
+ }
51
+ end
52
+
16
53
  let(:worker){ worker_class.new }
17
54
 
18
- subject{ Supervisor.new }
55
+ subject{ Supervisor.new(strategy: :one_for_one, monitor_interval: 0.1) }
56
+
57
+ it_should_behave_like :runnable
19
58
 
20
59
  after(:each) do
21
60
  subject.stop
61
+ sleep(0.1)
22
62
  end
23
63
 
24
64
  context '#initialize' do
@@ -38,21 +78,98 @@ module Concurrent
38
78
  supervisor.should_not be_running
39
79
  end
40
80
 
41
- it 'sets the monitor interval when given' do
81
+ it 'uses the given monitor interval' do
42
82
  supervisor = Supervisor.new
43
83
  supervisor.monitor_interval.should == Supervisor::DEFAULT_MONITOR_INTERVAL
44
84
  end
45
85
 
46
- it 'sets the monitor interval to the default when not given' do
86
+ it 'uses the default monitor interval when none given' do
47
87
  supervisor = Supervisor.new(monitor_interval: 5)
48
88
  supervisor.monitor_interval.should == 5
89
+ end
90
+
91
+ it 'raises an exception when given an invalid monitor interval' do
92
+ lambda {
93
+ Supervisor.new(monitor_interval: -1)
94
+ }.should raise_error(ArgumentError)
95
+
96
+ lambda {
97
+ Supervisor.new(monitor_interval: 'bogus')
98
+ }.should raise_error(ArgumentError)
99
+ end
100
+
101
+ it 'uses the given restart strategy' do
102
+ supervisor = Supervisor.new(restart_strategy: :rest_for_one)
103
+ supervisor.restart_strategy.should eq :rest_for_one
104
+ end
105
+
106
+ it 'uses :one_for_one when no restart strategy given' do
107
+ supervisor = Supervisor.new
108
+ supervisor.restart_strategy.should eq :one_for_one
109
+ end
110
+
111
+ it 'raises an exception when given an invalid restart strategy' do
112
+ Supervisor::RESTART_STRATEGIES.each do |strategy|
113
+ lambda {
114
+ supervisor = Supervisor.new(strategy: strategy)
115
+ }.should_not raise_error
116
+ end
117
+
118
+ lambda {
119
+ supervisor = Supervisor.new(strategy: :bogus)
120
+ }.should raise_error(ArgumentError)
121
+ end
122
+
123
+ it 'uses the given maximum restart value' do
124
+ supervisor = Supervisor.new(max_restart: 3)
125
+ supervisor.max_r.should == 3
126
+
127
+ supervisor = Supervisor.new(max_r: 3)
128
+ supervisor.max_restart.should == 3
129
+ end
130
+
131
+ it 'uses the default maximum restart value when none given' do
132
+ supervisor = Supervisor.new
133
+ supervisor.max_restart.should == Supervisor::DEFAULT_MAX_RESTART
134
+ supervisor.max_r.should == Supervisor::DEFAULT_MAX_RESTART
135
+ end
136
+
137
+ it 'raises an exception when given an invalid maximum restart value' do
138
+ lambda {
139
+ Supervisor.new(max_restart: -1)
140
+ }.should raise_error(ArgumentError)
141
+
142
+ lambda {
143
+ Supervisor.new(max_restart: 'bogus')
144
+ }.should raise_error(ArgumentError)
145
+ end
146
+
147
+ it 'uses the given maximum time value' do
148
+ supervisor = Supervisor.new(max_time: 3)
149
+ supervisor.max_t.should == 3
150
+
151
+ supervisor = Supervisor.new(max_t: 3)
152
+ supervisor.max_time.should == 3
153
+ end
154
+
155
+ it 'uses the default maximum time value when none given' do
156
+ supervisor = Supervisor.new
157
+ supervisor.max_time.should == Supervisor::DEFAULT_MAX_TIME
158
+ supervisor.max_t.should == Supervisor::DEFAULT_MAX_TIME
159
+ end
160
+
161
+ it 'raises an exception when given an invalid maximum time value' do
162
+ lambda {
163
+ Supervisor.new(max_time: -1)
164
+ }.should raise_error(ArgumentError)
49
165
 
50
- supervisor = Supervisor.new(monitor: 10)
51
- supervisor.monitor_interval.should == 10
166
+ lambda {
167
+ Supervisor.new(max_time: 'bogus')
168
+ }.should raise_error(ArgumentError)
52
169
  end
53
170
  end
54
171
 
55
- context 'run' do
172
+ context '#run' do
56
173
 
57
174
  it 'runs the monitor' do
58
175
  subject.should_receive(:monitor).with(no_args()).at_least(1).times
@@ -84,8 +201,10 @@ module Concurrent
84
201
  @thread = nil
85
202
  subject.run!
86
203
  lambda {
87
- @thread = Thread.new{ subject.run }
88
- @thread.abort_on_exception = true
204
+ @thread = Thread.new do
205
+ Thread.current.abort_on_exception = true
206
+ subject.run
207
+ end
89
208
  sleep(0.1)
90
209
  }.should raise_error(StandardError)
91
210
  subject.stop
@@ -99,6 +218,7 @@ module Concurrent
99
218
  thread = Thread.new{ nil }
100
219
  Thread.should_receive(:new).with(no_args()).and_return(thread)
101
220
  subject.run!
221
+ sleep(0.1)
102
222
  end
103
223
 
104
224
  it 'calls #run on all workers' do
@@ -124,15 +244,35 @@ module Concurrent
124
244
 
125
245
  context '#stop' do
126
246
 
127
- it 'stops the monitor thread' do
128
- Thread.should_receive(:kill).with(anything())
247
+ def mock_thread(status = 'run')
248
+ thread = double('thread')
249
+ thread.should_receive(:status).with(no_args()).and_return(status)
250
+ thread.stub(:join).with(any_args()).and_return(thread)
251
+ Thread.stub(:new).with(no_args()).and_return(thread)
252
+ return thread
253
+ end
254
+
255
+ it 'wakes the monitor thread if sleeping' do
256
+ thread = mock_thread('sleep')
257
+ thread.should_receive(:run).once.with(no_args())
258
+
259
+ subject.run!
260
+ sleep(0.1)
261
+ subject.stop
262
+ end
263
+
264
+ it 'kills the monitor thread if it does not wake up' do
265
+ thread = mock_thread('run')
266
+ thread.should_receive(:join).with(any_args()).and_return(nil)
267
+ thread.should_receive(:kill).with(no_args())
268
+
129
269
  subject.run!
130
270
  sleep(0.1)
131
271
  subject.stop
132
272
  end
133
273
 
134
274
  it 'calls #stop on all workers' do
135
- workers = (1..3).collect{ worker_class.new }
275
+ workers = (1..3).collect{ runner_class.new }
136
276
  workers.each{|worker| subject.add_worker(worker)}
137
277
  # must stub AFTER adding or else #add_worker will reject
138
278
  workers.each{|worker| worker.should_receive(:stop).with(no_args())}
@@ -152,7 +292,7 @@ module Concurrent
152
292
  end
153
293
 
154
294
  it 'unblocks a thread blocked by #run and exits normally' do
155
- supervisor = Supervisor.new(monitor: 0.1)
295
+ supervisor = Supervisor.new(monitor_interval: 0.1)
156
296
  @thread = Thread.new{ sleep(0.5); supervisor.stop }
157
297
  sleep(0.1)
158
298
  lambda {
@@ -185,6 +325,150 @@ module Concurrent
185
325
  end
186
326
  end
187
327
 
328
+ context '#count' do
329
+
330
+ let(:busy_supervisor) do
331
+ supervisor = Supervisor.new(monitor_interval: 60)
332
+ 3.times do
333
+ supervisor.add_worker(sleeper_class.new)
334
+ supervisor.add_worker(stopper_class.new)
335
+ supervisor.add_worker(error_class.new)
336
+ supervisor.add_worker(runner_class.new)
337
+ end
338
+ supervisor
339
+ end
340
+
341
+ let!(:total_count){ 12 }
342
+ let!(:active_count){ 6 }
343
+ let!(:sleeping_count){ 3 }
344
+ let!(:running_count){ 3 }
345
+ let!(:aborting_count){ 3 }
346
+ let!(:stopped_count){ 3 }
347
+ let!(:abend_count){ 3 }
348
+
349
+ after(:each) do
350
+ busy_supervisor.stop
351
+ end
352
+
353
+ it 'returns an immutable WorkerCounts object' do
354
+ counts = subject.count
355
+ counts.should be_a(Supervisor::WorkerCounts)
356
+
357
+ lambda {
358
+ counts.specs += 1
359
+ }.should raise_error(RuntimeError)
360
+ end
361
+
362
+ it 'returns the total worker count as #specs' do
363
+ subject.count.specs.should eq 0
364
+
365
+ 3.times do
366
+ subject.add_worker(worker_class.new, type: :worker)
367
+ subject.add_worker(worker_class.new, type: :supervisor)
368
+ end
369
+
370
+ subject.count.specs.should eq 6
371
+ end
372
+
373
+ it 'returns the count of all children marked as :supervisor as #supervisors' do
374
+ subject.count.supervisors.should eq 0
375
+
376
+ 3.times do
377
+ subject.add_worker(worker_class.new, type: :worker)
378
+ subject.add_worker(worker_class.new, type: :supervisor)
379
+ end
380
+
381
+ subject.count.supervisors.should eq 3
382
+ end
383
+
384
+ it 'returns the count of all children marked as :worker as #workers' do
385
+ subject.count.workers.should eq 0
386
+
387
+ 3.times do
388
+ subject.add_worker(worker_class.new, type: :worker)
389
+ subject.add_worker(worker_class.new, type: :supervisor)
390
+ end
391
+
392
+ subject.count.workers.should eq 3
393
+ end
394
+
395
+ it 'returns the count of all active workers as #active' do
396
+ busy_supervisor.count.active.should eq 0
397
+ busy_supervisor.run!
398
+ sleep(0.5)
399
+
400
+ busy_supervisor.count.active.should eq active_count
401
+ end
402
+
403
+ it 'returns the count of all sleeping workers as #sleeping' do
404
+ busy_supervisor.count.sleeping.should eq 0
405
+ busy_supervisor.run!
406
+ sleep(0.5)
407
+
408
+ busy_supervisor.count.sleeping.should eq sleeping_count
409
+ end
410
+
411
+ it 'returns the count of all running workers as #running' do
412
+ busy_supervisor.count.running.should eq 0
413
+ busy_supervisor.run!
414
+ sleep(0.5)
415
+
416
+ busy_supervisor.count.running.should eq running_count
417
+ end
418
+
419
+ it 'returns the count of all aborting workers as #aborting' do
420
+ busy_supervisor.count.aborting.should eq 0
421
+
422
+ count = Supervisor::WorkerCounts.new(5, 0, 5)
423
+ count.status = %w[aborting run aborting false aborting]
424
+ count.aborting.should eq 3
425
+ end
426
+
427
+ it 'returns the count of all stopped workers as #stopped' do
428
+ busy_supervisor.count.stopped.should eq total_count
429
+ busy_supervisor.run!
430
+ sleep(0.5)
431
+
432
+ busy_supervisor.count.stopped.should eq stopped_count
433
+ end
434
+
435
+ it 'returns the count of all workers terminated by exception as #abend' do
436
+ busy_supervisor.count.abend.should eq 0
437
+ busy_supervisor.run!
438
+ sleep(0.5)
439
+
440
+ busy_supervisor.count.abend.should eq abend_count
441
+ end
442
+ end
443
+
444
+ context '#current_restart_count' do
445
+
446
+ it 'is zero for a new Supervisor' do
447
+ subject.current_restart_count.should eq 0
448
+ end
449
+
450
+ it 'returns the number of worker restarts' do
451
+ worker = error_class.new
452
+ supervisor = Supervisor.new(monitor_interval: 0.1)
453
+ supervisor.add_worker(worker)
454
+ supervisor.run!
455
+ sleep(0.3)
456
+ supervisor.current_restart_count.should > 0
457
+ supervisor.stop
458
+ end
459
+
460
+ it 'resets to zero on #stop' do
461
+ worker = error_class.new
462
+ supervisor = Supervisor.new(monitor_interval: 0.1)
463
+ supervisor.add_worker(worker)
464
+ supervisor.run!
465
+ sleep(0.3)
466
+ supervisor.stop
467
+ sleep(0.1)
468
+ supervisor.current_restart_count.should eq 0
469
+ end
470
+ end
471
+
188
472
  context '#add_worker' do
189
473
 
190
474
  it 'adds the worker when stopped' do
@@ -192,10 +476,14 @@ module Concurrent
192
476
  subject.length.should == 1
193
477
  end
194
478
 
195
- it 'rejects the worker when running' do
479
+ it 'runs the worker when the supervisor is running' do
480
+ worker = worker_class.new
481
+ worker.start_count.to_i.should eq 0
196
482
  subject.run!
197
- subject.add_worker(worker)
198
- subject.length.should == 0
483
+ sleep(0.1)
484
+ subject.add_worker(worker).should be_true
485
+ sleep(0.1)
486
+ worker.start_count.should >= 1
199
487
  end
200
488
 
201
489
  it 'rejects a worker without the :runnable behavior' do
@@ -203,57 +491,633 @@ module Concurrent
203
491
  subject.length.should == 0
204
492
  end
205
493
 
206
- it 'returns true when a worker is accepted' do
207
- subject.add_worker(worker).should be_true
494
+ it 'sets the restart type to the given value' do
495
+ subject.add_worker(worker_class.new, restart: :temporary)
496
+ worker = subject.instance_variable_get(:@workers).first
497
+ worker.restart.should eq :temporary
498
+ end
499
+
500
+ it 'sets the restart type to :permanent when none given' do
501
+ subject.add_worker(worker_class.new)
502
+ worker = subject.instance_variable_get(:@workers).first
503
+ worker.restart.should eq :permanent
504
+ end
505
+
506
+ it 'raises an exception when given an invalid restart type' do
507
+ lambda {
508
+ subject.add_worker(worker_class.new, restart: :bogus)
509
+ }.should raise_error(ArgumentError)
510
+ end
511
+
512
+ it 'sets the child type to the given value' do
513
+ subject.add_worker(worker_class.new, type: :supervisor)
514
+ worker = subject.instance_variable_get(:@workers).first
515
+ worker.type.should eq :supervisor
516
+ end
517
+
518
+ it 'sets the worker type to :worker when none given' do
519
+ subject.add_worker(worker_class.new)
520
+ worker = subject.instance_variable_get(:@workers).first
521
+ worker.type.should eq :worker
522
+ end
523
+
524
+ it 'sets the worker type to :supervisor when #is_a? Supervisor' do
525
+ subject.add_worker(Supervisor.new)
526
+ worker = subject.instance_variable_get(:@workers).first
527
+ worker.type.should eq :supervisor
528
+ end
529
+
530
+ it 'raises an exception when given an invalid restart type' do
531
+ lambda {
532
+ subject.add_worker(worker_class.new, type: :bogus)
533
+ }.should raise_error(ArgumentError)
534
+ end
535
+
536
+ it 'returns an object id when a worker is accepted' do
537
+ worker_id = subject.add_worker(worker)
538
+ worker_id.should be_a(Integer)
539
+ first = subject.instance_variable_get(:@workers).first
540
+ worker_id.should eq first.object_id
208
541
  end
209
542
 
210
- it 'returns false when a worker is not accepted' do
211
- subject.add_worker('bogus worker').should be_false
543
+ it 'returns nil when a worker is not accepted' do
544
+ subject.add_worker('bogus worker').should be_nil
212
545
  end
213
546
  end
214
547
 
215
- context 'supervision' do
548
+ context '#add_workers' do
216
549
 
217
- it 'reruns any worker that stops' do
218
- worker = Class.new(worker_class){
219
- def run() sleep(0.2); end
220
- }.new
550
+ it 'calls #add_worker once for each worker' do
551
+ workers = 5.times.collect{ worker_class.new }
552
+ workers.each do |worker|
553
+ subject.should_receive(:add_worker).once.with(worker, anything())
554
+ end
555
+ subject.add_workers(workers)
556
+ end
221
557
 
222
- supervisor = Supervisor.new(worker: worker, monitor: 0.1)
223
- supervisor.add_worker(worker)
224
- # must stub AFTER adding or else #add_worker will reject
225
- worker.should_receive(:run).with(no_args()).at_least(2).times
558
+ it 'passes the options hash to each #add_worker call' do
559
+ options = {
560
+ restart: :permanent,
561
+ type: :worker
562
+ }
563
+ workers = 5.times.collect{ worker_class.new }
564
+ workers.each do |worker|
565
+ subject.should_receive(:add_worker).once.with(anything(), options)
566
+ end
567
+ subject.add_workers(workers, options)
568
+ end
569
+
570
+ it 'returns an array of object identifiers' do
571
+ workers = 5.times.collect{ worker_class.new }
572
+ context = subject.add_workers(workers)
573
+ context.size.should == 5
574
+ context.each do |wc|
575
+ wc.should be_a(Fixnum)
576
+ end
577
+ end
578
+ end
579
+
580
+ context '#remove_worker' do
581
+
582
+ it 'returns false if the worker is running' do
583
+ id = subject.add_worker(sleeper_class.new)
584
+ subject.run!
585
+ sleep(0.1)
586
+ subject.remove_worker(id).should be_false
587
+ end
588
+
589
+ it 'returns nil if the worker is not found' do
590
+ subject.run!
591
+ sleep(0.1)
592
+ subject.remove_worker(1234).should be_nil
593
+ end
594
+
595
+ it 'returns the worker on success' do
596
+ worker = error_class.new
597
+ supervisor = Supervisor.new(monitor_interval: 60)
598
+ id = supervisor.add_worker(worker)
599
+ supervisor.run!
600
+ sleep(0.1)
601
+ supervisor.remove_worker(id).should eq worker
602
+ supervisor.stop
603
+ end
604
+
605
+ it 'removes the worker from the supervisor on success' do
606
+ worker = error_class.new
607
+ supervisor = Supervisor.new(monitor_interval: 60)
608
+ id = supervisor.add_worker(worker)
609
+ supervisor.length.should == 1
226
610
  supervisor.run!
227
- sleep(1)
611
+ sleep(0.1)
612
+ supervisor.remove_worker(id)
613
+ supervisor.length.should == 0
228
614
  supervisor.stop
229
615
  end
616
+ end
230
617
 
231
- it 'reruns any dead threads' do
232
- worker = Class.new(worker_class){
233
- def run() raise StandardError; end
234
- }.new
618
+ context '#stop_worker' do
619
+
620
+ it 'returns true if the supervisor is not running' do
621
+ worker = worker_class.new
622
+ id = subject.add_worker(worker)
623
+ subject.stop_worker(id).should be_true
624
+ end
625
+
626
+ it 'returns nil if the worker is not found' do
627
+ worker = sleeper_class.new
628
+ id = subject.add_worker(worker)
629
+ subject.run!
630
+ sleep(0.1)
631
+ subject.stop_worker(1234).should be_nil
632
+ end
633
+
634
+ it 'returns true on success' do
635
+ worker = sleeper_class.new
636
+ id = subject.add_worker(worker)
637
+ subject.run!
638
+ sleep(0.1)
639
+ worker.should_receive(:stop).at_least(1).times.and_return(true)
640
+ subject.stop_worker(id).should be_true
641
+ end
642
+
643
+ it 'deletes the worker if it is :temporary' do
644
+ worker = sleeper_class.new
645
+ id = subject.add_worker(worker, restart: :temporary)
646
+ subject.size.should eq 1
647
+ subject.run!
648
+ sleep(0.1)
649
+ subject.stop_worker(id).should be_true
650
+ subject.size.should eq 0
651
+ end
652
+
653
+ it 'does not implicitly restart the worker' do
654
+ supervisor = Supervisor.new(monitor_interval: 0.1)
655
+ worker = runner_class.new
656
+ id = supervisor.add_worker(worker, restart: :permanent)
657
+ supervisor.run!
658
+ sleep(0.1)
659
+ supervisor.stop_worker(id)
660
+ sleep(0.5)
661
+ supervisor.stop
662
+ worker.start_count.should eq 1
663
+ end
664
+ end
665
+
666
+ context '#start_worker' do
667
+
668
+ it 'returns false if the supervisor is not running' do
669
+ worker = worker_class.new
670
+ id = subject.add_worker(worker)
671
+ subject.start_worker(id).should be_false
672
+ end
673
+
674
+ it 'returns nil if the worker is not found' do
675
+ subject.run!
676
+ sleep(0.1)
677
+ subject.start_worker(1234).should be_nil
678
+ end
679
+
680
+ it 'starts the worker if not running' do
681
+ supervisor = Supervisor.new(monitor_interval: 60)
682
+ worker = error_class.new
683
+ id = supervisor.add_worker(worker)
684
+ supervisor.run!
685
+ sleep(0.1)
686
+ supervisor.start_worker(id)
687
+ sleep(0.1)
688
+ worker.start_count.should == 2
689
+ supervisor.stop
690
+ end
691
+
692
+ it 'returns true when the worker is successfully started' do
693
+ supervisor = Supervisor.new(monitor_interval: 60)
694
+ worker = error_class.new
695
+ id = supervisor.add_worker(worker)
696
+ supervisor.run!
697
+ sleep(0.1)
698
+ supervisor.start_worker(id).should be_true
699
+ supervisor.stop
700
+ end
701
+
702
+ it 'returns true if the worker was already running' do
703
+ supervisor = Supervisor.new(monitor_interval: 60)
704
+ worker = sleeper_class.new
705
+ id = supervisor.add_worker(worker)
706
+ supervisor.run!
707
+ sleep(0.1)
708
+ supervisor.start_worker(id).should be_true
709
+ worker.start_count.should == 1
710
+ supervisor.stop
711
+ end
712
+ end
713
+
714
+ context '#restart_worker' do
715
+
716
+ it 'returns false if the supervisor is not running' do
717
+ worker = worker_class.new
718
+ id = subject.add_worker(worker)
719
+ subject.restart_worker(id).should be_false
720
+ end
721
+
722
+ it 'returns nil if the worker is not found' do
723
+ subject.run!
724
+ sleep(0.1)
725
+ subject.restart_worker(1234).should be_nil
726
+ end
727
+
728
+ it 'returns false if the worker is :temporary' do
729
+ worker = worker_class.new
730
+ id = subject.add_worker(worker, restart: :temporary)
731
+ subject.run!
732
+ sleep(0.1)
733
+ subject.restart_worker(id).should be_false
734
+ end
735
+
736
+ it 'stops and then starts a worker that is running' do
737
+ worker = runner_class.new
738
+ id = subject.add_worker(worker)
739
+ subject.run!
740
+ sleep(0.1)
741
+ subject.restart_worker(id)
742
+ sleep(0.1)
743
+ worker.start_count.should == 2
744
+ worker.stop_count.should == 1
745
+ end
746
+
747
+ it 'returns true if the worker is running and is successfully restarted' do
748
+ worker = runner_class.new
749
+ id = subject.add_worker(worker)
750
+ subject.run!
751
+ sleep(0.1)
752
+ subject.restart_worker(id).should be_true
753
+ end
754
+
755
+ it 'starts a worker that is not running' do
756
+ worker = error_class.new
757
+ id = subject.add_worker(worker)
758
+ subject.run!
759
+ sleep(0.1)
760
+ subject.restart_worker(id)
761
+ sleep(0.1)
762
+ worker.start_count.should >= 2
763
+ end
764
+
765
+ it 'returns true if the worker is not running and is successfully started' do
766
+ worker = error_class.new
767
+ id = subject.add_worker(worker)
768
+ subject.run!
769
+ sleep(0.1)
770
+ subject.restart_worker(id).should be_true
771
+ end
772
+ end
773
+
774
+ context 'maximum restart frequency' do
775
+
776
+ context '#exceeded_max_restart_frequency?' do
777
+
778
+ # Normally I am very opposed to testing private methods
779
+ # but this functionality has proven extremely difficult to test.
780
+ # Geting the timing right is almost impossible. This is the
781
+ # best approach I could think of.
782
+
783
+ it 'increments the restart count on every call' do
784
+ subject.send(:exceeded_max_restart_frequency?)
785
+ subject.current_restart_count.should eq 1
786
+
787
+ subject.send(:exceeded_max_restart_frequency?)
788
+ subject.current_restart_count.should eq 2
789
+
790
+ subject.send(:exceeded_max_restart_frequency?)
791
+ subject.current_restart_count.should eq 3
792
+ end
793
+
794
+ it 'returns false when the restart count is lower than :max_restart' do
795
+ supervisor = Supervisor.new(max_restart: 5, max_time: 60)
796
+
797
+ Timecop.freeze do
798
+ 4.times do
799
+ Timecop.travel(5)
800
+ supervisor.send(:exceeded_max_restart_frequency?).should be_false
801
+ end
802
+
803
+ Timecop.travel(5)
804
+ supervisor.send(:exceeded_max_restart_frequency?).should be_true
805
+ end
806
+ end
807
+
808
+ it 'returns false when the restart count is high but the time range is out of scope' do
809
+ supervisor = Supervisor.new(max_restart: 3, max_time: 8)
810
+
811
+ Timecop.freeze do
812
+ 10.times do
813
+ Timecop.travel(5)
814
+ supervisor.send(:exceeded_max_restart_frequency?).should be_false
815
+ end
816
+ end
817
+ end
818
+
819
+ it 'returns true when the restart count is exceeded within the max time range' do
820
+ supervisor = Supervisor.new(max_restart: 2, max_time: 5)
821
+ Timecop.freeze do
822
+ supervisor.send(:exceeded_max_restart_frequency?).should be_false
823
+ Timecop.travel(1)
824
+ supervisor.send(:exceeded_max_restart_frequency?).should be_true
825
+ end
826
+ end
827
+ end
828
+
829
+ context 'restarts when true for strategy' do
830
+
831
+ specify ':one_for_one' do
832
+ supervisor = Supervisor.new(restart_strategy: :one_for_one,
833
+ monitor_interval: 0.1)
834
+ supervisor.add_worker(error_class.new)
835
+ supervisor.should_receive(:exceeded_max_restart_frequency?).once.and_return(true)
836
+ supervisor.run!
837
+ sleep(0.2)
838
+ supervisor.should_not be_running
839
+ end
840
+
841
+ specify ':one_for_all' do
842
+ supervisor = Supervisor.new(restart_strategy: :one_for_all,
843
+ monitor_interval: 0.1)
844
+ supervisor.add_worker(error_class.new)
845
+ supervisor.should_receive(:exceeded_max_restart_frequency?).once.and_return(true)
846
+ supervisor.run!
847
+ sleep(0.2)
848
+ supervisor.should_not be_running
849
+ end
850
+
851
+ specify ':rest_for_one' do
852
+ supervisor = Supervisor.new(restart_strategy: :rest_for_one,
853
+ monitor_interval: 0.1)
854
+ supervisor.add_worker(error_class.new)
855
+ supervisor.should_receive(:exceeded_max_restart_frequency?).once.and_return(true)
856
+ supervisor.run!
857
+ sleep(0.2)
858
+ supervisor.should_not be_running
859
+ end
860
+ end
861
+ end
862
+
863
+ context 'restart strategies' do
864
+
865
+ context ':one_for_one' do
866
+
867
+ it 'restarts any worker that stops' do
868
+
869
+ workers = [
870
+ sleeper_class.new,
871
+ stopper_class.new,
872
+ sleeper_class.new
873
+ ]
874
+
875
+ supervisor = Supervisor.new(strategy: :one_for_one, monitor_interval: 0.1)
876
+ workers.each{|worker| supervisor.add_worker(worker) }
877
+
878
+ supervisor.run!
879
+ sleep(1)
880
+
881
+ workers[0].start_count.should == 1
882
+ workers[1].start_count.should >= 2
883
+ workers[2].start_count.should == 1
884
+
885
+ supervisor.stop
886
+ end
887
+
888
+ it 'restarts any dead threads' do
889
+
890
+ workers = [
891
+ sleeper_class.new,
892
+ error_class.new,
893
+ sleeper_class.new
894
+ ]
895
+
896
+ supervisor = Supervisor.new(strategy: :one_for_one, monitor_interval: 0.1)
897
+ workers.each{|worker| supervisor.add_worker(worker) }
898
+
899
+ supervisor.run!
900
+ sleep(1)
901
+
902
+ workers[0].start_count.should == 1
903
+ workers[1].start_count.should >= 2
904
+ workers[2].start_count.should == 1
905
+
906
+ supervisor.stop
907
+ end
908
+ end
909
+
910
+ context ':one_for_all' do
911
+
912
+ it 'restarts all workers when one stops' do
913
+
914
+ workers = [
915
+ sleeper_class.new,
916
+ stopper_class.new,
917
+ sleeper_class.new
918
+ ]
919
+
920
+ supervisor = Supervisor.new(strategy: :one_for_all, monitor_interval: 0.1)
921
+ workers.each{|worker| supervisor.add_worker(worker) }
922
+
923
+ workers[0].should_receive(:stop).once.with(no_args())
924
+ workers[2].should_receive(:stop).once.with(no_args())
925
+
926
+ supervisor.run!
927
+ sleep(1)
928
+ workers.each{|worker| worker.start_count.should >= 2 }
929
+
930
+ supervisor.stop
931
+ end
932
+
933
+ it 'restarts all workers when one thread dies' do
934
+
935
+ workers = [
936
+ sleeper_class.new,
937
+ error_class.new,
938
+ sleeper_class.new
939
+ ]
940
+
941
+ supervisor = Supervisor.new(strategy: :one_for_all, monitor_interval: 0.1)
942
+ workers.each{|worker| supervisor.add_worker(worker) }
943
+
944
+ workers[0].should_receive(:stop).once.with(no_args())
945
+ workers[2].should_receive(:stop).once.with(no_args())
946
+
947
+ supervisor.run!
948
+ sleep(1)
949
+ workers.each{|worker| worker.start_count.should >= 2 }
950
+
951
+ supervisor.stop
952
+ end
953
+ end
954
+
955
+ context ':rest_for_one' do
956
+
957
+ it 'restarts a stopped worker and all workers added after it' do
958
+
959
+ workers = [
960
+ sleeper_class.new,
961
+ stopper_class.new,
962
+ sleeper_class.new
963
+ ]
964
+
965
+ supervisor = Supervisor.new(strategy: :rest_for_one, monitor_interval: 0.1)
966
+ workers.each{|worker| supervisor.add_worker(worker) }
967
+
968
+ workers[0].should_not_receive(:stop)
969
+ workers[2].should_receive(:stop).once.with(no_args())
970
+
971
+ supervisor.run!
972
+ sleep(1)
973
+
974
+ workers[0].start_count.should == 1
975
+ workers[1].start_count.should >= 2
976
+ workers[2].start_count.should >= 2
977
+
978
+ supervisor.stop
979
+ end
980
+
981
+ it 'restarts a dead worker thread and all workers added after it' do
982
+
983
+ workers = [
984
+ sleeper_class.new,
985
+ error_class.new,
986
+ sleeper_class.new
987
+ ]
988
+
989
+ supervisor = Supervisor.new(strategy: :rest_for_one, monitor_interval: 0.1)
990
+ workers.each{|worker| supervisor.add_worker(worker) }
991
+
992
+ workers[0].should_not_receive(:stop)
993
+ workers[2].should_receive(:stop).once.with(no_args())
994
+
995
+ supervisor.run!
996
+ sleep(1)
997
+
998
+ workers[0].start_count.should == 1
999
+ workers[1].start_count.should >= 2
1000
+ workers[2].start_count.should >= 2
1001
+
1002
+ supervisor.stop
1003
+ end
1004
+ end
1005
+ end
1006
+
1007
+ context 'child restart options' do
1008
+
1009
+ def worker_status(supervisor)
1010
+ worker = supervisor.instance_variable_get(:@workers).first
1011
+ return worker.thread.status
1012
+ end
1013
+
1014
+ specify ':permanent restarts on abend' do
1015
+ worker = error_class.new
1016
+ supervisor = Supervisor.new(monitor_interval: 0.1)
1017
+ supervisor.add_worker(worker, restart: :permanent)
1018
+
1019
+ supervisor.run!
1020
+ sleep(0.5)
1021
+ supervisor.stop
1022
+
1023
+ worker.start_count.should >= 1
1024
+ end
1025
+
1026
+ specify ':permanent restarts on normal stop' do
1027
+ worker = stopper_class.new(0.1)
1028
+ supervisor = Supervisor.new(monitor_interval: 0.1)
1029
+ supervisor.add_worker(worker, restart: :permanent)
1030
+
1031
+ supervisor.run!
1032
+ sleep(0.5)
1033
+ supervisor.stop
1034
+
1035
+ worker.start_count.should >= 1
1036
+ end
1037
+
1038
+ specify ':temporary does not restart on abend' do
1039
+ worker = error_class.new
1040
+ supervisor = Supervisor.new(monitor_interval: 0.1)
1041
+ supervisor.add_worker(worker, restart: :temporary)
1042
+
1043
+ supervisor.run!
1044
+ sleep(0.5)
1045
+ supervisor.stop
1046
+
1047
+ worker.start_count.should eq 1
1048
+ end
1049
+
1050
+ specify ':temporary does not restart on normal stop' do
1051
+ worker = stopper_class.new
1052
+ supervisor = Supervisor.new(monitor_interval: 0.1)
1053
+ supervisor.add_worker(worker, restart: :temporary)
1054
+
1055
+ supervisor.run!
1056
+ sleep(0.5)
1057
+ supervisor.stop
1058
+
1059
+ worker.start_count.should eq 1
1060
+ end
1061
+
1062
+ specify ':temporary is deleted on abend' do
1063
+ worker = error_class.new
1064
+ supervisor = Supervisor.new(monitor_interval: 0.1)
1065
+ supervisor.add_worker(worker, restart: :temporary)
235
1066
 
236
- supervisor = Supervisor.new(worker: worker, monitor: 0.1)
237
- supervisor.add_worker(worker)
238
- # must stub AFTER adding or else #add_worker will reject
239
- worker.should_receive(:run).with(no_args()).at_least(2).times
240
1067
  supervisor.run!
241
- sleep(1)
1068
+ sleep(0.5)
242
1069
  supervisor.stop
1070
+
1071
+ supervisor.size.should eq 0
1072
+ end
1073
+
1074
+ specify ':temporary is deleted on normal stop' do
1075
+ worker = stopper_class.new
1076
+ supervisor = Supervisor.new(monitor_interval: 0.1)
1077
+ supervisor.add_worker(worker, restart: :temporary)
1078
+
1079
+ supervisor.run!
1080
+ sleep(0.5)
1081
+ supervisor.stop
1082
+
1083
+ supervisor.size.should eq 0
1084
+ end
1085
+
1086
+ specify ':transient restarts on abend' do
1087
+ worker = error_class.new
1088
+ supervisor = Supervisor.new(monitor_interval: 0.1)
1089
+ supervisor.add_worker(worker, restart: :transient)
1090
+
1091
+ supervisor.run!
1092
+ sleep(0.5)
1093
+ supervisor.stop
1094
+
1095
+ worker.start_count.should >= 1
1096
+ end
1097
+
1098
+ specify ':transient does not restart on normal stop' do
1099
+ worker = stopper_class.new
1100
+ supervisor = Supervisor.new(monitor_interval: 0.1)
1101
+ supervisor.add_worker(worker, restart: :transient)
1102
+
1103
+ supervisor.run!
1104
+ sleep(0.5)
1105
+ supervisor.stop
1106
+
1107
+ worker.start_count.should eq 1
243
1108
  end
244
1109
  end
245
1110
 
246
- context 'supervisor tree' do
1111
+ context 'supervision tree' do
247
1112
 
248
1113
  specify do
249
- s1 = Supervisor.new(monitor: 0.1)
250
- s2 = Supervisor.new(monitor: 0.1)
251
- s3 = Supervisor.new(monitor: 0.1)
1114
+ s1 = Supervisor.new(monitor_interval: 0.1)
1115
+ s2 = Supervisor.new(monitor_interval: 0.1)
1116
+ s3 = Supervisor.new(monitor_interval: 0.1)
252
1117
 
253
- workers = (1..3).collect{ worker_class.new }
1118
+ workers = (1..3).collect{ sleeper_class.new }
254
1119
  workers.each{|worker| s3.add_worker(worker)}
255
- # must stub AFTER adding or else #add_worker will reject
256
- workers.each{|worker| worker.should_receive(:run).at_least(1).times.with(no_args())}
1120
+
257
1121
  workers.each{|worker| worker.should_receive(:stop).at_least(1).times.with(no_args())}
258
1122
 
259
1123
  s1.add_worker(s2)
@@ -261,6 +1125,7 @@ module Concurrent
261
1125
 
262
1126
  s1.run!
263
1127
  sleep(0.2)
1128
+
264
1129
  s1.stop
265
1130
  sleep(0.2)
266
1131
  end