dat-worker-pool 0.6.0 → 0.6.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA512:
3
+ data.tar.gz: c0a52d7d5c533e9d71623775137e77131dcc4eceae71e4e55085f0248983442518e394fbca0aeaf02be04f6bf7484dd139ad84f4c7e165c194057d662d7689f2
4
+ metadata.gz: 8c5250174e3f695c2ad78ab377e1c29973dd37295abe65cd94d3b2a266ce0171087ab1262316004591882de23497a0e24649f8c1b0c7d75ec3cbb31fdc589938
5
+ SHA1:
6
+ data.tar.gz: 4a978550e606a83e897ca45f4eaf5e8c8e945622
7
+ metadata.gz: 43f2dd81c2d52ce758a87a403381536899526c58
data/Gemfile CHANGED
@@ -2,5 +2,4 @@ source "https://rubygems.org"
2
2
 
3
3
  gemspec
4
4
 
5
- gem 'rake'
6
5
  gem 'pry', "~> 0.9.0"
File without changes
@@ -8,8 +8,8 @@ Gem::Specification.new do |gem|
8
8
  gem.version = DatWorkerPool::VERSION
9
9
  gem.authors = ["Collin Redding", "Kelly Redding"]
10
10
  gem.email = ["collin.redding@me.com", "kelly@kellyredding.com"]
11
- gem.description = "A simple thread pool for processing generic 'work'"
12
11
  gem.summary = "A simple thread pool for processing generic 'work'"
12
+ gem.description = "A simple thread pool for processing generic 'work'"
13
13
  gem.homepage = "http://github.com/redding/dat-worker-pool"
14
14
  gem.license = 'MIT'
15
15
 
@@ -18,8 +18,8 @@ Gem::Specification.new do |gem|
18
18
  gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
19
19
  gem.require_paths = ["lib"]
20
20
 
21
- gem.add_dependency("SystemTimer", ["~> 1.2"])
21
+ gem.add_development_dependency("assert", ["~> 2.16.1"])
22
22
 
23
- gem.add_development_dependency("assert", ["~> 2.15"])
23
+ gem.add_dependency("much-timeout", ["~> 0.1.0"])
24
24
 
25
25
  end
@@ -42,7 +42,7 @@ class DatWorkerPool
42
42
  end
43
43
 
44
44
  def shutdown(timeout = nil)
45
- @runner.shutdown(timeout, caller)
45
+ @runner.shutdown(timeout)
46
46
  end
47
47
 
48
48
  def add_work(work_item)
@@ -1,5 +1,5 @@
1
+ require 'much-timeout'
1
2
  require 'set'
2
- require 'system_timer'
3
3
  require 'dat-worker-pool'
4
4
  require 'dat-worker-pool/locked_object'
5
5
 
@@ -46,7 +46,7 @@ class DatWorkerPool
46
46
  # instead of letting ruby kill the threads when the process exits;
47
47
  # non-timeout errors will be re-raised so they can be caught and handled (or
48
48
  # shown when the process exits)
49
- def shutdown(timeout = nil, backtrace = nil)
49
+ def shutdown(timeout = nil)
50
50
  log do
51
51
  timeout_message = timeout ? "#{timeout} second(s)" : "none"
52
52
  "Shutting down worker pool (timeout: #{timeout_message})"
@@ -54,15 +54,21 @@ class DatWorkerPool
54
54
  begin
55
55
  @workers.with_lock{ |m, ws| ws.each(&:dwp_signal_shutdown) }
56
56
  @queue.dwp_signal_shutdown
57
- OptionalTimeout.new(timeout) do
58
- @queue.dwp_shutdown
59
- wait_for_workers_to_shutdown
57
+ MuchTimeout.just_optional_timeout(timeout, {
58
+ :do => proc{
59
+ @queue.dwp_shutdown
60
+ wait_for_workers_to_shutdown
61
+ },
62
+ :on_timeout => proc{
63
+ e = ShutdownError.new("Timed out shutting down (#{timeout} seconds).")
64
+ force_workers_to_shutdown(e, timeout)
65
+ }
66
+ }) do
60
67
  end
61
- rescue StandardError => exception
62
- force_workers_to_shutdown(exception, timeout, backtrace)
63
- raise exception
64
- rescue TimeoutInterruptError => exception
65
- force_workers_to_shutdown(exception, timeout, backtrace)
68
+ rescue StandardError => err
69
+ e = ShutdownError.new("Errored while shutting down: #{err.inspect}")
70
+ force_workers_to_shutdown(e, timeout)
71
+ raise err
66
72
  end
67
73
  log{ "Finished shutting down" }
68
74
  end
@@ -116,16 +122,15 @@ class DatWorkerPool
116
122
  end
117
123
  end
118
124
 
119
- # use an until loop instead of each to join all the workers, while we are
125
+ # use a while loop instead of each to join all the workers, while we are
120
126
  # joining a worker a different worker can shutdown and remove itself from
121
127
  # the `@workers` array; rescue when joining the workers, ruby will raise any
122
128
  # exceptions that aren't handled by a thread when its joined, this ensures
123
129
  # if the hard shutdown is raised and not rescued (for example, in the
124
130
  # workers ensure), then it won't cause the forced shutdown to end
125
131
  # prematurely
126
- def force_workers_to_shutdown(orig_exception, timeout, backtrace)
132
+ def force_workers_to_shutdown(error, timeout)
127
133
  log{ "Forcing #{@workers.size} workers to shutdown" }
