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.
@@ -20,6 +20,6 @@ Gem::Specification.new do |gem|
20
20
 
21
21
  gem.add_dependency("SystemTimer", ["~> 1.2"])
22
22
 
23
- gem.add_development_dependency("assert", ["~> 2.12"])
23
+ gem.add_development_dependency("assert", ["~> 2.14"])
24
24
 
25
25
  end
@@ -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 << block; end
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 on_worker_start(&block); @on_worker_start_callbacks << block; end
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 << block; end
125
- def on_worker_wakeup(&block); @on_worker_wakeup_callbacks << block; end
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 << block; end
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
- protected
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 do
134
- Worker.new(@queue).tap do |w|
135
- w.on_work = proc{ |worker, work_item| do_work(work_item) }
136
- w.on_start_callbacks = @on_worker_start_callbacks
137
- w.on_shutdown_callbacks = @on_worker_shutdown_callbacks
138
- w.on_sleep_callbacks = @on_worker_sleep_callbacks
139
- w.on_wakeup_callbacks = @on_worker_wakeup_callbacks
140
- w.before_work_callbacks = @before_work_callbacks
141
- w.after_work_callbacks = @after_work_callbacks
142
-
143
- @workers << w
144
- @spawned += 1
145
-
146
- w.start
147
- end
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 do
153
- @spawned -= 1
154
- @workers.delete worker
155
- end
172
+ @mutex.synchronize{ despawn_worker!(worker) }
156
173
  end
157
174
 
158
- def do_work(work_item)
159
- @do_work_proc.call(work_item)
160
- rescue Exception => exception
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
- class OptionalTimeoutProc
184
- def initialize(timeout, reraise = false, &proc)
185
- @timeout = timeout
186
- @reraise = reraise
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
- @proc.call
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 = false
12
- @mutex = Mutex.new
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 work_items
20
- @mutex.synchronize{ @work_items }
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 work_item and wake up the first worker (the `signal`) that's
24
- # waiting (because of `wait_for_work_item`)
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 empty?
45
- @mutex.synchronize{ @work_items.empty? }
46
- end
47
-
48
- def start
49
- @shutdown = false
53
+ def work_items
54
+ @mutex.synchronize{ @work_items }
50
55
  end
51
56
 
52
- # wake up any workers who are idle (because of `wait_for_work_item`)
53
- def shutdown
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,3 +1,3 @@
1
1
  class DatWorkerPool
2
- VERSION = "0.4.0"
2
+ VERSION = "0.5.0"
3
3
  end
@@ -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
- protected
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
- do_work(work_item) if work_item
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 worker_available?
37
- @worker_available
39
+ def start
40
+ @start_called = true
38
41
  end
39
42
 
40
- def queue_empty?
41
- @work_items.empty?
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 start
57
- @start_called = true
60
+ def queue_empty?
61
+ @work_items.empty?
58
62
  end
59
63
 
60
- def shutdown(timeout = nil)
61
- @shutdown_called = true
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 << block; end
66
- def on_queue_push(&block); @on_queue_push_callbacks << block; end
67
- def on_worker_start(&block); @on_worker_start_callbacks << block; end
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 << block; end
70
- def on_worker_wakeup(&block); @on_worker_wakeup_callbacks << block; end
71
- def before_work(&block); @before_work_callbacks << block; end
72
- def after_work(&block); @after_work_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
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, :despawn_worker
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
- @start_called = false
117
- @shutdown_called = false
118
- @sleep_called = false
119
- @wakeup_called = false
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 = false
122
-
123
- @work_pool = DatWorkerPool.new(1){ |work| }.tap do |p|
124
- p.on_worker_start{ @start_called = true }
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 "timeout if the workers take to long to finish" do
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
- assert_raises(DatWorkerPool::TimeoutError) do
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
- should "allow jobs to finish by not providing a shutdown timeout" do
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
- assert_includes 'a', @finished
267
- assert_includes 'b', @finished
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)
@@ -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 with #start" do
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 with #shutdown and " \
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
- @on_start_called_with = nil
78
- @on_start_call_count = nil
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
- @on_shutdown_call_count = nil
81
- @on_sleep_called_with = nil
82
- @on_sleep_call_count = nil
83
- @on_wakeup_called_with = nil
84
- @on_wakeup_call_count = nil
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 = nil
87
- @after_work_called_with = nil
88
- @after_work_called_at = nil
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.on_start_callbacks << proc do |*args|
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 = (@call_counter += 1)
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 = (@call_counter += 1)
119
+ @on_shutdown_called_at = (@call_counter += 1)
97
120
  end
98
- w.on_sleep_callbacks << proc do |*args|
121
+ w.on_sleep_callbacks << proc do |*args|
99
122
  @on_sleep_called_with = args
100
- @on_sleep_called_at = (@call_counter += 1)
123
+ @on_sleep_called_at = (@call_counter += 1)
101
124
  end
102
- w.on_wakeup_callbacks << proc do |*args|
125
+ w.on_wakeup_callbacks << proc do |*args|
103
126
  @on_wakeup_called_with = args
104
- @on_wakeup_called_at = (@call_counter += 1)
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 = (@call_counter += 1)
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 = (@call_counter += 1)
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 wakupe callbacks" do
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 beofre and after work callbacks" do
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: 15
4
+ hash: 11
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 4
8
+ - 5
9
9
  - 0
10
- version: 0.4.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-01-02 00:00:00 Z
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: 27
42
+ hash: 31
43
43
  segments:
44
44
  - 2
45
- - 12
46
- version: "2.12"
45
+ - 14
46
+ version: "2.14"
47
47
  version_requirements: *id002
48
48
  type: :development
49
49
  name: assert