dat-worker-pool 0.5.0 → 0.6.0
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.
- data/Gemfile +1 -1
- data/bench/report.rb +130 -0
- data/bench/report.txt +7 -0
- data/dat-worker-pool.gemspec +1 -1
- data/lib/dat-worker-pool.rb +38 -187
- data/lib/dat-worker-pool/default_queue.rb +60 -0
- data/lib/dat-worker-pool/locked_object.rb +60 -0
- data/lib/dat-worker-pool/queue.rb +71 -48
- data/lib/dat-worker-pool/runner.rb +196 -0
- data/lib/dat-worker-pool/version.rb +1 -1
- data/lib/dat-worker-pool/worker.rb +251 -72
- data/lib/dat-worker-pool/worker_pool_spy.rb +39 -53
- data/test/helper.rb +13 -0
- data/test/support/factory.rb +15 -0
- data/test/support/thread_spies.rb +83 -0
- data/test/system/dat-worker-pool_tests.rb +399 -0
- data/test/unit/dat-worker-pool_tests.rb +132 -255
- data/test/unit/default_queue_tests.rb +217 -0
- data/test/unit/locked_object_tests.rb +260 -0
- data/test/unit/queue_tests.rb +95 -72
- data/test/unit/runner_tests.rb +365 -0
- data/test/unit/worker_pool_spy_tests.rb +95 -102
- data/test/unit/worker_tests.rb +819 -153
- metadata +27 -12
- data/test/system/use_worker_pool_tests.rb +0 -34
@@ -1,80 +1,66 @@
|
|
1
|
+
require 'dat-worker-pool/worker'
|
2
|
+
|
1
3
|
class DatWorkerPool
|
2
4
|
|
3
5
|
class WorkerPoolSpy
|
4
6
|
|
5
|
-
attr_reader :
|
6
|
-
attr_reader :
|
7
|
-
attr_reader :start_called
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
7
|
+
attr_reader :logger, :queue
|
8
|
+
attr_reader :options, :num_workers, :worker_class, :worker_params
|
9
|
+
attr_reader :start_called, :shutdown_called, :shutdown_timeout
|
10
|
+
attr_accessor :available_worker_count, :worker_available
|
11
|
+
|
12
|
+
def initialize(worker_class, options = nil)
|
13
|
+
@worker_class = worker_class
|
14
|
+
if !@worker_class.kind_of?(Class) || !@worker_class.include?(DatWorkerPool::Worker)
|
15
|
+
raise ArgumentError, "worker class must include `#{DatWorkerPool::Worker}`"
|
16
|
+
end
|
17
|
+
|
18
|
+
@options = options || {}
|
19
|
+
@num_workers = (@options[:num_workers] || DEFAULT_NUM_WORKERS).to_i
|
20
|
+
if @num_workers && @num_workers < MIN_WORKERS
|
21
|
+
raise ArgumentError, "number of workers must be at least #{MIN_WORKERS}"
|
22
|
+
end
|
15
23
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
@work_proc = block
|
24
|
+
@queue = @options[:queue] || begin
|
25
|
+
require 'dat-worker-pool/default_queue'
|
26
|
+
DatWorkerPool::DefaultQueue.new
|
27
|
+
end
|
21
28
|
|
22
|
-
@
|
23
|
-
@
|
24
|
-
@start_called = false
|
25
|
-
@shutdown_called = false
|
26
|
-
@shutdown_timeout = nil
|
29
|
+
@logger = @options[:logger]
|
30
|
+
@worker_params = @options[:worker_params]
|
27
31
|
|
28
|
-
@
|
29
|
-
@
|
30
|
-
@
|
31
|
-
@
|
32
|
-
@
|
33
|
-
@on_worker_sleep_callbacks = []
|
34
|
-
@on_worker_wakeup_callbacks = []
|
35
|
-
@before_work_callbacks = []
|
36
|
-
@after_work_callbacks = []
|
32
|
+
@available_worker_count = 0
|
33
|
+
@worker_available = false
|
34
|
+
@start_called = false
|
35
|
+
@shutdown_called = false
|
36
|
+
@shutdown_timeout = nil
|
37
37
|
end
|
38
38
|
|
39
39
|
def start
|
40
40
|
@start_called = true
|
41
|
+
@queue.dwp_start
|
41
42
|
end
|
42
43
|
|
43
44
|
def shutdown(timeout = nil)
|
44
|
-
@shutdown_called
|
45
|
+
@shutdown_called = true
|
45
46
|
@shutdown_timeout = timeout
|
47
|
+
@queue.dwp_shutdown
|
46
48
|
end
|
47
49
|
|
48
|
-
def add_work(
|
49
|
-
return
|
50
|
-
@
|
51
|
-
@on_queue_push_callbacks.each(&:call)
|
50
|
+
def add_work(work_item)
|
51
|
+
return if work_item.nil?
|
52
|
+
@queue.dwp_push(work_item)
|
52
53
|
end
|
54
|
+
alias :push :add_work
|
53
55
|
|
54
|
-
def
|
55
|
-
|
56
|
-
@on_queue_pop_callbacks.each(&:call)
|
57
|
-
work
|
58
|
-
end
|
59
|
-
|
60
|
-
def queue_empty?
|
61
|
-
@work_items.empty?
|
56
|
+
def work_items
|
57
|
+
@queue.work_items
|
62
58
|
end
|
63
59
|
|
64
60
|
def worker_available?
|
65
|
-
|
61
|
+
!!@worker_available
|
66
62
|
end
|
67
63
|
|
68
|
-
def on_queue_pop(&block); @on_queue_pop_callbacks << block; end
|
69
|
-
def on_queue_push(&block); @on_queue_push_callbacks << block; end
|
70
|
-
def on_worker_error(&block); @on_worker_error_callbacks << block; end
|
71
|
-
def on_worker_start(&block); @on_worker_start_callbacks << block; end
|
72
|
-
def on_worker_shutdown(&block); @on_worker_shutdown_callbacks << block; end
|
73
|
-
def on_worker_sleep(&block); @on_worker_sleep_callbacks << block; end
|
74
|
-
def on_worker_wakeup(&block); @on_worker_wakeup_callbacks << block; end
|
75
|
-
def before_work(&block); @before_work_callbacks << block; end
|
76
|
-
def after_work(&block); @after_work_callbacks << block; end
|
77
|
-
|
78
64
|
end
|
79
65
|
|
80
66
|
end
|
data/test/helper.rb
CHANGED
@@ -7,4 +7,17 @@ $LOAD_PATH.unshift(File.expand_path("../..", __FILE__))
|
|
7
7
|
# require pry for debugging (`binding.pry`)
|
8
8
|
require 'pry'
|
9
9
|
|
10
|
+
require 'pathname'
|
11
|
+
ROOT_PATH = Pathname.new(File.expand_path('../..', __FILE__))
|
12
|
+
|
13
|
+
require 'logger'
|
14
|
+
TEST_LOGGER = if ENV['DEBUG']
|
15
|
+
# don't show datetime in the logs
|
16
|
+
Logger.new(ROOT_PATH.join("log/test.log")).tap{ |l| l.datetime_format = '' }
|
17
|
+
end
|
18
|
+
|
19
|
+
JOIN_SECONDS = 0.001
|
20
|
+
|
21
|
+
require 'test/support/factory'
|
22
|
+
|
10
23
|
# TODO: put test helpers here...
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'assert/factory'
|
2
|
+
|
3
|
+
module Factory
|
4
|
+
extend Assert::Factory
|
5
|
+
|
6
|
+
def self.exception(klass = nil, message = nil)
|
7
|
+
klass ||= StandardError
|
8
|
+
message ||= Factory.text
|
9
|
+
exception = nil
|
10
|
+
begin; raise(klass, message); rescue klass => exception; end
|
11
|
+
exception.set_backtrace(nil) if Factory.boolean
|
12
|
+
exception
|
13
|
+
end
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,83 @@
|
|
1
|
+
require 'thread'
|
2
|
+
|
3
|
+
class ThreadSpy
|
4
|
+
attr_reader :join_seconds, :join_called
|
5
|
+
attr_reader :raised_exception
|
6
|
+
|
7
|
+
def initialize(&block)
|
8
|
+
@block = block
|
9
|
+
@mutex = Mutex.new
|
10
|
+
@cond_var = ConditionVariable.new
|
11
|
+
|
12
|
+
@join_seconds = nil
|
13
|
+
@join_called = false
|
14
|
+
@raised_exception = nil
|
15
|
+
|
16
|
+
@thread = Thread.new do
|
17
|
+
@mutex.synchronize{ @cond_var.wait(@mutex) } if @block.nil?
|
18
|
+
@block.call
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def block=(new_block)
|
23
|
+
@block = new_block
|
24
|
+
@mutex.synchronize{ @cond_var.signal }
|
25
|
+
end
|
26
|
+
|
27
|
+
def join(seconds = nil)
|
28
|
+
@join_seconds = seconds
|
29
|
+
@join_called = true
|
30
|
+
@thread.join(seconds)
|
31
|
+
end
|
32
|
+
|
33
|
+
def raise(exception)
|
34
|
+
@raised_exception = exception
|
35
|
+
@thread.raise(exception)
|
36
|
+
end
|
37
|
+
|
38
|
+
def status; @thread.status; end
|
39
|
+
def alive?; @thread.alive?; end
|
40
|
+
end
|
41
|
+
|
42
|
+
class MutexSpy < Mutex
|
43
|
+
attr_accessor :synchronize_called
|
44
|
+
|
45
|
+
def initialize
|
46
|
+
@synchronize_called = false
|
47
|
+
super
|
48
|
+
end
|
49
|
+
|
50
|
+
def synchronize
|
51
|
+
@synchronize_called = true
|
52
|
+
super
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class ConditionVariableSpy < ConditionVariable
|
57
|
+
attr_reader :signal_called, :broadcast_called
|
58
|
+
attr_reader :wait_called_on, :wait_call_count
|
59
|
+
|
60
|
+
def initialize
|
61
|
+
@signal_called = false
|
62
|
+
@broadcast_called = false
|
63
|
+
@wait_called_on = nil
|
64
|
+
@wait_call_count = 0
|
65
|
+
super
|
66
|
+
end
|
67
|
+
|
68
|
+
def signal
|
69
|
+
@signal_called = true
|
70
|
+
super
|
71
|
+
end
|
72
|
+
|
73
|
+
def broadcast
|
74
|
+
@broadcast_called = true
|
75
|
+
super
|
76
|
+
end
|
77
|
+
|
78
|
+
def wait(mutex)
|
79
|
+
@wait_called_on = mutex
|
80
|
+
@wait_call_count += 1
|
81
|
+
super
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,399 @@
|
|
1
|
+
require 'assert'
|
2
|
+
require 'dat-worker-pool'
|
3
|
+
|
4
|
+
require 'timeout'
|
5
|
+
require 'dat-worker-pool/locked_object'
|
6
|
+
require 'dat-worker-pool/worker'
|
7
|
+
|
8
|
+
class DatWorkerPool
|
9
|
+
|
10
|
+
class SystemTests < Assert::Context
|
11
|
+
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
|
+
subject{ @worker_pool }
|
23
|
+
|
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
|
+
end
|
51
|
+
|
52
|
+
class StartAddProcessAndShutdownTests < SystemTests
|
53
|
+
setup do
|
54
|
+
@worker_class = Class.new do
|
55
|
+
include SystemTestWorker
|
56
|
+
def work!(number)
|
57
|
+
params[:results].push(number * 100)
|
58
|
+
signal_test_suite_thread
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# at least 5 work items, up to 10
|
63
|
+
@work_items = (Factory.integer(5) + 5).times.map{ Factory.integer(10) }
|
64
|
+
@results = LockedArray.new
|
65
|
+
|
66
|
+
@worker_pool = DatWorkerPool.new(@worker_class, {
|
67
|
+
:num_workers => @num_workers,
|
68
|
+
:logger => TEST_LOGGER,
|
69
|
+
:worker_params => @worker_params.merge(:results => @results)
|
70
|
+
})
|
71
|
+
end
|
72
|
+
|
73
|
+
should "be able to start, add work, process it and shutdown" do
|
74
|
+
subject.start
|
75
|
+
@work_items.each{ |work_item| subject.add_work(work_item) }
|
76
|
+
|
77
|
+
wait_for_workers{ @results.size == @work_items.size }
|
78
|
+
subject.shutdown(0)
|
79
|
+
|
80
|
+
assert_equal @work_items.size, @results.size
|
81
|
+
@work_items.each do |number|
|
82
|
+
assert_includes number * 100, @results.values
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
end
|
87
|
+
|
88
|
+
class WorkerAvailabilityTests < SystemTests
|
89
|
+
setup do
|
90
|
+
@worker_class = Class.new do
|
91
|
+
include SystemTestWorker
|
92
|
+
on_available{ signal_test_suite_thread }
|
93
|
+
on_unavailable{ signal_test_suite_thread }
|
94
|
+
|
95
|
+
# this allows controlling how many workers are available and unavailable
|
96
|
+
# the worker will be unavailable until we signal it
|
97
|
+
def work!(work_item)
|
98
|
+
mutex, cond_var = work_item
|
99
|
+
mutex.synchronize{ cond_var.wait(mutex) }
|
100
|
+
end
|
101
|
+
end
|
102
|
+
@work_mutex = Mutex.new
|
103
|
+
@work_cond_var = ConditionVariable.new
|
104
|
+
@work_item = [@work_mutex, @work_cond_var]
|
105
|
+
|
106
|
+
@worker_pool = DatWorkerPool.new(@worker_class, {
|
107
|
+
:num_workers => @num_workers,
|
108
|
+
:logger => TEST_LOGGER,
|
109
|
+
:worker_params => @worker_params
|
110
|
+
})
|
111
|
+
@worker_pool.start
|
112
|
+
end
|
113
|
+
teardown do
|
114
|
+
# ensure we wakeup any workers still stuck in their `work!`
|
115
|
+
@work_mutex.synchronize{ @work_cond_var.broadcast }
|
116
|
+
@worker_pool.shutdown(0)
|
117
|
+
end
|
118
|
+
|
119
|
+
should "know if and how many workers are available" do
|
120
|
+
wait_for_workers_to_become_available
|
121
|
+
assert_equal @num_workers, subject.available_worker_count
|
122
|
+
assert_true subject.worker_available?
|
123
|
+
|
124
|
+
# make one worker unavailable
|
125
|
+
subject.add_work(@work_item)
|
126
|
+
|
127
|
+
wait_for_a_worker_to_become_unavailable
|
128
|
+
assert_equal @num_workers - 1, subject.available_worker_count
|
129
|
+
assert_true subject.worker_available?
|
130
|
+
|
131
|
+
# make the rest of the workers unavailable
|
132
|
+
(@num_workers - 1).times{ subject.add_work(@work_item) }
|
133
|
+
|
134
|
+
wait_for_workers_to_become_unavailable
|
135
|
+
assert_equal 0, subject.available_worker_count
|
136
|
+
assert_false subject.worker_available?
|
137
|
+
|
138
|
+
# make one worker available
|
139
|
+
@work_mutex.synchronize{ @work_cond_var.signal }
|
140
|
+
|
141
|
+
wait_for_a_worker_to_become_available
|
142
|
+
assert_equal 1, subject.available_worker_count
|
143
|
+
assert_true subject.worker_available?
|
144
|
+
|
145
|
+
# make the rest of the workers available
|
146
|
+
@work_mutex.synchronize{ @work_cond_var.broadcast }
|
147
|
+
|
148
|
+
wait_for_workers_to_become_available
|
149
|
+
assert_equal @num_workers, subject.available_worker_count
|
150
|
+
assert_true subject.worker_available?
|
151
|
+
end
|
152
|
+
|
153
|
+
end
|
154
|
+
|
155
|
+
class WorkerCallbackTests < SystemTests
|
156
|
+
setup do
|
157
|
+
@worker_class = Class.new do
|
158
|
+
include SystemTestWorker
|
159
|
+
|
160
|
+
on_start{ params[:callbacks_called][:on_start] = true }
|
161
|
+
on_shutdown{ params[:callbacks_called][:on_shutdown] = true }
|
162
|
+
|
163
|
+
on_available{ params[:callbacks_called][:on_available] = true }
|
164
|
+
on_unavailable{ params[:callbacks_called][:on_unavailable] = true }
|
165
|
+
|
166
|
+
on_error{ |e, wi| params[:callbacks_called][:on_error] = true }
|
167
|
+
|
168
|
+
before_work{ |wi| params[:callbacks_called][:before_work] = true }
|
169
|
+
after_work{ |wi| params[:callbacks_called][:after_work] = true }
|
170
|
+
|
171
|
+
on_available{ signal_test_suite_thread }
|
172
|
+
on_unavailable{ signal_test_suite_thread }
|
173
|
+
|
174
|
+
def work!(work_item)
|
175
|
+
params[:finished].push(work_item)
|
176
|
+
signal_test_suite_thread
|
177
|
+
raise if work_item == 'error'
|
178
|
+
end
|
179
|
+
end
|
180
|
+
# use one worker to simplify; we only need to see that one worker runs its
|
181
|
+
# callbacks
|
182
|
+
@num_workers = 1
|
183
|
+
@callbacks_called = {}
|
184
|
+
@finished = LockedArray.new
|
185
|
+
|
186
|
+
@worker_pool = DatWorkerPool.new(@worker_class, {
|
187
|
+
:num_workers => @num_workers,
|
188
|
+
:logger => TEST_LOGGER,
|
189
|
+
:worker_params => @worker_params.merge({
|
190
|
+
:callbacks_called => @callbacks_called,
|
191
|
+
:finished => @finished
|
192
|
+
})
|
193
|
+
})
|
194
|
+
end
|
195
|
+
teardown do
|
196
|
+
@worker_pool.shutdown(0)
|
197
|
+
end
|
198
|
+
|
199
|
+
should "run worker callbacks when started" do
|
200
|
+
assert_nil @callbacks_called[:on_start]
|
201
|
+
assert_nil @callbacks_called[:on_available]
|
202
|
+
|
203
|
+
subject.start
|
204
|
+
wait_for_workers_to_become_available
|
205
|
+
|
206
|
+
assert_true @callbacks_called[:on_start]
|
207
|
+
assert_true @callbacks_called[:on_available]
|
208
|
+
end
|
209
|
+
|
210
|
+
should "run worker callbacks when work is pushed" do
|
211
|
+
subject.start
|
212
|
+
wait_for_workers_to_become_available
|
213
|
+
@callbacks_called.delete(:on_available)
|
214
|
+
|
215
|
+
assert_nil @callbacks_called[:on_unavailable]
|
216
|
+
assert_nil @callbacks_called[:before_work]
|
217
|
+
assert_nil @callbacks_called[:after_work]
|
218
|
+
|
219
|
+
subject.add_work(Factory.string)
|
220
|
+
wait_for_workers do
|
221
|
+
@finished.size == @num_workers &&
|
222
|
+
subject.available_worker_count == @num_workers
|
223
|
+
end
|
224
|
+
|
225
|
+
assert_true @callbacks_called[:on_unavailable]
|
226
|
+
assert_true @callbacks_called[:before_work]
|
227
|
+
assert_true @callbacks_called[:after_work]
|
228
|
+
assert_true @callbacks_called[:on_available]
|
229
|
+
end
|
230
|
+
|
231
|
+
should "run worker callbacks when it errors" do
|
232
|
+
subject.start
|
233
|
+
wait_for_workers_to_become_available
|
234
|
+
@callbacks_called.delete(:on_available)
|
235
|
+
|
236
|
+
assert_nil @callbacks_called[:on_unavailable]
|
237
|
+
assert_nil @callbacks_called[:before_work]
|
238
|
+
assert_nil @callbacks_called[:on_error]
|
239
|
+
assert_nil @callbacks_called[:after_work]
|
240
|
+
|
241
|
+
subject.add_work('error')
|
242
|
+
wait_for_workers do
|
243
|
+
@finished.size == @num_workers &&
|
244
|
+
subject.available_worker_count == @num_workers
|
245
|
+
end
|
246
|
+
|
247
|
+
assert_true @callbacks_called[:on_unavailable]
|
248
|
+
assert_true @callbacks_called[:before_work]
|
249
|
+
assert_true @callbacks_called[:on_error]
|
250
|
+
assert_nil @callbacks_called[:after_work]
|
251
|
+
assert_true @callbacks_called[:on_available]
|
252
|
+
end
|
253
|
+
|
254
|
+
should "run callbacks when its shutdown" do
|
255
|
+
subject.start
|
256
|
+
wait_for_workers_to_become_available
|
257
|
+
|
258
|
+
assert_nil @callbacks_called[:on_unavailable]
|
259
|
+
assert_nil @callbacks_called[:on_shutdown]
|
260
|
+
|
261
|
+
subject.shutdown(0)
|
262
|
+
|
263
|
+
assert_true @callbacks_called[:on_unavailable]
|
264
|
+
assert_true @callbacks_called[:on_shutdown]
|
265
|
+
end
|
266
|
+
|
267
|
+
end
|
268
|
+
|
269
|
+
class ShutdownSystemTests < SystemTests
|
270
|
+
setup do
|
271
|
+
@worker_class = Class.new do
|
272
|
+
include SystemTestWorker
|
273
|
+
on_available{ signal_test_suite_thread }
|
274
|
+
on_unavailable{ signal_test_suite_thread }
|
275
|
+
|
276
|
+
on_error do |error, wi|
|
277
|
+
params[:errored].push([error, wi])
|
278
|
+
end
|
279
|
+
|
280
|
+
# this allows controlling how long a worker takes to finish processing
|
281
|
+
# the work item
|
282
|
+
def work!(work_item)
|
283
|
+
params[:work_mutex].synchronize do
|
284
|
+
params[:work_cond_var].wait(params[:work_mutex])
|
285
|
+
end
|
286
|
+
params[:finished].push(work_item)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
@work_mutex = Mutex.new
|
290
|
+
@work_cond_var = ConditionVariable.new
|
291
|
+
@finished = LockedArray.new
|
292
|
+
@errored = LockedArray.new
|
293
|
+
|
294
|
+
@worker_pool = DatWorkerPool.new(@worker_class, {
|
295
|
+
:num_workers => @num_workers,
|
296
|
+
:logger => TEST_LOGGER,
|
297
|
+
:worker_params => @worker_params.merge({
|
298
|
+
:work_mutex => @work_mutex,
|
299
|
+
:work_cond_var => @work_cond_var,
|
300
|
+
:finished => @finished,
|
301
|
+
:errored => @errored
|
302
|
+
})
|
303
|
+
})
|
304
|
+
|
305
|
+
@worker_pool.start
|
306
|
+
wait_for_workers_to_become_available
|
307
|
+
|
308
|
+
# add 1 more work item than we have workers to handle it
|
309
|
+
@work_items = (@num_workers + 1).times.map{ Factory.string }
|
310
|
+
@work_items.each{ |wi| @worker_pool.add_work(wi) }
|
311
|
+
wait_for_workers_to_become_unavailable
|
312
|
+
end
|
313
|
+
teardown do
|
314
|
+
# ensure we wakeup any workers still stuck in their `work!`
|
315
|
+
@work_mutex.synchronize{ @work_cond_var.broadcast }
|
316
|
+
@worker_pool.shutdown(0)
|
317
|
+
end
|
318
|
+
|
319
|
+
should "allow any work that has been picked up to finish processing " \
|
320
|
+
"when shutdown without a timeout" do
|
321
|
+
assert_true @finished.empty?
|
322
|
+
assert_true @errored.empty?
|
323
|
+
|
324
|
+
# start the shutdown in a thread, this will hang it indefinitely because
|
325
|
+
# it has no timeout and the workers will never exit on their own (because
|
326
|
+
# they are waiting to be signaled by the cond var)
|
327
|
+
shutdown_thread = Thread.new{ subject.shutdown }
|
328
|
+
shutdown_thread.join(JOIN_SECONDS)
|
329
|
+
assert_equal 'sleep', shutdown_thread.status
|
330
|
+
|
331
|
+
# allow the workers to finish working
|
332
|
+
@work_mutex.synchronize{ @work_cond_var.broadcast }
|
333
|
+
wait_for_workers{ @finished.size == @num_workers }
|
334
|
+
|
335
|
+
# ensure we finished what we started processing; assert the size and
|
336
|
+
# includes because we can't ensure the order that the work items are
|
337
|
+
# finished
|
338
|
+
assert_equal @num_workers, @finished.size
|
339
|
+
@finished.values.each do |work_item|
|
340
|
+
assert_includes work_item, @work_items[0, @num_workers]
|
341
|
+
end
|
342
|
+
|
343
|
+
# ensure it didn't pick up anymore work
|
344
|
+
assert_equal [@work_items.last], subject.queue.work_items
|
345
|
+
|
346
|
+
# ensure nothing errored
|
347
|
+
assert_true @errored.empty?
|
348
|
+
|
349
|
+
# ensure the shutdown exits
|
350
|
+
shutdown_thread.join
|
351
|
+
assert_false shutdown_thread.alive?
|
352
|
+
end
|
353
|
+
|
354
|
+
should "allow any work that has been picked up to finish processing " \
|
355
|
+
"when forced to shutdown because it timed out" do
|
356
|
+
assert_true @finished.empty?
|
357
|
+
assert_true @errored.empty?
|
358
|
+
|
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
|
365
|
+
|
366
|
+
# wait for the workers to get forced to exit
|
367
|
+
wait_for_workers{ @errored.size == @num_workers }
|
368
|
+
|
369
|
+
# ensure it didn't finish what it started processing
|
370
|
+
assert_true @finished.empty?
|
371
|
+
|
372
|
+
# ensure it didn't pick up anymore work
|
373
|
+
assert_equal [@work_items.last], subject.queue.work_items
|
374
|
+
|
375
|
+
# ensure all the work it picked up was reported to its on-error callback
|
376
|
+
assert_equal @num_workers, @errored.size
|
377
|
+
@errored.values.each do |(exception, work_item)|
|
378
|
+
assert_instance_of ShutdownError, exception
|
379
|
+
assert_includes work_item, @work_items[0, @num_workers]
|
380
|
+
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
|
+
end
|
393
|
+
|
394
|
+
def signal_test_suite_thread
|
395
|
+
params[:mutex].synchronize{ params[:cond_var].signal }
|
396
|
+
end
|
397
|
+
end
|
398
|
+
|
399
|
+
end
|