128
- error = build_forced_shutdown_error(orig_exception, timeout, backtrace)
129
134
  while !(worker = @workers.first).nil?
130
135
  worker.dwp_raise(error)
131
136
  begin
@@ -149,34 +154,6 @@ class DatWorkerPool
149
154
  @workers.delete(worker)
150
155
  end
151
156
 
152
- def build_forced_shutdown_error(orig_exception, timeout, backtrace)
153
- if orig_exception.kind_of?(TimeoutInterruptError)
154
- ShutdownError.new("Timed out shutting down (#{timeout} seconds).").tap do |e|
155
- e.set_backtrace(backtrace) if backtrace
156
- end
157
- else
158
- ShutdownError.new("Errored while shutting down: #{orig_exception.inspect}").tap do |e|
159
- e.set_backtrace(orig_exception.backtrace)
160
- end
161
- end
162
- end
163
-
164
- # this needs to be an `Interrupt` to be sure we don't accidentally catch it
165
- # when rescueing exceptions; in the shutdown methods we rescue any errors
166
- # from `worker.join`, this will also rescue the timeout error if its a
167
- # standard error and will keep it from doing a forced shutdown
168
- TimeoutInterruptError = Class.new(Interrupt)
169
-
170
- module OptionalTimeout
171
- def self.new(seconds, &block)
172
- if seconds
173
- SystemTimer.timeout(seconds, TimeoutInterruptError, &block)
174
- else
175
- block.call
176
- end
177
- end
178
- end
179
-
180
157
  class LoggerProxy < Struct.new(:logger)
181
158
  def runner_log(&message_block)
182
159
  self.logger.debug("[DWP] #{message_block.call}")
@@ -1,3 +1,3 @@
1
1
  class DatWorkerPool
2
- VERSION = "0.6.0"
2
+ VERSION = "0.6.1"
3
3
  end
@@ -20,4 +20,11 @@ JOIN_SECONDS = 0.001
20
20
 
21
21
  require 'test/support/factory'
22
22
 
23
- # TODO: put test helpers here...
23
+ # 1.8.7 backfills
24
+
25
+ # Array#sample
26
+ if !(a = Array.new).respond_to?(:sample) && a.respond_to?(:choice)
27
+ class Array
28
+ alias_method :sample, :choice
29
+ end
30
+ end
@@ -0,0 +1,71 @@
1
+ require 'much-timeout'
2
+ require 'dat-worker-pool/worker'
3
+
4
+ module SignalTestWorker
5
+ def self.included(klass)
6
+ klass.class_eval{ include DatWorkerPool::Worker }
7
+ end
8
+
9
+ private
10
+
11
+ def signal_test_suite_thread
12
+ params[:signal_worker_mutex].synchronize do
13
+ params[:signal_worker_cond_var].signal
14
+ end
15
+ end
16
+
17
+ module TestHelpers
18
+
19
+ def self.included(klass)
20
+ klass.class_eval do
21
+ setup do
22
+ # at least 2 workers, up to 4
23
+ @num_workers = Factory.integer(3) + 1
24
+
25
+ @signal_worker_mutex = Mutex.new
26
+ @signal_worker_cond_var = ConditionVariable.new
27
+
28
+ @worker_params = {
29
+ :signal_worker_mutex => @signal_worker_mutex,
30
+ :signal_worker_cond_var => @signal_worker_cond_var
31
+ }
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ # this could loop forever so ensure it doesn't by using a timeout
39
+ def wait_for_workers(&block)
40
+ MuchTimeout.just_timeout(1, {
41
+ :do => proc{
42
+ while !block.call do
43
+ @signal_worker_mutex.synchronize do
44
+ @signal_worker_cond_var.wait(@signal_worker_mutex)
45
+ end
46
+ end
47
+ },
48
+ :on_timeout => proc{
49
+ raise "timed out waiting for workers" unless block.call
50
+ }
51
+ })
52
+ end
53
+
54
+ def wait_for_workers_to_become_available
55
+ wait_for_workers{ subject.available_worker_count == @num_workers }
56
+ end
57
+
58
+ def wait_for_workers_to_become_unavailable
59
+ wait_for_workers{ subject.available_worker_count == 0 }
60
+ end
61
+
62
+ def wait_for_a_worker_to_become_available
63
+ wait_for_workers{ subject.available_worker_count != 0 }
64
+ end
65
+
66
+ def wait_for_a_worker_to_become_unavailable
67
+ wait_for_workers{ subject.available_worker_count != @num_workers }
68
+ end
69
+
70
+ end
71
+ end
@@ -1,58 +1,24 @@
1
1
  require 'assert'
2
2
  require 'dat-worker-pool'
3
3
 
4
- require 'timeout'
5
4
  require 'dat-worker-pool/locked_object'
6
5
  require 'dat-worker-pool/worker'
6
+ require 'test/support/signal_test_worker'
7
7
 
8
8
  class DatWorkerPool
9
9
 
10
10
  class SystemTests < Assert::Context
11
+ include SignalTestWorker::TestHelpers
12
+
11
13
  desc "DatWorkerPool"
12
- setup do
13
- # at least 2 workers, up to 4
14
- @num_workers = Factory.integer(3) + 1
15
- @mutex = Mutex.new
16
- @cond_var = ConditionVariable.new
17
- @worker_params = {
18
- :mutex => @mutex,
19
- :cond_var => @cond_var
20
- }
21
- end
22
14
  subject{ @worker_pool }
