dat-worker-pool 0.6.0 → 0.6.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.
@@ -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"