dat-worker-pool 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +1 -1
- data/bench/report.rb +130 -0
- data/bench/report.txt +7 -0
- data/dat-worker-pool.gemspec +1 -1
- data/lib/dat-worker-pool.rb +38 -187
- data/lib/dat-worker-pool/default_queue.rb +60 -0
- data/lib/dat-worker-pool/locked_object.rb +60 -0
- data/lib/dat-worker-pool/queue.rb +71 -48
- data/lib/dat-worker-pool/runner.rb +196 -0
- data/lib/dat-worker-pool/version.rb +1 -1
- data/lib/dat-worker-pool/worker.rb +251 -72
- data/lib/dat-worker-pool/worker_pool_spy.rb +39 -53
- data/test/helper.rb +13 -0
- data/test/support/factory.rb +15 -0
- data/test/support/thread_spies.rb +83 -0
- data/test/system/dat-worker-pool_tests.rb +399 -0
- data/test/unit/dat-worker-pool_tests.rb +132 -255
- data/test/unit/default_queue_tests.rb +217 -0
- data/test/unit/locked_object_tests.rb +260 -0
- data/test/unit/queue_tests.rb +95 -72
- data/test/unit/runner_tests.rb +365 -0
- data/test/unit/worker_pool_spy_tests.rb +95 -102
- data/test/unit/worker_tests.rb +819 -153
- metadata +27 -12
- data/test/system/use_worker_pool_tests.rb +0 -34
data/Gemfile
CHANGED
data/bench/report.rb
ADDED
@@ -0,0 +1,130 @@
|
|
1
|
+
require 'benchmark'
|
2
|
+
require 'thread'
|
3
|
+
|
4
|
+
require 'dat-worker-pool'
|
5
|
+
require 'dat-worker-pool/default_queue'
|
6
|
+
|
7
|
+
class BenchRunner
|
8
|
+
|
9
|
+
NUM_WORKERS = 4
|
10
|
+
TIME_MODIFIER = 10 ** 4 # 4 decimal places
|
11
|
+
|
12
|
+
LOGGER = if ENV['DEBUG']
|
13
|
+
Logger.new(File.expand_path("../../log/bench.log", __FILE__)).tap do |l|
|
14
|
+
l.datetime_format = '' # don't show datetime in the logs
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def initialize
|
19
|
+
output_file_path = if ENV['OUTPUT_FILE']
|
20
|
+
File.expand_path(ENV['OUTPUT_FILE'])
|
21
|
+
else
|
22
|
+
File.expand_path('../report.txt', __FILE__)
|
23
|
+
end
|
24
|
+
@output = Output.new(File.open(output_file_path, 'w'))
|
25
|
+
|
26
|
+
@number_of_work_items = ENV['NUM_WORK_ITEMS'] || 100_000
|
27
|
+
|
28
|
+
@mutex = Mutex.new
|
29
|
+
@cond_var = ConditionVariable.new
|
30
|
+
@finished = LockedInteger.new(0)
|
31
|
+
|
32
|
+
@result = nil
|
33
|
+
end
|
34
|
+
|
35
|
+
def run
|
36
|
+
output "Running benchmark report..."
|
37
|
+
output("\n", false)
|
38
|
+
|
39
|
+
benchmark_processing_work_items
|
40
|
+
|
41
|
+
output "\n", false
|
42
|
+
output "Processing #{@number_of_work_items} Work Items: #{@result}ms"
|
43
|
+
|
44
|
+
output "\n"
|
45
|
+
output "Done running benchmark report"
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def benchmark_processing_work_items
|
51
|
+
queue = DatWorkerPool::DefaultQueue.new.tap(&:dwp_start)
|
52
|
+
@number_of_work_items.times.each{ |n| queue.dwp_push(n + 1) }
|
53
|
+
|
54
|
+
worker_pool = DatWorkerPool.new(BenchWorker, {
|
55
|
+
:num_workers => NUM_WORKERS,
|
56
|
+
:logger => LOGGER,
|
57
|
+
:queue => queue,
|
58
|
+
:worker_params => {
|
59
|
+
:mutex => @mutex,
|
60
|
+
:cond_var => @cond_var,
|
61
|
+
:finished => @finished,
|
62
|
+
:output => @output
|
63
|
+
}
|
64
|
+
})
|
65
|
+
benchmark = Benchmark.measure do
|
66
|
+
worker_pool.start
|
67
|
+
while @finished.value != @number_of_work_items
|
68
|
+
@mutex.synchronize{ @cond_var.wait(@mutex) }
|
69
|
+
end
|
70
|
+
worker_pool.shutdown
|
71
|
+
end
|
72
|
+
|
73
|
+
@result = round_and_display(benchmark.real * 1000.to_f)
|
74
|
+
output "\n", false
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def output(message, puts = true)
|
80
|
+
@output.log(message, puts)
|
81
|
+
end
|
82
|
+
|
83
|
+
def round_and_display(time_in_ms)
|
84
|
+
display_time(round_time(time_in_ms))
|
85
|
+
end
|
86
|
+
|
87
|
+
def round_time(time_in_ms)
|
88
|
+
(time_in_ms * TIME_MODIFIER).to_i / TIME_MODIFIER.to_f
|
89
|
+
end
|
90
|
+
|
91
|
+
def display_time(time)
|
92
|
+
integer, fractional = time.to_s.split('.')
|
93
|
+
[integer, fractional.ljust(4, '0')].join('.')
|
94
|
+
end
|
95
|
+
|
96
|
+
class BenchWorker
|
97
|
+
include DatWorkerPool::Worker
|
98
|
+
|
99
|
+
on_available{ signal_main_thread }
|
100
|
+
|
101
|
+
on_error{ params[:output].log('F', false) }
|
102
|
+
|
103
|
+
def work!(n)
|
104
|
+
params[:finished].increment
|
105
|
+
params[:output].log('.', false) if ((n - 1) % 100 == 0)
|
106
|
+
end
|
107
|
+
|
108
|
+
private
|
109
|
+
|
110
|
+
def signal_main_thread
|
111
|
+
params[:mutex].synchronize{ params[:cond_var].signal }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
class Output < Struct.new(:file)
|
116
|
+
def log(message, puts = true)
|
117
|
+
method = puts ? :puts : :print
|
118
|
+
self.send(method, message)
|
119
|
+
self.file.send(method, message)
|
120
|
+
STDOUT.flush if method == :print
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
class LockedInteger < DatWorkerPool::LockedObject
|
125
|
+
def increment; @mutex.synchronize{ @object = @object + 1 }; end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
BenchRunner.new.run
|
data/bench/report.txt
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
Running benchmark report...
|
2
|
+
|
3
|
+
........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
|
4
|
+
|
5
|
+
Processing 100000 Work Items: 3388.7569ms
|
6
|
+
|
7
|
+
Done running benchmark report
|
data/dat-worker-pool.gemspec
CHANGED
data/lib/dat-worker-pool.rb
CHANGED
@@ -1,220 +1,71 @@
|
|
1
1
|
require 'logger'
|
2
|
-
require 'system_timer'
|
3
|
-
require 'thread'
|
4
2
|
|
5
3
|
require 'dat-worker-pool/version'
|
6
4
|
require 'dat-worker-pool/queue'
|
5
|
+
require 'dat-worker-pool/runner'
|
7
6
|
require 'dat-worker-pool/worker'
|
8
7
|
|
9
8
|
class DatWorkerPool
|
10
9
|
|
11
|
-
|
10
|
+
DEFAULT_NUM_WORKERS = 1
|
11
|
+
MIN_WORKERS = 1
|
12
|
+
|
12
13
|
attr_reader :queue
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
@queue
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
@
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
@before_work_callbacks = []
|
38
|
-
@after_work_callbacks = []
|
39
|
-
|
40
|
-
@started = false
|
14
|
+
|
15
|
+
def initialize(worker_class, options = nil)
|
16
|
+
if !worker_class.kind_of?(Class) || !worker_class.include?(DatWorkerPool::Worker)
|
17
|
+
raise ArgumentError, "worker class must include `#{DatWorkerPool::Worker}`"
|
18
|
+
end
|
19
|
+
|
20
|
+
options ||= {}
|
21
|
+
num_workers = (options[:num_workers] || DEFAULT_NUM_WORKERS).to_i
|
22
|
+
if num_workers < MIN_WORKERS
|
23
|
+
raise ArgumentError, "number of workers must be at least #{MIN_WORKERS}"
|
24
|
+
end
|
25
|
+
|
26
|
+
@queue = options[:queue] || begin
|
27
|
+
require 'dat-worker-pool/default_queue'
|
28
|
+
DatWorkerPool::DefaultQueue.new
|
29
|
+
end
|
30
|
+
|
31
|
+
@runner = DatWorkerPool::Runner.new({
|
32
|
+
:num_workers => num_workers,
|
33
|
+
:logger => options[:logger],
|
34
|
+
:queue => @queue,
|
35
|
+
:worker_class => worker_class,
|
36
|
+
:worker_params => options[:worker_params]
|
37
|
+
})
|
41
38
|
end
|
42
39
|
|
43
40
|
def start
|
44
|
-
@
|
45
|
-
@queue.start
|
46
|
-
@min_workers.times{ spawn_worker }
|
41
|
+
@runner.start
|
47
42
|
end
|
48
43
|
|
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
44
|
def shutdown(timeout = nil)
|
52
|
-
@
|
53
|
-
begin
|
54
|
-
OptionalTimeout.new(timeout){ graceful_shutdown }
|
55
|
-
rescue TimeoutError
|
56
|
-
force_shutdown(timeout, caller)
|
57
|
-
end
|
45
|
+
@runner.shutdown(timeout, caller)
|
58
46
|
end
|
59
47
|
|
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
48
|
def add_work(work_item)
|
65
49
|
return if work_item.nil?
|
66
|
-
|
67
|
-
@queue.push work_item
|
68
|
-
spawn_worker if @started && new_worker_needed && !reached_max_workers?
|
50
|
+
@queue.dwp_push work_item
|
69
51
|
end
|
52
|
+
alias :push :add_work
|
70
53
|
|
71
54
|
def work_items
|
72
55
|
@queue.work_items
|
73
56
|
end
|
74
57
|
|
75
|
-
def
|
76
|
-
@
|
77
|
-
end
|
78
|
-
|
79
|
-
def waiting
|
80
|
-
@workers_waiting.count
|
58
|
+
def available_worker_count
|
59
|
+
@runner.available_worker_count
|
81
60
|
end
|
82
61
|
|
83
62
|
def worker_available?
|
84
|
-
|
85
|
-
end
|
86
|
-
|
87
|
-
def all_spawned_workers_are_busy?
|
88
|
-
@workers_waiting.count <= 0
|
89
|
-
end
|
90
|
-
|
91
|
-
def reached_max_workers?
|
92
|
-
@mutex.synchronize{ @spawned >= @max_workers }
|
93
|
-
end
|
94
|
-
|
95
|
-
def on_queue_pop_callbacks; @queue.on_pop_callbacks; end
|
96
|
-
def on_queue_push_callbacks; @queue.on_push_callbacks; end
|
97
|
-
|
98
|
-
def on_queue_pop(&block); @queue.on_pop_callbacks << block; end
|
99
|
-
def on_queue_push(&block); @queue.on_push_callbacks << block; end
|
100
|
-
|
101
|
-
def on_worker_error(&block); @on_worker_error_callbacks << block; end
|
102
|
-
def on_worker_start(&block); @on_worker_start_callbacks << block; end
|
103
|
-
def on_worker_shutdown(&block); @on_worker_shutdown_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
|
106
|
-
|
107
|
-
def before_work(&block); @before_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
|
115
|
-
|
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
|
63
|
+
@runner.worker_available?
|
146
64
|
end
|
147
65
|
|
148
|
-
|
149
|
-
|
150
|
-
|
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
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
def despawn_worker(worker)
|
172
|
-
@mutex.synchronize{ despawn_worker!(worker) }
|
173
|
-
end
|
174
|
-
|
175
|
-
def despawn_worker!(worker)
|
176
|
-
@spawned -= 1
|
177
|
-
@workers.delete worker
|
178
|
-
end
|
179
|
-
|
180
|
-
class WorkersWaiting
|
181
|
-
attr_reader :count
|
182
|
-
|
183
|
-
def initialize
|
184
|
-
@mutex = Mutex.new
|
185
|
-
@count = 0
|
186
|
-
end
|
187
|
-
|
188
|
-
def increment
|
189
|
-
@mutex.synchronize{ @count += 1 }
|
190
|
-
end
|
191
|
-
|
192
|
-
def decrement
|
193
|
-
@mutex.synchronize{ @count -= 1 }
|
194
|
-
end
|
195
|
-
end
|
196
|
-
|
197
|
-
module OptionalTimeout
|
198
|
-
def self.new(seconds, &block)
|
199
|
-
if seconds
|
200
|
-
SystemTimer.timeout(seconds, TimeoutError, &block)
|
201
|
-
else
|
202
|
-
block.call
|
203
|
-
end
|
204
|
-
end
|
205
|
-
end
|
206
|
-
|
207
|
-
module Logger
|
208
|
-
def self.new(debug)
|
209
|
-
debug ? ::Logger.new(STDOUT) : ::Logger.new(File.open('/dev/null', 'w'))
|
210
|
-
end
|
211
|
-
end
|
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.
|
66
|
+
# this error should never be "swallowed", if it is caught be sure to re-raise
|
67
|
+
# it so the workers shutdown; otherwise workers will get killed
|
68
|
+
# (`Thread#kill`) by ruby which can cause a problems
|
218
69
|
ShutdownError = Class.new(Interrupt)
|
219
70
|
|
220
71
|
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'thread'
|
2
|
+
require 'dat-worker-pool/locked_object'
|
3
|
+
require 'dat-worker-pool/queue'
|
4
|
+
|
5
|
+
class DatWorkerPool
|
6
|
+
|
7
|
+
class DefaultQueue
|
8
|
+
include DatWorkerPool::Queue
|
9
|
+
|
10
|
+
attr_reader :on_push_callbacks, :on_pop_callbacks
|
11
|
+
|
12
|
+
def initialize
|
13
|
+
@work_items = LockedArray.new
|
14
|
+
@cond_var = ConditionVariable.new
|
15
|
+
|
16
|
+
@on_push_callbacks = []
|
17
|
+
@on_pop_callbacks = []
|
18
|
+
end
|
19
|
+
|
20
|
+
def work_items; @work_items.values; end
|
21
|
+
def empty?; @work_items.empty?; end
|
22
|
+
|
23
|
+
def on_push(&block); @on_push_callbacks << block; end
|
24
|
+
def on_pop(&block); @on_pop_callbacks << block; end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
# wake up workers (`@cond_var.broadcast`) who are sleeping because of `pop`
|
29
|
+
def shutdown!
|
30
|
+
@work_items.with_lock{ @cond_var.broadcast }
|
31
|
+
end
|
32
|
+
|
33
|
+
# add the work item and wakeup (`@cond_var.signal`) the first sleeping
|
34
|
+
# worker (from calling `pop`)
|
35
|
+
def push!(work_item)
|
36
|
+
@work_items.with_lock do |mutex, work_items|
|
37
|
+
work_items << work_item
|
38
|
+
@cond_var.signal
|
39
|
+
end
|
40
|
+
@on_push_callbacks.each{ |p| p.call(self, work_item) }
|
41
|
+
end
|
42
|
+
|
43
|
+
# check if the queue is empty, if so sleep (`@cond_var.wait(@mutex)`) until
|
44
|
+
# signaled (via `push!` or `shutdown!`); once a work item is available pop
|
45
|
+
# it from the front and return it; if shutdown, return `nil` which the
|
46
|
+
# workers will ignore
|
47
|
+
def pop!
|
48
|
+
work_item = @work_items.with_lock do |mutex, work_items|
|
49
|
+
while !self.shutdown? && work_items.empty?
|
50
|
+
@cond_var.wait(mutex)
|
51
|
+
end
|
52
|
+
work_items.shift unless self.shutdown?
|
53
|
+
end
|
54
|
+
@on_pop_callbacks.each{ |p| p.call(self, work_item) } if !work_item.nil?
|
55
|
+
work_item
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
|
60
|
+
end
|