23
15
 
24
- # this could loop forever so ensure it doesn't by using a timeout; use
25
- # timeout instead of system timer because system timer is paranoid about a
26
- # deadlock even though its intended to prevent the deadlock because it times
27
- # out the block
28
- def wait_for_workers(&block)
29
- Timeout.timeout(1) do
30
- @mutex.synchronize{ @cond_var.wait(@mutex) } while !block.call
31
- end
32
- end
33
-
34
- def wait_for_workers_to_become_available
35
- wait_for_workers{ @worker_pool.available_worker_count == @num_workers }
36
- end
37
-
38
- def wait_for_workers_to_become_unavailable
39
- wait_for_workers{ @worker_pool.available_worker_count == 0 }
40
- end
41
-
42
- def wait_for_a_worker_to_become_available
43
- wait_for_workers{ @worker_pool.available_worker_count != 0 }
44
- end
45
-
46
- def wait_for_a_worker_to_become_unavailable
47
- wait_for_workers{ @worker_pool.available_worker_count != @num_workers }
48
- end
49
-
50
16
  end
51
17
 
52
18
  class StartAddProcessAndShutdownTests < SystemTests
53
19
  setup do
54
20
  @worker_class = Class.new do
55
- include SystemTestWorker
21
+ include SignalTestWorker
56
22
  def work!(number)
57
23
  params[:results].push(number * 100)
58
24
  signal_test_suite_thread
@@ -88,17 +54,20 @@ class DatWorkerPool
88
54
  class WorkerAvailabilityTests < SystemTests
89
55
  setup do
90
56
  @worker_class = Class.new do
91
- include SystemTestWorker
57
+ include SignalTestWorker
92
58
  on_available{ signal_test_suite_thread }
93
59
  on_unavailable{ signal_test_suite_thread }
94
60
 
95
61
  # this allows controlling how many workers are available and unavailable
96
62
  # the worker will be unavailable until we signal it
97
63
  def work!(work_item)
64
+ params[:working].add(self)
98
65
  mutex, cond_var = work_item
99
66
  mutex.synchronize{ cond_var.wait(mutex) }
67
+ params[:working].remove(self)
100
68
  end
101
69
  end
70
+ @working = LockedSet.new
102
71
  @work_mutex = Mutex.new
103
72
  @work_cond_var = ConditionVariable.new
104
73
  @work_item = [@work_mutex, @work_cond_var]
@@ -106,7 +75,7 @@ class DatWorkerPool
106
75
  @worker_pool = DatWorkerPool.new(@worker_class, {
107
76
  :num_workers => @num_workers,
108
77
  :logger => TEST_LOGGER,
109
- :worker_params => @worker_params
78
+ :worker_params => @worker_params.merge(:working => @working)
110
79
  })
111
80
  @worker_pool.start
112
81
  end
@@ -131,7 +100,7 @@ class DatWorkerPool
131
100
  # make the rest of the workers unavailable
132
101
  (@num_workers - 1).times{ subject.add_work(@work_item) }
133
102
 
134
- wait_for_workers_to_become_unavailable
103
+ wait_for_workers{ @working.size == @num_workers }
135
104
  assert_equal 0, subject.available_worker_count
136
105
  assert_false subject.worker_available?
137
106
 
@@ -155,7 +124,7 @@ class DatWorkerPool
155
124
  class WorkerCallbackTests < SystemTests
156
125
  setup do
157
126
  @worker_class = Class.new do
158
- include SystemTestWorker
127
+ include SignalTestWorker
159
128
 
160
129
  on_start{ params[:callbacks_called][:on_start] = true }
161
130
  on_shutdown{ params[:callbacks_called][:on_shutdown] = true }
@@ -269,7 +238,7 @@ class DatWorkerPool
269
238
  class ShutdownSystemTests < SystemTests
270
239
  setup do
271
240
  @worker_class = Class.new do
272
- include SystemTestWorker
241
+ include SignalTestWorker
273
242
  on_available{ signal_test_suite_thread }
274
243
  on_unavailable{ signal_test_suite_thread }
275
244
 
@@ -280,12 +249,15 @@ class DatWorkerPool
280
249
  # this allows controlling how long a worker takes to finish processing
281
250
  # the work item
282
251
  def work!(work_item)
252
+ params[:working].add(self)
283
253
  params[:work_mutex].synchronize do
284
254
  params[:work_cond_var].wait(params[:work_mutex])
285
255
  end
286
256
  params[:finished].push(work_item)
257
+ params[:working].remove(self)
287
258
  end
288
259
  end
260
+ @working = LockedSet.new
289
261
  @work_mutex = Mutex.new
290
262
  @work_cond_var = ConditionVariable.new
291
263
  @finished = LockedArray.new
@@ -295,6 +267,7 @@ class DatWorkerPool
295
267
  :num_workers => @num_workers,
296
268
  :logger => TEST_LOGGER,
