dat-worker-pool 0.4.0 → 0.5.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/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
|