dat-worker-pool 0.4.0 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/dat-worker-pool.gemspec +1 -1
- data/lib/dat-worker-pool.rb +114 -103
- data/lib/dat-worker-pool/queue.rb +20 -17
- data/lib/dat-worker-pool/version.rb +1 -1
- data/lib/dat-worker-pool/worker.rb +37 -10
- data/lib/dat-worker-pool/worker_pool_spy.rb +20 -16
- data/test/unit/dat-worker-pool_tests.rb +71 -22
- data/test/unit/worker_pool_spy_tests.rb +7 -0
- data/test/unit/worker_tests.rb +68 -25
- metadata +7 -7
data/dat-worker-pool.gemspec
CHANGED
data/lib/dat-worker-pool.rb
CHANGED
@@ -8,10 +8,9 @@ require 'dat-worker-pool/worker'
|
|
8
8
|
|
9
9
|
class DatWorkerPool
|
10
10
|
|
11
|
-
TimeoutError = Class.new(RuntimeError)
|
12
|
-
|
13
11
|
attr_reader :logger, :spawned
|
14
12
|
attr_reader :queue
|
13
|
+
attr_reader :on_worker_error_callbacks
|
15
14
|
attr_reader :on_worker_start_callbacks, :on_worker_shutdown_callbacks
|
16
15
|
attr_reader :on_worker_sleep_callbacks, :on_worker_wakeup_callbacks
|
17
16
|
attr_reader :before_work_callbacks, :after_work_callbacks
|
@@ -30,20 +29,53 @@ class DatWorkerPool
|
|
30
29
|
@workers = []
|
31
30
|
@spawned = 0
|
32
31
|
|
32
|
+
@on_worker_error_callbacks = []
|
33
33
|
@on_worker_start_callbacks = []
|
34
34
|
@on_worker_shutdown_callbacks = [proc{ |worker| despawn_worker(worker) }]
|
35
35
|
@on_worker_sleep_callbacks = [proc{ @workers_waiting.increment }]
|
36
36
|
@on_worker_wakeup_callbacks = [proc{ @workers_waiting.decrement }]
|
37
|
-
@before_work_callbacks
|
38
|
-
@after_work_callbacks
|
37
|
+
@before_work_callbacks = []
|
38
|
+
@after_work_callbacks = []
|
39
|
+
|
40
|
+
@started = false
|
41
|
+
end
|
42
|
+
|
43
|
+
def start
|
44
|
+
@started = true
|
45
|
+
@queue.start
|
46
|
+
@min_workers.times{ spawn_worker }
|
47
|
+
end
|
39
48
|
|
49
|
+
# * All work on the queue is left on the queue. It's up to the controlling
|
50
|
+
# system to decide how it should handle this.
|
51
|
+
def shutdown(timeout = nil)
|
40
52
|
@started = false
|
53
|
+
begin
|
54
|
+
OptionalTimeout.new(timeout){ graceful_shutdown }
|
55
|
+
rescue TimeoutError
|
56
|
+
force_shutdown(timeout, caller)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# * Always check if all workers are busy before pushing the work because
|
61
|
+
# `@queue.push` can wakeup a worker. If you check after, you can see all
|
62
|
+
# workers are busy because one just wokeup to handle what was just pushed.
|
63
|
+
# This would cause it to spawn a worker when one isn't needed.
|
64
|
+
def add_work(work_item)
|
65
|
+
return if work_item.nil?
|
66
|
+
new_worker_needed = self.all_spawned_workers_are_busy?
|
67
|
+
@queue.push work_item
|
68
|
+
spawn_worker if @started && new_worker_needed && !reached_max_workers?
|
41
69
|
end
|
42
70
|
|
43
71
|
def work_items
|
44
72
|
@queue.work_items
|
45
73
|
end
|
46
74
|
|
75
|
+
def queue_empty?
|
76
|
+
@queue.empty?
|
77
|
+
end
|
78
|
+
|
47
79
|
def waiting
|
48
80
|
@workers_waiting.count
|
49
81
|
end
|
@@ -60,107 +92,89 @@ class DatWorkerPool
|
|
60
92
|
@mutex.synchronize{ @spawned >= @max_workers }
|
61
93
|
end
|
62
94
|
|
63
|
-
def queue_empty?
|
64
|
-
@queue.empty?
|
65
|
-
end
|
66
|
-
|
67
|
-
# Check if all workers are busy before adding the work. When the work is
|
68
|
-
# added, a worker will stop waiting (if it was idle). Because of that, we
|
69
|
-
# can't reliably check if all workers are busy. We might think all workers are
|
70
|
-
# busy because we just woke up a sleeping worker to process this work. Then we
|
71
|
-
# would spawn a worker to do nothing.
|
72
|
-
def add_work(work_item)
|
73
|
-
return if work_item.nil?
|
74
|
-
new_worker_needed = all_spawned_workers_are_busy?
|
75
|
-
@queue.push work_item
|
76
|
-
self.spawn_worker if @started && new_worker_needed && !reached_max_workers?
|
77
|
-
end
|
78
|
-
|
79
|
-
def start
|
80
|
-
@started = true
|
81
|
-
@queue.start
|
82
|
-
@min_workers.times{ self.spawn_worker }
|
83
|
-
end
|
84
|
-
|
85
|
-
# Shutdown each worker and then the queue. Shutting down the queue will
|
86
|
-
# signal any workers waiting on it to wake up, so they can start shutting
|
87
|
-
# down. If a worker is processing work, then it will be joined and allowed to
|
88
|
-
# finish.
|
89
|
-
# **NOTE** Any work that is left on the queue isn't processed. The controlling
|
90
|
-
# application for the worker pool should gracefully handle these items.
|
91
|
-
def shutdown(timeout = nil)
|
92
|
-
@started = false
|
93
|
-
begin
|
94
|
-
proc = OptionalTimeoutProc.new(timeout, true) do
|
95
|
-
# Workers need to be shutdown before the queue. This marks a flag that
|
96
|
-
# tells the workers to exit out of their loop once they wakeup. The
|
97
|
-
# queue shutdown signals the workers to wakeup, so the flag needs to be
|
98
|
-
# set before they wakeup.
|
99
|
-
@workers.each(&:shutdown)
|
100
|
-
@queue.shutdown
|
101
|
-
|
102
|
-
# use this pattern instead of `each` -- we don't want to call `join` on
|
103
|
-
# every worker (especially if they are shutting down on their own), we
|
104
|
-
# just want to make sure that any who haven't had a chance to finish
|
105
|
-
# get to (this is safe, otherwise you might get a dead thread in the
|
106
|
-
# `each`).
|
107
|
-
@workers.first.join until @workers.empty?
|
108
|
-
end
|
109
|
-
proc.call
|
110
|
-
rescue TimeoutError => exception
|
111
|
-
exception.message.replace "Timed out shutting down the worker pool"
|
112
|
-
@debug ? raise(exception) : self.logger.error(exception.message)
|
113
|
-
end
|
114
|
-
end
|
115
|
-
|
116
95
|
def on_queue_pop_callbacks; @queue.on_pop_callbacks; end
|
117
96
|
def on_queue_push_callbacks; @queue.on_push_callbacks; end
|
118
97
|
|
119
|
-
def on_queue_pop(&block); @queue.on_pop_callbacks
|
98
|
+
def on_queue_pop(&block); @queue.on_pop_callbacks << block; end
|
120
99
|
def on_queue_push(&block); @queue.on_push_callbacks << block; end
|
121
100
|
|
122
|
-
def
|
101
|
+
def on_worker_error(&block); @on_worker_error_callbacks << block; end
|
102
|
+
def on_worker_start(&block); @on_worker_start_callbacks << block; end
|
123
103
|
def on_worker_shutdown(&block); @on_worker_shutdown_callbacks << block; end
|
124
|
-
def on_worker_sleep(&block); @on_worker_sleep_callbacks
|
125
|
-
def on_worker_wakeup(&block); @on_worker_wakeup_callbacks
|
104
|
+
def on_worker_sleep(&block); @on_worker_sleep_callbacks << block; end
|
105
|
+
def on_worker_wakeup(&block); @on_worker_wakeup_callbacks << block; end
|
126
106
|
|
127
107
|
def before_work(&block); @before_work_callbacks << block; end
|
128
|
-
def after_work(&block); @after_work_callbacks
|
108
|
+
def after_work(&block); @after_work_callbacks << block; end
|
109
|
+
|
110
|
+
private
|
111
|
+
|
112
|
+
def do_work(work_item)
|
113
|
+
@do_work_proc.call(work_item)
|
114
|
+
end
|
129
115
|
|
130
|
-
|
116
|
+
# * Always shutdown workers before the queue. `@queue.shutdown` wakes up the
|
117
|
+
# workers. If you haven't told them to shutdown before they wakeup then they
|
118
|
+
# won't start their shutdown when they are woken up.
|
119
|
+
# * Use `@workers.first.join until @workers.empty?` instead of `each` to join
|
120
|
+
# all the workers. While we are joining a worker a separate worker can
|
121
|
+
# shutdown and remove itself from the `@workers` array.
|
122
|
+
def graceful_shutdown
|
123
|
+
@workers.each(&:shutdown)
|
124
|
+
@queue.shutdown
|
125
|
+
@workers.first.join until @workers.empty?
|
126
|
+
end
|
127
|
+
|
128
|
+
# * Use `@workers.first until @workers.empty?` pattern instead of `each` to
|
129
|
+
# raise and join all the workers. While we are raising and joining a worker
|
130
|
+
# a separate worker can shutdown and remove itself from the `@workers`
|
131
|
+
# array.
|
132
|
+
# * `rescue false` when joining the workers. Ruby will raise any exceptions
|
133
|
+
# that aren't handled by a thread when its joined. This ensures if the hard
|
134
|
+
# shutdown is raised and not rescued (for example, in the workers ensure),
|
135
|
+
# then it won't cause the forced shutdown to end prematurely.
|
136
|
+
def force_shutdown(timeout, backtrace)
|
137
|
+
error = ShutdownError.new "Timed out shutting down the worker pool " \
|
138
|
+
"(#{timeout} seconds)."
|
139
|
+
error.set_backtrace(backtrace)
|
140
|
+
until @workers.empty?
|
141
|
+
worker = @workers.first
|
142
|
+
worker.raise(error)
|
143
|
+
worker.join rescue false
|
144
|
+
end
|
145
|
+
raise error if @debug
|
146
|
+
end
|
131
147
|
|
132
148
|
def spawn_worker
|
133
|
-
@mutex.synchronize
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
149
|
+
@mutex.synchronize{ spawn_worker! }
|
150
|
+
end
|
151
|
+
|
152
|
+
def spawn_worker!
|
153
|
+
Worker.new(@queue).tap do |w|
|
154
|
+
w.on_work = proc{ |worker, work_item| do_work(work_item) }
|
155
|
+
|
156
|
+
w.on_error_callbacks = @on_worker_error_callbacks
|
157
|
+
w.on_start_callbacks = @on_worker_start_callbacks
|
158
|
+
w.on_shutdown_callbacks = @on_worker_shutdown_callbacks
|
159
|
+
w.on_sleep_callbacks = @on_worker_sleep_callbacks
|
160
|
+
w.on_wakeup_callbacks = @on_worker_wakeup_callbacks
|
161
|
+
w.before_work_callbacks = @before_work_callbacks
|
162
|
+
w.after_work_callbacks = @after_work_callbacks
|
163
|
+
|
164
|
+
@workers << w
|
165
|
+
@spawned += 1
|
166
|
+
|
167
|
+
w.start
|
148
168
|
end
|
149
169
|
end
|
150
170
|
|
151
171
|
def despawn_worker(worker)
|
152
|
-
@mutex.synchronize
|
153
|
-
@spawned -= 1
|
154
|
-
@workers.delete worker
|
155
|
-
end
|
172
|
+
@mutex.synchronize{ despawn_worker!(worker) }
|
156
173
|
end
|
157
174
|
|
158
|
-
def
|
159
|
-
@
|
160
|
-
|
161
|
-
self.logger.error "Exception raised while doing work!"
|
162
|
-
self.logger.error "#{exception.class}: #{exception.message}"
|
163
|
-
self.logger.error exception.backtrace.join("\n")
|
175
|
+
def despawn_worker!(worker)
|
176
|
+
@spawned -= 1
|
177
|
+
@workers.delete worker
|
164
178
|
end
|
165
179
|
|
166
180
|
class WorkersWaiting
|
@@ -180,22 +194,12 @@ class DatWorkerPool
|
|
180
194
|
end
|
181
195
|
end
|
182
196
|
|
183
|
-
|
184
|
-
def
|
185
|
-
|
186
|
-
|
187
|
-
@proc = proc
|
188
|
-
end
|
189
|
-
|
190
|
-
def call
|
191
|
-
if @timeout
|
192
|
-
begin
|
193
|
-
SystemTimer.timeout(@timeout, TimeoutError, &@proc)
|
194
|
-
rescue TimeoutError
|
195
|
-
raise if @reraise
|
196
|
-
end
|
197
|
+
module OptionalTimeout
|
198
|
+
def self.new(seconds, &block)
|
199
|
+
if seconds
|
200
|
+
SystemTimer.timeout(seconds, TimeoutError, &block)
|
197
201
|
else
|
198
|
-
|
202
|
+
block.call
|
199
203
|
end
|
200
204
|
end
|
201
205
|
end
|
@@ -206,4 +210,11 @@ class DatWorkerPool
|
|
206
210
|
end
|
207
211
|
end
|
208
212
|
|
213
|
+
TimeoutError = Class.new(RuntimeError)
|
214
|
+
|
215
|
+
# * This error should never be "swallowed". If it is caught be sure to
|
216
|
+
# re-raise it so the workers shutdown. Otherwise workers will get killed
|
217
|
+
# (`Thread#kill`) by ruby which causes lots of issues.
|
218
|
+
ShutdownError = Class.new(Interrupt)
|
219
|
+
|
209
220
|
end
|
@@ -7,21 +7,28 @@ class DatWorkerPool
|
|
7
7
|
attr_accessor :on_push_callbacks, :on_pop_callbacks
|
8
8
|
|
9
9
|
def initialize
|
10
|
-
@work_items
|
11
|
-
@shutdown
|
12
|
-
@mutex
|
10
|
+
@work_items = []
|
11
|
+
@shutdown = false
|
12
|
+
@mutex = Mutex.new
|
13
13
|
@condition_variable = ConditionVariable.new
|
14
14
|
|
15
15
|
@on_pop_callbacks = []
|
16
16
|
@on_push_callbacks = []
|
17
17
|
end
|
18
18
|
|
19
|
-
def
|
20
|
-
@
|
19
|
+
def start
|
20
|
+
@shutdown = false
|
21
|
+
end
|
22
|
+
|
23
|
+
# * Wakes up any threads (`@condition_variable.broadcast`) who are sleeping
|
24
|
+
# because of `pop`.
|
25
|
+
def shutdown
|
26
|
+
@shutdown = true
|
27
|
+
@mutex.synchronize{ @condition_variable.broadcast }
|
21
28
|
end
|
22
29
|
|
23
|
-
# Add the
|
24
|
-
#
|
30
|
+
# * Add the work and wake up the first thread waiting from calling `pop`
|
31
|
+
# (`@condition_variable.signal`).
|
25
32
|
def push(work_item)
|
26
33
|
raise "Unable to add work while shutting down" if @shutdown
|
27
34
|
@mutex.synchronize do
|
@@ -31,6 +38,8 @@ class DatWorkerPool
|
|
31
38
|
@on_push_callbacks.each(&:call)
|
32
39
|
end
|
33
40
|
|
41
|
+
# * Sleeps the current thread (`@condition_variable.wait(@mutex)`) until it
|
42
|
+
# is signaled via `push` or `shutdown`.
|
34
43
|
def pop
|
35
44
|
return if @shutdown
|
36
45
|
item = @mutex.synchronize do
|
@@ -41,18 +50,12 @@ class DatWorkerPool
|
|
41
50
|
item
|
42
51
|
end
|
43
52
|
|
44
|
-
def
|
45
|
-
@mutex.synchronize{ @work_items
|
46
|
-
end
|
47
|
-
|
48
|
-
def start
|
49
|
-
@shutdown = false
|
53
|
+
def work_items
|
54
|
+
@mutex.synchronize{ @work_items }
|
50
55
|
end
|
51
56
|
|
52
|
-
|
53
|
-
|
54
|
-
@shutdown = true
|
55
|
-
@mutex.synchronize{ @condition_variable.broadcast }
|
57
|
+
def empty?
|
58
|
+
@mutex.synchronize{ @work_items.empty? }
|
56
59
|
end
|
57
60
|
|
58
61
|
def shutdown?
|
@@ -1,10 +1,11 @@
|
|
1
1
|
require 'thread'
|
2
|
+
require 'dat-worker-pool'
|
2
3
|
|
3
4
|
class DatWorkerPool
|
4
5
|
|
5
6
|
class Worker
|
6
7
|
|
7
|
-
attr_accessor :on_work
|
8
|
+
attr_accessor :on_work, :on_error_callbacks
|
8
9
|
attr_accessor :on_start_callbacks, :on_shutdown_callbacks
|
9
10
|
attr_accessor :on_sleep_callbacks, :on_wakeup_callbacks
|
10
11
|
attr_accessor :before_work_callbacks, :after_work_callbacks
|
@@ -12,6 +13,7 @@ class DatWorkerPool
|
|
12
13
|
def initialize(queue)
|
13
14
|
@queue = queue
|
14
15
|
@on_work = proc{ |worker, work_item| }
|
16
|
+
@on_error_callbacks = []
|
15
17
|
@on_start_callbacks = []
|
16
18
|
@on_shutdown_callbacks = []
|
17
19
|
@on_sleep_callbacks = []
|
@@ -27,40 +29,65 @@ class DatWorkerPool
|
|
27
29
|
@thread ||= Thread.new{ work_loop }
|
28
30
|
end
|
29
31
|
|
30
|
-
def running?
|
31
|
-
@thread && @thread.alive?
|
32
|
-
end
|
33
|
-
|
34
32
|
def shutdown
|
35
33
|
@shutdown = true
|
36
34
|
end
|
37
35
|
|
36
|
+
def running?
|
37
|
+
!!(@thread && @thread.alive?)
|
38
|
+
end
|
39
|
+
|
38
40
|
def join(*args)
|
39
41
|
@thread.join(*args) if running?
|
40
42
|
end
|
41
43
|
|
42
|
-
|
44
|
+
def raise(*args)
|
45
|
+
@thread.raise(*args) if running?
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
43
49
|
|
50
|
+
# * Rescue `ShutdownError` but don't do anything with it. We want to handle
|
51
|
+
# the error but we just want it to cause the worker to exit its work loop.
|
52
|
+
# If the `ShutdownError` isn't rescued, it will be raised when the worker
|
53
|
+
# is joined.
|
44
54
|
def work_loop
|
45
55
|
@on_start_callbacks.each{ |p| p.call(self) }
|
46
56
|
loop do
|
47
|
-
@on_sleep_callbacks.each{ |p| p.call(self) }
|
48
|
-
work_item = @queue.pop
|
49
|
-
@on_wakeup_callbacks.each{ |p| p.call(self) }
|
50
57
|
break if @shutdown
|
51
|
-
|
58
|
+
fetch_and_do_work
|
52
59
|
end
|
60
|
+
rescue ShutdownError
|
53
61
|
ensure
|
54
62
|
@on_shutdown_callbacks.each{ |p| p.call(self) }
|
55
63
|
@thread = nil
|
56
64
|
end
|
57
65
|
|
66
|
+
# * Rescue `ShutdownError` but re-raise it after calling the error
|
67
|
+
# callbacks. This ensures it causes the work loop to exit (see
|
68
|
+
# `work_loop`).
|
69
|
+
def fetch_and_do_work
|
70
|
+
@on_sleep_callbacks.each{ |p| p.call(self) }
|
71
|
+
work_item = @queue.pop
|
72
|
+
@on_wakeup_callbacks.each{ |p| p.call(self) }
|
73
|
+
do_work(work_item) if work_item
|
74
|
+
rescue ShutdownError => exception
|
75
|
+
handle_exception(exception, work_item)
|
76
|
+
raise exception
|
77
|
+
rescue StandardError => exception
|
78
|
+
handle_exception(exception, work_item)
|
79
|
+
end
|
80
|
+
|
58
81
|
def do_work(work_item)
|
59
82
|
@before_work_callbacks.each{ |p| p.call(self, work_item) }
|
60
83
|
@on_work.call(self, work_item)
|
61
84
|
@after_work_callbacks.each{ |p| p.call(self, work_item) }
|
62
85
|
end
|
63
86
|
|
87
|
+
def handle_exception(exception, work_item = nil)
|
88
|
+
@on_error_callbacks.each{ |p| p.call(self, exception, work_item) }
|
89
|
+
end
|
90
|
+
|
64
91
|
end
|
65
92
|
|
66
93
|
end
|
@@ -1,11 +1,13 @@
|
|
1
1
|
class DatWorkerPool
|
2
2
|
|
3
3
|
class WorkerPoolSpy
|
4
|
+
|
4
5
|
attr_reader :min_workers, :max_workers, :debug
|
5
6
|
attr_reader :work_proc, :work_items
|
6
7
|
attr_reader :start_called
|
7
8
|
attr_reader :shutdown_called, :shutdown_timeout
|
8
9
|
attr_reader :on_queue_pop_callbacks, :on_queue_push_callbacks
|
10
|
+
attr_reader :on_worker_error_callbacks
|
9
11
|
attr_reader :on_worker_start_callbacks, :on_worker_shutdown_callbacks
|
10
12
|
attr_reader :on_worker_sleep_callbacks, :on_worker_wakeup_callbacks
|
11
13
|
attr_reader :before_work_callbacks, :after_work_callbacks
|
@@ -25,6 +27,7 @@ class DatWorkerPool
|
|
25
27
|
|
26
28
|
@on_queue_pop_callbacks = []
|
27
29
|
@on_queue_push_callbacks = []
|
30
|
+
@on_worker_error_callbacks = []
|
28
31
|
@on_worker_start_callbacks = []
|
29
32
|
@on_worker_shutdown_callbacks = []
|
30
33
|
@on_worker_sleep_callbacks = []
|
@@ -33,12 +36,13 @@ class DatWorkerPool
|
|
33
36
|
@after_work_callbacks = []
|
34
37
|
end
|
35
38
|
|
36
|
-
def
|
37
|
-
@
|
39
|
+
def start
|
40
|
+
@start_called = true
|
38
41
|
end
|
39
42
|
|
40
|
-
def
|
41
|
-
@
|
43
|
+
def shutdown(timeout = nil)
|
44
|
+
@shutdown_called = true
|
45
|
+
@shutdown_timeout = timeout
|
42
46
|
end
|
43
47
|
|
44
48
|
def add_work(work)
|
@@ -53,23 +57,23 @@ class DatWorkerPool
|
|
53
57
|
work
|
54
58
|
end
|
55
59
|
|
56
|
-
def
|
57
|
-
@
|
60
|
+
def queue_empty?
|
61
|
+
@work_items.empty?
|
58
62
|
end
|
59
63
|
|
60
|
-
def
|
61
|
-
@
|
62
|
-
@shutdown_timeout = timeout
|
64
|
+
def worker_available?
|
65
|
+
@worker_available
|
63
66
|
end
|
64
67
|
|
65
|
-
def on_queue_pop(&block); @on_queue_pop_callbacks
|
66
|
-
def on_queue_push(&block); @on_queue_push_callbacks
|
67
|
-
def
|
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
|
68
72
|
def on_worker_shutdown(&block); @on_worker_shutdown_callbacks << block; end
|
69
|
-
def on_worker_sleep(&block); @on_worker_sleep_callbacks
|
70
|
-
def on_worker_wakeup(&block); @on_worker_wakeup_callbacks
|
71
|
-
def before_work(&block); @before_work_callbacks
|
72
|
-
def after_work(&block); @after_work_callbacks
|
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
|
73
77
|
|
74
78
|
end
|
75
79
|
|
@@ -11,16 +11,18 @@ class DatWorkerPool
|
|
11
11
|
subject{ @work_pool }
|
12
12
|
|
13
13
|
should have_readers :logger, :spawned, :queue
|
14
|
+
should have_readers :on_worker_error_callbacks
|
14
15
|
should have_readers :on_worker_start_callbacks, :on_worker_shutdown_callbacks
|
15
16
|
should have_readers :on_worker_sleep_callbacks, :on_worker_wakeup_callbacks
|
16
17
|
should have_readers :before_work_callbacks, :after_work_callbacks
|
17
|
-
should have_imeths :add_work, :start, :shutdown
|
18
|
+
should have_imeths :add_work, :start, :shutdown
|
18
19
|
should have_imeths :work_items, :waiting
|
19
20
|
should have_imeths :worker_available?, :all_spawned_workers_are_busy?
|
20
21
|
should have_imeths :reached_max_workers?
|
21
22
|
should have_imeths :queue_empty?
|
22
23
|
should have_imeths :on_queue_pop_callbacks, :on_queue_push_callbacks
|
23
24
|
should have_imeths :on_queue_pop, :on_queue_push
|
25
|
+
should have_imeths :on_worker_error
|
24
26
|
should have_imeths :on_worker_start, :on_worker_shutdown
|
25
27
|
should have_imeths :on_worker_sleep, :on_worker_wakeup
|
26
28
|
should have_imeths :before_work, :after_work
|
@@ -32,6 +34,7 @@ class DatWorkerPool
|
|
32
34
|
end
|
33
35
|
|
34
36
|
should "default its worker callbacks" do
|
37
|
+
assert_equal [], subject.on_worker_error_callbacks
|
35
38
|
assert_equal [], subject.on_worker_start_callbacks
|
36
39
|
assert_equal 1, subject.on_worker_shutdown_callbacks.size
|
37
40
|
assert_instance_of Proc, subject.on_worker_shutdown_callbacks.first
|
@@ -113,21 +116,24 @@ class DatWorkerPool
|
|
113
116
|
class WorkerCallbackTests < UnitTests
|
114
117
|
desc "worker callbacks"
|
115
118
|
setup do
|
116
|
-
@
|
117
|
-
@
|
118
|
-
@
|
119
|
-
@
|
119
|
+
@error_called = false
|
120
|
+
@start_called = false
|
121
|
+
@shutdown_called = false
|
122
|
+
@sleep_called = false
|
123
|
+
@wakeup_called = false
|
120
124
|
@before_work_called = false
|
121
|
-
@after_work_called
|
122
|
-
|
123
|
-
@work_pool = DatWorkerPool.new(1)
|
124
|
-
|
125
|
-
p.on_worker_shutdown{ @shutdown_called = true }
|
126
|
-
p.on_worker_sleep{ @sleep_called = true }
|
127
|
-
p.on_worker_wakeup{ @wakeup_called = true }
|
128
|
-
p.before_work{ @before_work_called = true }
|
129
|
-
p.after_work{ @after_work_called = true }
|
125
|
+
@after_work_called = false
|
126
|
+
|
127
|
+
@work_pool = DatWorkerPool.new(1) do |work|
|
128
|
+
raise work if work == 'error'
|
130
129
|
end
|
130
|
+
@work_pool.on_worker_error{ @error_called = true }
|
131
|
+
@work_pool.on_worker_start{ @start_called = true }
|
132
|
+
@work_pool.on_worker_shutdown{ @shutdown_called = true }
|
133
|
+
@work_pool.on_worker_sleep{ @sleep_called = true }
|
134
|
+
@work_pool.on_worker_wakeup{ @wakeup_called = true }
|
135
|
+
@work_pool.before_work{ @before_work_called = true }
|
136
|
+
@work_pool.after_work{ @after_work_called = true }
|
131
137
|
end
|
132
138
|
subject{ @work_pool }
|
133
139
|
|
@@ -137,15 +143,27 @@ class DatWorkerPool
|
|
137
143
|
subject.start
|
138
144
|
assert_true @start_called
|
139
145
|
assert_true @sleep_called
|
146
|
+
|
147
|
+
@sleep_called = false
|
140
148
|
assert_false @wakeup_called
|
141
149
|
assert_false @before_work_called
|
142
150
|
assert_false @after_work_called
|
143
|
-
@sleep_called = false
|
144
151
|
subject.add_work 'work'
|
145
152
|
assert_true @wakeup_called
|
146
153
|
assert_true @before_work_called
|
147
154
|
assert_true @after_work_called
|
148
155
|
assert_true @sleep_called
|
156
|
+
|
157
|
+
@before_work_called = false
|
158
|
+
@after_work_called = false
|
159
|
+
assert_false @before_work_called
|
160
|
+
assert_false @error_called
|
161
|
+
assert_false @after_work_called
|
162
|
+
subject.add_work 'error'
|
163
|
+
assert_true @before_work_called
|
164
|
+
assert_true @error_called
|
165
|
+
assert_false @after_work_called
|
166
|
+
|
149
167
|
@wakeup_called = false
|
150
168
|
assert_false @shutdown_called
|
151
169
|
subject.shutdown
|
@@ -252,19 +270,50 @@ class DatWorkerPool
|
|
252
270
|
assert_includes 'c', subject.work_items
|
253
271
|
end
|
254
272
|
|
255
|
-
should "
|
256
|
-
# make sure the workers haven't processed any work
|
273
|
+
should "allow jobs to finish by not providing a shutdown timeout" do
|
257
274
|
assert_equal [], @finished
|
258
|
-
|
275
|
+
subject.shutdown
|
276
|
+
assert_includes 'a', @finished
|
277
|
+
assert_includes 'b', @finished
|
278
|
+
end
|
279
|
+
|
280
|
+
should "reraise shutdown errors in debug mode if workers take to long to finish" do
|
281
|
+
assert_raises(DatWorkerPool::ShutdownError) do
|
259
282
|
subject.shutdown(0.1)
|
260
283
|
end
|
261
284
|
end
|
262
285
|
|
263
|
-
|
286
|
+
end
|
287
|
+
|
288
|
+
class ForcedShutdownTests < UnitTests
|
289
|
+
desc "forced shutdown"
|
290
|
+
setup do
|
291
|
+
@mutex = Mutex.new
|
292
|
+
@finished = []
|
293
|
+
@max_workers = 2
|
294
|
+
# don't put leave the worker pool in debug mode
|
295
|
+
@work_pool = DatWorkerPool.new(1, @max_workers, false) do |work|
|
296
|
+
begin
|
297
|
+
sleep 1
|
298
|
+
rescue ShutdownError => error
|
299
|
+
@mutex.synchronize{ @finished << error }
|
300
|
+
raise error # re-raise it otherwise worker won't shutdown
|
301
|
+
end
|
302
|
+
end
|
303
|
+
@work_pool.start
|
304
|
+
@work_pool.add_work 'a'
|
305
|
+
@work_pool.add_work 'b'
|
306
|
+
@work_pool.add_work 'c'
|
307
|
+
end
|
308
|
+
|
309
|
+
should "force workers to shutdown if they take to long to finish" do
|
310
|
+
# make sure the workers haven't processed any work
|
264
311
|
assert_equal [], @finished
|
265
|
-
subject.shutdown
|
266
|
-
|
267
|
-
|
312
|
+
subject.shutdown(0.1)
|
313
|
+
assert_equal @max_workers, @finished.size
|
314
|
+
@finished.each do |error|
|
315
|
+
assert_instance_of DatWorkerPool::ShutdownError, error
|
316
|
+
end
|
268
317
|
end
|
269
318
|
|
270
319
|
end
|
@@ -14,6 +14,7 @@ class DatWorkerPool::WorkerPoolSpy
|
|
14
14
|
should have_readers :work_proc, :work_items
|
15
15
|
should have_readers :start_called, :shutdown_called, :shutdown_timeout
|
16
16
|
should have_readers :on_queue_pop_callbacks, :on_queue_push_callbacks
|
17
|
+
should have_readers :on_worker_error_callbacks
|
17
18
|
should have_readers :on_worker_start_callbacks, :on_worker_shutdown_callbacks
|
18
19
|
should have_readers :on_worker_sleep_callbacks, :on_worker_wakeup_callbacks
|
19
20
|
should have_readers :before_work_callbacks, :after_work_callbacks
|
@@ -21,6 +22,7 @@ class DatWorkerPool::WorkerPoolSpy
|
|
21
22
|
should have_imeths :worker_available?, :queue_empty?
|
22
23
|
should have_imeths :add_work, :start, :shutdown
|
23
24
|
should have_imeths :on_queue_pop, :on_queue_push
|
25
|
+
should have_imeths :on_worker_error
|
24
26
|
should have_imeths :on_worker_start, :on_worker_shutdown
|
25
27
|
should have_imeths :on_worker_sleep, :on_worker_wakeup
|
26
28
|
should have_imeths :before_work, :after_work
|
@@ -101,6 +103,11 @@ class DatWorkerPool::WorkerPoolSpy
|
|
101
103
|
subject.on_queue_push(&callback)
|
102
104
|
assert_equal [callback], subject.on_queue_push_callbacks
|
103
105
|
|
106
|
+
assert_equal [], subject.on_worker_error_callbacks
|
107
|
+
callback = proc{ }
|
108
|
+
subject.on_worker_error(&callback)
|
109
|
+
assert_equal [callback], subject.on_worker_error_callbacks
|
110
|
+
|
104
111
|
assert_equal [], subject.on_worker_start_callbacks
|
105
112
|
callback = proc{ }
|
106
113
|
subject.on_worker_start(&callback)
|
data/test/unit/worker_tests.rb
CHANGED
@@ -22,14 +22,15 @@ class DatWorkerPool::Worker
|
|
22
22
|
end
|
23
23
|
subject{ @worker }
|
24
24
|
|
25
|
-
should have_accessors :on_work
|
25
|
+
should have_accessors :on_work, :on_error_callbacks
|
26
26
|
should have_accessors :on_start_callbacks, :on_shutdown_callbacks
|
27
27
|
should have_accessors :on_sleep_callbacks, :on_wakeup_callbacks
|
28
28
|
should have_accessors :before_work_callbacks, :after_work_callbacks
|
29
|
-
should have_imeths :start, :shutdown, :join, :running?
|
29
|
+
should have_imeths :start, :shutdown, :join, :raise, :running?
|
30
30
|
|
31
31
|
should "default its callbacks" do
|
32
32
|
worker = DatWorkerPool::Worker.new(@queue)
|
33
|
+
assert_equal [], worker.on_error_callbacks
|
33
34
|
assert_equal [], worker.on_start_callbacks
|
34
35
|
assert_equal [], worker.on_shutdown_callbacks
|
35
36
|
assert_equal [], worker.on_sleep_callbacks
|
@@ -38,7 +39,7 @@ class DatWorkerPool::Worker
|
|
38
39
|
assert_equal [], worker.after_work_callbacks
|
39
40
|
end
|
40
41
|
|
41
|
-
should "start a thread with it's work loop
|
42
|
+
should "start a thread with it's work loop using `start`" do
|
42
43
|
thread = nil
|
43
44
|
assert_nothing_raised{ thread = subject.start }
|
44
45
|
assert_instance_of Thread, thread
|
@@ -55,7 +56,7 @@ class DatWorkerPool::Worker
|
|
55
56
|
assert_equal [ 'one', 'two' ], @work_done
|
56
57
|
end
|
57
58
|
|
58
|
-
should "flag itself for exiting it's work loop
|
59
|
+
should "flag itself for exiting it's work loop using `shutdown` and " \
|
59
60
|
"end it's thread once it's queue is shutdown" do
|
60
61
|
thread = subject.start
|
61
62
|
subject.join 0.1 # trigger the worker's thread to run, allow it to get into it's
|
@@ -68,53 +69,75 @@ class DatWorkerPool::Worker
|
|
68
69
|
assert_not subject.running?
|
69
70
|
end
|
70
71
|
|
72
|
+
should "raise an error on the thread using `raise`" do
|
73
|
+
subject.on_work = proc do |worker, work|
|
74
|
+
begin
|
75
|
+
sleep 1
|
76
|
+
rescue RuntimeError => error
|
77
|
+
@work_done << error
|
78
|
+
raise error
|
79
|
+
end
|
80
|
+
end
|
81
|
+
subject.start
|
82
|
+
@queue.push 'a'
|
83
|
+
subject.join 0.1 # trigger the worker's thread to run
|
84
|
+
|
85
|
+
exception = RuntimeError.new
|
86
|
+
subject.raise exception
|
87
|
+
assert_equal [exception], @work_done
|
88
|
+
end
|
89
|
+
|
71
90
|
end
|
72
91
|
|
73
92
|
class CallbacksTests < UnitTests
|
74
93
|
desc "callbacks"
|
75
94
|
setup do
|
76
95
|
@call_counter = 0
|
77
|
-
@
|
78
|
-
@
|
96
|
+
@on_error_called_with = nil
|
97
|
+
@on_start_called_with = nil
|
98
|
+
@on_start_called_at = nil
|
79
99
|
@on_shutdown_called_with = nil
|
80
|
-
@
|
81
|
-
@on_sleep_called_with
|
82
|
-
@
|
83
|
-
@on_wakeup_called_with
|
84
|
-
@
|
100
|
+
@on_shutdown_called_at = nil
|
101
|
+
@on_sleep_called_with = nil
|
102
|
+
@on_sleep_called_at = nil
|
103
|
+
@on_wakeup_called_with = nil
|
104
|
+
@on_wakeup_called_at = nil
|
85
105
|
@before_work_called_with = nil
|
86
|
-
@before_work_called_at
|
87
|
-
@after_work_called_with
|
88
|
-
@after_work_called_at
|
106
|
+
@before_work_called_at = nil
|
107
|
+
@after_work_called_with = nil
|
108
|
+
@after_work_called_at = nil
|
89
109
|
@worker = DatWorkerPool::Worker.new(@queue).tap do |w|
|
90
|
-
w.
|
110
|
+
w.on_error_callbacks << proc do |*args|
|
111
|
+
@on_error_called_with = args
|
112
|
+
end
|
113
|
+
w.on_start_callbacks << proc do |*args|
|
91
114
|
@on_start_called_with = args
|
92
|
-
@on_start_called_at
|
115
|
+
@on_start_called_at = (@call_counter += 1)
|
93
116
|
end
|
94
117
|
w.on_shutdown_callbacks << proc do |*args|
|
95
118
|
@on_shutdown_called_with = args
|
96
|
-
@on_shutdown_called_at
|
119
|
+
@on_shutdown_called_at = (@call_counter += 1)
|
97
120
|
end
|
98
|
-
w.on_sleep_callbacks
|
121
|
+
w.on_sleep_callbacks << proc do |*args|
|
99
122
|
@on_sleep_called_with = args
|
100
|
-
@on_sleep_called_at
|
123
|
+
@on_sleep_called_at = (@call_counter += 1)
|
101
124
|
end
|
102
|
-
w.on_wakeup_callbacks
|
125
|
+
w.on_wakeup_callbacks << proc do |*args|
|
103
126
|
@on_wakeup_called_with = args
|
104
|
-
@on_wakeup_called_at
|
127
|
+
@on_wakeup_called_at = (@call_counter += 1)
|
105
128
|
end
|
106
129
|
w.before_work_callbacks << proc do |*args|
|
107
130
|
@before_work_called_with = args
|
108
|
-
@before_work_called_at
|
131
|
+
@before_work_called_at = (@call_counter += 1)
|
109
132
|
end
|
110
133
|
w.after_work_callbacks << proc do |*args|
|
111
134
|
@after_work_called_with = args
|
112
|
-
@after_work_called_at
|
135
|
+
@after_work_called_at = (@call_counter += 1)
|
113
136
|
end
|
114
137
|
end
|
115
138
|
end
|
116
139
|
|
117
|
-
should "pass its self to its start, shutdown, sleep and
|
140
|
+
should "pass its self to its start, shutdown, sleep and wakeup callbacks" do
|
118
141
|
subject.start
|
119
142
|
@queue.push('work')
|
120
143
|
subject.shutdown
|
@@ -126,7 +149,7 @@ class DatWorkerPool::Worker
|
|
126
149
|
assert_equal [subject], @on_wakeup_called_with
|
127
150
|
end
|
128
151
|
|
129
|
-
should "pass its self and work to its
|
152
|
+
should "pass its self and work to its before and after work callbacks" do
|
130
153
|
subject.start
|
131
154
|
@queue.push('work')
|
132
155
|
subject.shutdown
|
@@ -151,6 +174,26 @@ class DatWorkerPool::Worker
|
|
151
174
|
assert_equal 8, @on_shutdown_called_at
|
152
175
|
end
|
153
176
|
|
177
|
+
should "call its error callbacks when an exception occurs" do
|
178
|
+
exception = RuntimeError.new
|
179
|
+
subject.on_work = proc{ raise exception }
|
180
|
+
thread = subject.start
|
181
|
+
@queue.push('work')
|
182
|
+
assert_equal [subject, exception, 'work'], @on_error_called_with
|
183
|
+
assert_true thread.alive?
|
184
|
+
end
|
185
|
+
|
186
|
+
should "call its error callbacks when an shutdown error occurs and reraise" do
|
187
|
+
exception = DatWorkerPool::ShutdownError.new
|
188
|
+
subject.on_work = proc{ raise exception }
|
189
|
+
thread = subject.start
|
190
|
+
@queue.push('work')
|
191
|
+
assert_equal [subject, exception, 'work'], @on_error_called_with
|
192
|
+
assert_false thread.alive?
|
193
|
+
# ensure the shutdown error is handled and isn't thrown when we join
|
194
|
+
assert_nothing_raised{ thread.join }
|
195
|
+
end
|
196
|
+
|
154
197
|
end
|
155
198
|
|
156
199
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dat-worker-pool
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 11
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 5
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.5.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Collin Redding
|
@@ -16,7 +16,7 @@ autorequire:
|
|
16
16
|
bindir: bin
|
17
17
|
cert_chain: []
|
18
18
|
|
19
|
-
date: 2015-
|
19
|
+
date: 2015-04-22 00:00:00 Z
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
22
22
|
requirement: &id001 !ruby/object:Gem::Requirement
|
@@ -39,11 +39,11 @@ dependencies:
|
|
39
39
|
requirements:
|
40
40
|
- - ~>
|
41
41
|
- !ruby/object:Gem::Version
|
42
|
-
hash:
|
42
|
+
hash: 31
|
43
43
|
segments:
|
44
44
|
- 2
|
45
|
-
-
|
46
|
-
version: "2.
|
45
|
+
- 14
|
46
|
+
version: "2.14"
|
47
47
|
version_requirements: *id002
|
48
48
|
type: :development
|
49
49
|
name: assert
|