297
269
  :worker_params => @worker_params.merge({
270
+ :working => @working,
298
271
  :work_mutex => @work_mutex,
299
272
  :work_cond_var => @work_cond_var,
300
273
  :finished => @finished,
@@ -308,7 +281,7 @@ class DatWorkerPool
308
281
  # add 1 more work item than we have workers to handle it
309
282
  @work_items = (@num_workers + 1).times.map{ Factory.string }
310
283
  @work_items.each{ |wi| @worker_pool.add_work(wi) }
311
- wait_for_workers_to_become_unavailable
284
+ wait_for_workers{ @working.size == @num_workers }
312
285
  end
313
286
  teardown do
314
287
  # ensure we wakeup any workers still stuck in their `work!`
@@ -351,17 +324,12 @@ class DatWorkerPool
351
324
  assert_false shutdown_thread.alive?
352
325
  end
353
326
 
354
- should "allow any work that has been picked up to finish processing " \
327
+ should "not allow any work that has been picked up to finish processing " \
355
328
  "when forced to shutdown because it timed out" do
356
329
  assert_true @finished.empty?
357
330
  assert_true @errored.empty?
358
331
 
359
- # start the shutdown in a thread, this will hang until the timeout
360
- # finishes; this is required otherwise system timer will think we are
361
- # triggering a deadlock (it's not a deadlock because of the timeout)
362
- shutdown_thread = Thread.new{ subject.shutdown(0) }
363
- shutdown_thread.join(JOIN_SECONDS)
364
- assert_equal 'sleep', shutdown_thread.status
332
+ subject.shutdown(0)
365
333
 
366
334
  # wait for the workers to get forced to exit
367
335
  wait_for_workers{ @errored.size == @num_workers }
@@ -378,22 +346,8 @@ class DatWorkerPool
378
346
  assert_instance_of ShutdownError, exception
379
347
  assert_includes work_item, @work_items[0, @num_workers]
380
348
  end
381
-
382
- # ensure the shutdown exits
383
- shutdown_thread.join
384
- assert_false shutdown_thread.alive?
385
- end
386
-
387
- end
388
-
389
- module SystemTestWorker
390
- def self.included(klass)
391
- klass.class_eval{ include DatWorkerPool::Worker }
392
349
  end
393
350
 
394
- def signal_test_suite_thread
395
- params[:mutex].synchronize{ params[:cond_var].signal }
396
- end
397
351
  end
398
352
 
399
353
  end
@@ -1,7 +1,6 @@
1
1
  require 'assert'
2
2
  require 'dat-worker-pool'
3
3
 
4
- require 'system_timer'
5
4
  require 'dat-worker-pool/queue'
6
5
  require 'dat-worker-pool/runner'
7
6
  require 'dat-worker-pool/worker'
@@ -97,8 +96,6 @@ class DatWorkerPool
97
96
  subject.shutdown
98
97
  assert_true @runner_spy.shutdown_called
99
98
  assert_nil @runner_spy.shutdown_timeout
100
- exp = "test/unit/dat-worker-pool_tests.rb"
101
- assert_match exp, @runner_spy.shutdown_backtrace.first
102
99
 
103
100
  timeout = Factory.integer
104
101
  subject.shutdown(timeout)
@@ -118,7 +115,7 @@ class DatWorkerPool
118
115
  should "raise an argument error if given an invalid number of workers" do
119
116
  assert_raises(ArgumentError) do
120
117
  @worker_pool_class.new(@worker_class, {
121
- :num_workers => [0, (Factory.integer * -1)].choice
118
+ :num_workers => [0, (Factory.integer * -1)].sample
122
119
  })
123
120
  end
124
121
  end
@@ -172,14 +169,13 @@ class DatWorkerPool
172
169
  class RunnerSpy < DatWorkerPool::Runner
173
170
  attr_accessor :args
174
171
  attr_reader :start_called, :shutdown_called
175
- attr_reader :shutdown_timeout, :shutdown_backtrace
172
+ attr_reader :shutdown_timeout
176
173
 
177
174
  def initialize
178
175
  super({})
179
176
  @start_called = false
180
177
  @shutdown_called = false
181
178
  @shutdown_timeout = nil
182
- @shutdown_backtrace = nil
183
179
  end
184
180
 
185
181
  def start
@@ -187,11 +183,10 @@ class DatWorkerPool
187
183
  @start_called = true
188
184
  end
189
185
 
190
- def shutdown(timeout, backtrace)
186
+ def shutdown(timeout)
191
187
  @args[:queue].dwp_shutdown
192
- @shutdown_called = true
193
- @shutdown_timeout = timeout
194
- @shutdown_backtrace = backtrace
188
+ @shutdown_called = true
189
+ @shutdown_timeout = timeout
195
190
  end
196
191
  end
197
192
 
@@ -162,6 +162,7 @@ class DatWorkerPool::DefaultQueue
162
162
 
163
163
  value = Factory.string
164
164
  @queue.dwp_push(value)
165
+ subject.join(JOIN_SECONDS)
165
166
 
166
167
  assert_not subject.alive?
167
168
  assert_equal value, subject['popped_value']
@@ -176,6 +177,7 @@ class DatWorkerPool::DefaultQueue
176
177
  # sleeps and grabs the lock and work item before the thread being woken
177
178
  # up
178
179
  @work_items.with_lock{ @cond_var_spy.signal }
180
+ subject.join(JOIN_SECONDS)
179
181
 
180
182
  assert_equal 'sleep', subject.status
181
183
  assert_equal 2, @cond_var_spy.wait_call_count
@@ -186,6 +188,7 @@ class DatWorkerPool::DefaultQueue
186
188
  assert_equal 1, @cond_var_spy.wait_call_count
187
189
 
188
190
  @queue.dwp_shutdown
191
+ subject.join(JOIN_SECONDS)
189
192
 
190
193
  assert_not subject.alive?
191
194
  assert_equal 1, @cond_var_spy.wait_call_count
@@ -207,6 +210,7 @@ class DatWorkerPool::DefaultQueue
207
210
  # accesses the array directly and pushes an item on it
208
211
  @work_items.push(Factory.string)
209
212
  @queue.dwp_shutdown
213
+ subject.join(JOIN_SECONDS)
210
214
 
211
215
  assert_not subject.alive?
212
216
  assert_nil subject['popped_value']
@@ -1,11 +1,15 @@
1
1
  require 'assert'
2
2
  require 'dat-worker-pool/runner'
3
3
 
4
+ require 'much-timeout'
4
5
  require 'dat-worker-pool/default_queue'
6
+ require 'test/support/signal_test_worker'
5
7
 
6
8
  class DatWorkerPool::Runner
7
9
 
8
10
  class UnitTests < Assert::Context
11
+ include SignalTestWorker::TestHelpers
12
+
9
13
  desc "DatWorkerPool::Runner"
10
14
  setup do
11
15
  @runner_class = DatWorkerPool::Runner
@@ -17,12 +21,13 @@ class DatWorkerPool::Runner
17
21
  class InitTests < UnitTests
18
22
  desc "when init"
19
23
  setup do
20
- # at least 2 workers, up to 4
21
- @num_workers = Factory.integer(3) + 1
22
- @logger = TEST_LOGGER || Logger.new("/dev/null")
23
- @queue = DatWorkerPool::DefaultQueue.new
24
- @worker_class = TestWorker
25
- @worker_params = { Factory.string => Factory.string }
24
+ @logger = TEST_LOGGER || Logger.new("/dev/null")
25
+ @queue = DatWorkerPool::DefaultQueue.new
26
+ @worker_class = TestWorker
27
+
28
+ @worker_params.merge!({
29
+ Factory.string => Factory.string
30
+ })
26
31
 
27
32
  @workers = DatWorkerPool::LockedArray.new
28
33
  Assert.stub(DatWorkerPool::LockedArray, :new){ @workers }
@@ -73,24 +78,6 @@ class DatWorkerPool::Runner
73
78
  assert_equal @workers.values, subject.workers
74
79
  end
75
80
 
76
- should "start its queue when its started" do
77
- assert_false @queue.running?
78
- subject.start
79
- assert_true @queue.running?
80
- end
81
-
82
- should "build and add workers when its started" do
83
- subject.start
84
-
85
- assert_equal @num_workers, subject.workers.size
86
- subject.workers.each_with_index do |worker, n|
87
- assert_equal subject, worker.dwp_runner
88
- assert_equal @queue, worker.dwp_queue
89
- assert_equal n + 1, worker.dwp_number
90
- assert_true worker.dwp_running?
91
- end
92
- end
93
-
94
81
  should "allow making workers available/unavailable" do
95
82
  worker = @worker_class.new(@runner, @queue, Factory.integer(10))
96
83
 
@@ -142,25 +129,45 @@ class DatWorkerPool::Runner
142
129
 
143
130
  end
144
131
 
145
- class ShutdownSetupTests < InitTests
146
- desc "and started and shutdown"
132
+ class StartTests < InitTests
133
+ desc "and started"
134
+
135
+ should "start its queue" do
136
+ assert_false @queue.running?
137
+ subject.start
138
+ assert_true @queue.running?
139
+ end
140
+
141
+ should "build and add workers" do
142
+ subject.start
143
+ wait_for_workers_to_become_available
144
+
145
+ assert_equal @num_workers, subject.workers.size
146
+ subject.workers.each_with_index do |worker, n|
147
+ assert_equal subject, worker.dwp_runner
148
+ assert_equal @queue, worker.dwp_queue
149
+ assert_equal n + 1, worker.dwp_number
150
+ assert_true worker.dwp_running?
151
+ end
152
+ end
147
153
 
148
154
  end
149
155
 
150
- class ShutdownTests < ShutdownSetupTests
156
+ class ShutdownTests < StartTests
157
+ desc "and shutdown"
151
158
  setup do
152
159
  @timeout_seconds = nil
153
160
  @optional_timeout_called = false
154
161
  # this acts as a spy but also keeps the shutdown from ever timing out
155
- Assert.stub(OptionalTimeout, :new) do |secs, &block|
162
+ Assert.stub(MuchTimeout, :just_optional_timeout) do |secs, args|
156
163
  @timeout_seconds = secs
157
164
  @optional_timeout_called = true
158
- block.call
165
+ args[:do].call
159
166
  end
160
167
 
161
- @options[:worker_class] = ShutdownSpyWorker
162
- @runner = @runner_class.new(@options)
163
168
  @runner.start
169
+ wait_for_workers_to_become_available
170
+
164
171
  # we need a reference to the workers, the runners workers will get removed
165
172
  # as they shutdown
166
173
  @running_workers = @runner.workers.dup
@@ -182,7 +189,10 @@ class DatWorkerPool::Runner
182
189
  @running_workers.each do |worker|
183
190
  assert_false worker.dwp_shutdown?
184
191
  end
192
+
185
193
  subject.shutdown(Factory.boolean ? Factory.integer : nil)
194
+ wait_for_workers_to_become_unavailable
195
+
186
196
  @running_workers.each do |worker|
187
197
  assert_true worker.dwp_shutdown?
188
198
  end
@@ -198,15 +208,21 @@ class DatWorkerPool::Runner
198
208
  @running_workers.each do |worker|
199
209
  assert_false worker.join_called
200
210
  end
211
+
201
212
  subject.shutdown(Factory.boolean ? Factory.integer : nil)
213
+ wait_for_workers_to_become_unavailable
214
+
202
215
  @running_workers.each do |worker|
203
216
  assert_true worker.join_called
204
217
  end
205
218
  end
206
219
 
207
220
  should "join all workers even if one raises an error when joined" do
208
- @running_workers.choice.join_error = Factory.exception
221
+ @running_workers.sample.join_error = Factory.exception
222
+
209
223
  subject.shutdown(Factory.boolean ? Factory.integer : nil)
224
+ wait_for_workers_to_become_unavailable
225
+
210
226
  @running_workers.each do |worker|
211
227
  assert_true worker.join_called
212
228
  end
@@ -214,7 +230,10 @@ class DatWorkerPool::Runner
214
230
 
215
231
  should "remove workers as they finish" do
216
232
  assert_false subject.workers.empty?
233
+
217
234
  subject.shutdown(Factory.boolean ? Factory.integer : nil)
235
+ wait_for_workers_to_become_unavailable
236
+
218
237
  assert_true subject.workers.empty?
219
238
  end
220
239
 
@@ -223,13 +242,21 @@ class DatWorkerPool::Runner
223
242
 
224
243
  assert_false subject.workers.empty?
225
244
  assert_false @available_workers_spy.empty?
245
+
226
246
  subject.shutdown(Factory.boolean ? Factory.integer : nil)
247
+ wait_for_workers_to_become_unavailable
248
+
227
249
  assert_true subject.workers.empty?
228
250
  assert_true @available_workers_spy.empty?
229
251
  end
230
252
 
231
253
  should "force its workers to shutdown if a timeout error occurs" do
232
- Assert.stub(OptionalTimeout, :new){ raise TimeoutInterruptError }
254
+ Assert.stub(MuchTimeout, :just_optional_timeout) do |secs, args|
255
+ args[:on_timeout].call # force an immediate timeout
256
+ end
257
+
258
+ @num_workers.times{ @queue.dwp_push(:hang) }
259
+ wait_for_workers_to_become_unavailable
233
260
  subject.shutdown(Factory.integer)
234
261
 
235
262
  @running_workers.each do |worker|
@@ -244,6 +271,9 @@ class DatWorkerPool::Runner
244
271
  queue_exception = Factory.exception
245
272
  Assert.stub(@queue, :dwp_shutdown){ raise queue_exception }
246
273
 
274
+ @num_workers.times{ @queue.dwp_push(:hang) }
275
+ wait_for_workers_to_become_unavailable
276
+
247
277
  caught_exception = nil
248
278
  begin
249
279
  subject.shutdown(Factory.integer)
@@ -260,15 +290,24 @@ class DatWorkerPool::Runner
260
290
  end
261
291
 
262
292
  should "force shutdown all of its workers even if one raises an error when joining" do
263
- Assert.stub(OptionalTimeout, :new){ raise TimeoutInterruptError }
264
- error_class = Factory.boolean ? DatWorkerPool::ShutdownError : RuntimeError
265
- @running_workers.choice.join_error = Factory.exception(error_class)
293
+ Assert.stub(MuchTimeout, :just_optional_timeout) do |secs, args|
294
+ args[:on_timeout].call # force an immediate timeout
295
+ end
296
+ error_class = Factory.boolean ? DatWorkerPool::ShutdownError : RuntimeError
297
+ error_worker = @running_workers.sample
298
+ error_worker.join_error = Factory.exception(error_class)
299
+
300
+ @num_workers.times{ @queue.dwp_push(:hang) }
301
+ wait_for_workers_to_become_unavailable
266
302
  subject.shutdown(Factory.boolean ? Factory.integer : nil)
267
303
 
268
- @running_workers.each do |worker|
304
+ (@running_workers - [error_worker]).each do |worker|
269
305
  assert_instance_of DatWorkerPool::ShutdownError, worker.raised_error
270
306
  assert_true worker.join_called
271
307
  end
308
+ assert_instance_of error_class, error_worker.raised_error
309
+ assert_true error_worker.join_called
310
+
272
311
  assert_true subject.workers.empty?
273
312
  assert_true @available_workers_spy.empty?
274
313
  end
@@ -318,48 +357,46 @@ class DatWorkerPool::Runner
318
357
  end
319
358
 
320
359
  class TestWorker
321
- include DatWorkerPool::Worker
360
+ include SignalTestWorker
361
+
362
+ attr_reader :join_called
363
+ attr_accessor :join_error, :raised_error
322
364
 
323
365
  # for testing what is passed to the worker
324
366
  attr_reader :dwp_runner, :dwp_queue
325
- end
326
367
 
327
- FakeWorker = Struct.new(:dwp_number)
368
+ on_available{ signal_test_suite_thread }
369
+ on_unavailable{ signal_test_suite_thread }
328
370
 
329
- class ShutdownSpyWorker < TestWorker
330
- attr_reader :join_called
331
- attr_accessor :join_error, :raised_error
371
+ on_shutdown{ raise self.join_error if self.join_error }
332
372
 
333
- # this pauses the shutdown so we can test that join or raise are called
334
- # depending if we are doing a standard or forced shutdown; otherwise the
335
- # worker threads can exit before join or raise ever gets called on them and
336
- # then there is nothing to test
337
- on_shutdown{ wait_for_join_or_raise }
373
+ on_error{ |e, wi| self.raised_error = e }
338
374
 
339
375
  def initialize(*args)
340
376
  super
341
- @mutex = Mutex.new
342
- @cond_var = ConditionVariable.new
343
- @join_called = false
344
- @join_error = nil
345
- @raised_error = nil
346
- end
377
+ @join_called = false
378
+ @join_error = nil
347
379
 
348
- def dwp_join(*args)
349
- @join_called = true
350
- raise @join_error if @join_error
351
- @mutex.synchronize{ @cond_var.broadcast }
380
+ @hang_mutex = Mutex.new
381
+ @hang_cond_var = ConditionVariable.new
352
382
  end
353
383
 
354
- def dwp_raise(error)
355
- @raised_error = error
384
+ def work!(work_item)
385
+ case(work_item)
386
+ when :hang
387
+ MuchTimeout.timeout(1) do
388
+ @hang_mutex.synchronize{ @hang_cond_var.wait(@hang_mutex) }
389
+ end
390
+ end
356
391
  end
357
392
 
358
- private
359
-
360
- def wait_for_join_or_raise
361
- @mutex.synchronize{ @cond_var.wait(@mutex) }
393
+ def dwp_join(*args)
394
+ @join_called = true
395
+ super
362
396
  end
397
+
363
398
  end
364
399
 
400
+ FakeWorker = Struct.new(:dwp_number)
401
+
365
402
  end
@@ -1,7 +1,7 @@
1
1
  require 'assert'
2
2
  require 'dat-worker-pool/worker'
3
3
 
4
- require 'system_timer'
4
+ require 'much-timeout'
5
5
  require 'dat-worker-pool/default_queue'
6
6
  require 'dat-worker-pool/runner'
7
7
  require 'test/support/thread_spies'
@@ -125,9 +125,18 @@ module DatWorkerPool::Worker
125
125
  })
