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.
@@ -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