126
126
  @number = Factory.integer(10)
127
127
 
128
- @thread_spy = ThreadSpy.new
128
+ # we only want to stub the first `Thread.new` call, not all calls;
129
+ # stubbing all threads can generate unexpected behavior and specifically
130
+ # causes `MuchTimeout` to not work correctly
131
+ @worker_thread_built = false
132
+ @thread_spy = ThreadSpy.new
129
133
  Assert.stub(Thread, :new) do |&block|
130
- @thread_spy.tap{ |s| s.block = block }
134
+ if !@worker_thread_built
135
+ @worker_thread_built = true
136
+ @thread_spy.tap{ |s| s.block = block }
137
+ else
138
+ Assert.stub_send(Thread, :new, &block)
139
+ end
131
140
  end
132
141
 
133
142
  @available_worker = nil
@@ -245,7 +254,7 @@ module DatWorkerPool::Worker
245
254
 
246
255
  should "make itself available/unavailable and run callbacks if it errors" do
247
256
  # these are the only errors that could interfere with it
248
- error_method = [:on_unavailable_error, :work_error].choice
257
+ error_method = [:on_unavailable_error, :work_error].sample
249
258
  exception = Factory.exception
250
259
  subject.send("#{error_method}=", exception)
251
260
 
@@ -295,7 +304,7 @@ module DatWorkerPool::Worker
295
304
 
296
305
  should "run its on-error callbacks if it errors while starting" do
297
306
  exception = Factory.exception
298
- error_method = [:on_start_error, :on_available_error].choice
307
+ error_method = [:on_start_error, :on_available_error].sample
299
308
  subject.send("#{error_method}=", exception)
300
309
 
301
310
  subject.dwp_start
@@ -307,7 +316,7 @@ module DatWorkerPool::Worker
307
316
 
308
317
  should "stop its thread if it errors while starting" do
309
318
  exception = Factory.exception
310
- error_method = [:on_start_error, :on_available_error].choice
319
+ error_method = [:on_start_error, :on_available_error].sample
311
320
  subject.send("#{error_method}=", exception)
312
321
 
313
322
  subject.dwp_start
@@ -319,7 +328,7 @@ module DatWorkerPool::Worker
319
328
 
320
329
  should "run its on-error callbacks if it errors while shutting down" do
321
330
  exception = Factory.exception
322
- error_method = [:on_shutdown_error, :on_unavailable_error].choice
331
+ error_method = [:on_shutdown_error, :on_unavailable_error].sample
323
332
  subject.send("#{error_method}=", exception)
324
333
 
325
334
  subject.dwp_start
@@ -401,7 +410,7 @@ module DatWorkerPool::Worker
401
410
  subject.dwp_start
402
411
  wait_for_worker_to_be_available
403
412
 
404
- error_method = ERROR_METHODS.reject{ |e| e == :on_available_error }.choice
413
+ error_method = ERROR_METHODS.reject{ |e| e == :on_available_error }.sample
405
414
  subject.send("#{error_method}=", exception)
406
415
  work_item = Factory.string
407
416
  @queue.dwp_push(work_item)
@@ -544,17 +553,14 @@ module DatWorkerPool::Worker
544
553
  :after_work_error
545
554
  ].freeze
546
555
  def setup_work_loop_to_raise_exception(exception)
547
- error_method = ERROR_METHODS.choice
556
+ error_method = ERROR_METHODS.sample
548
557
  @worker.send("#{error_method}=", exception)
549
558
  error_method
550
559
  end
551
560
 
552
- # this could loop forever so ensure it doesn't by using a timeout; use
553
- # timeout instead of system timer because system timer is paranoid about a
554
- # deadlock even though its intended to prevent the deadlock because it times
555
- # out the block
561
+ # this could loop forever so ensure it doesn't by using a timeout
556
562
  def wait_for_worker(&block)
557
- Timeout.timeout(1) do
563
+ MuchTimeout.timeout(1) do
558
564
  @mutex.synchronize{ @cond_var.wait(@mutex) } while !block.call
559
565
  end
560
566
  end
@@ -587,7 +593,7 @@ module DatWorkerPool::Worker
587
593
 
588
594
  def wait_for_worker_thread_to_stop
589
595
  return unless @worker.dwp_thread_alive?
590
- Timeout.timeout(1){ @worker.dwp_join }
596
+ MuchTimeout.timeout(1){ @worker.dwp_join }
591
597
  end
592
598
 
593
599
  # this is needed because errors will be re-raised when the thread is joined
metadata CHANGED
@@ -1,13 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dat-worker-pool
3
3
  version: !ruby/object:Gem::Version
4
- hash: 7
5
- prerelease:
6
- segments:
7
- - 0
8
- - 6
9
- - 0
10
- version: 0.6.0
4
+ version: 0.6.1
11
5
  platform: ruby
12
6
  authors:
13
7
  - Collin Redding
@@ -16,38 +10,28 @@ autorequire:
16
10
  bindir: bin
17
11
  cert_chain: []
18
12
 
19
- date: 2015-11-24 00:00:00 Z
13
+ date: 2016-06-09 00:00:00 Z
20
14
  dependencies:
21
15
  - !ruby/object:Gem::Dependency
16
+ name: assert
17
+ prerelease: false
22
18
  requirement: &id001 !ruby/object:Gem::Requirement
23
- none: false
24
19
  requirements:
25
20
  - - ~>
26
21
  - !ruby/object:Gem::Version
27
- hash: 11
28
- segments:
29
- - 1
30
- - 2
31
- version: "1.2"
32
- type: :runtime
33
- name: SystemTimer
22
+ version: 2.16.1
23
+ type: :development
34
24
  version_requirements: *id001
35
- prerelease: false
36
25
  - !ruby/object:Gem::Dependency
26
+ name: much-timeout
27
+ prerelease: false
37
28
  requirement: &id002 !ruby/object:Gem::Requirement
38
- none: false
39
29
  requirements:
40
30
  - - ~>
41
31
  - !ruby/object:Gem::Version
42
- hash: 29
43
- segments:
44
- - 2
45
- - 15
46
- version: "2.15"
47
- type: :development
48
- name: assert
32
+ version: 0.1.0
33
+ type: :runtime
49
34
  version_requirements: *id002
50
- prerelease: false
51
35
  description: A simple thread pool for processing generic 'work'
52
36
  email:
53
37
  - collin.redding@me.com
@@ -61,9 +45,8 @@ extra_rdoc_files: []
61
45
  files:
62
46
  - .gitignore
63
47
  - Gemfile
64
- - LICENSE.txt
48
+ - LICENSE
65
49
  - README.md
66
- - Rakefile
67
50
  - bench/report.rb
68
51
  - bench/report.txt
69
52
  - dat-worker-pool.gemspec
@@ -78,6 +61,7 @@ files:
78
61
  - log/.gitkeep
79
62
  - test/helper.rb
80
63
  - test/support/factory.rb
64
+ - test/support/signal_test_worker.rb
81
65
  - test/support/thread_spies.rb
82
66
  - test/system/dat-worker-pool_tests.rb
83
67
  - test/unit/dat-worker-pool_tests.rb
@@ -91,39 +75,33 @@ files:
91
75
  homepage: http://github.com/redding/dat-worker-pool
92
76
  licenses:
93
77
  - MIT
78
+ metadata: {}
79
+
94
80
  post_install_message:
95
81
  rdoc_options: []
96
82
 
97
83
  require_paths:
98
84
  - lib
99
85
  required_ruby_version: !ruby/object:Gem::Requirement
100
- none: false
101
86
  requirements:
102
- - - ">="
87
+ - &id003
88
+ - ">="
103
89
  - !ruby/object:Gem::Version
104
- hash: 3
105
- segments:
106
- - 0
107
90
  version: "0"
108
91
  required_rubygems_version: !ruby/object:Gem::Requirement
109
- none: false
110
92
  requirements:
111
- - - ">="
112
- - !ruby/object:Gem::Version
113
- hash: 3
114
- segments:
115
- - 0
116
- version: "0"
93
+ - *id003
117
94
  requirements: []
118
95
 
119
96
  rubyforge_project:
120
- rubygems_version: 1.8.25
97
+ rubygems_version: 2.6.4
121
98
  signing_key:
122
- specification_version: 3
99
+ specification_version: 4
123
100
  summary: A simple thread pool for processing generic 'work'
124
101
  test_files:
125
102
  - test/helper.rb
126
103
  - test/support/factory.rb
104
+ - test/support/signal_test_worker.rb
127
105
  - test/support/thread_spies.rb
128
106
  - test/system/dat-worker-pool_tests.rb
129
107
  - test/unit/dat-worker-pool_tests.rb
data/Rakefile DELETED
@@ -1 +0,0 @@
1
- require "bundler/gem_tasks"