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 CHANGED
@@ -3,4 +3,4 @@ source "https://rubygems.org"
3
3
  gemspec
4
4
 
5
5
  gem 'rake'
6
- gem 'pry'
6
+ gem 'pry', "~> 0.9.0"
@@ -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
@@ -0,0 +1,7 @@
1
+ Running benchmark report...
2
+
3
+ ........................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................................
4
+
5
+ Processing 100000 Work Items: 3388.7569ms
6
+
7
+ Done running benchmark report
@@ -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.14"])
23
+ gem.add_development_dependency("assert", ["~> 2.15"])
24
24
 
25
25
  end
@@ -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
- attr_reader :logger, :spawned
10
+ DEFAULT_NUM_WORKERS = 1
11
+ MIN_WORKERS = 1
12
+
12
13
  attr_reader :queue
13
- attr_reader :on_worker_error_callbacks
14
- attr_reader :on_worker_start_callbacks, :on_worker_shutdown_callbacks
15
- attr_reader :on_worker_sleep_callbacks, :on_worker_wakeup_callbacks
16
- attr_reader :before_work_callbacks, :after_work_callbacks
17
-
18
- def initialize(min = 0, max = 1, debug = false, &do_work_proc)
19
- @min_workers = min
20
- @max_workers = max
21
- @debug = debug
22
- @logger = Logger.new(@debug)
23
- @do_work_proc = do_work_proc
24
-
25
- @queue = Queue.new
26
- @workers_waiting = WorkersWaiting.new
27
-
28
- @mutex = Mutex.new
29
- @workers = []
30
- @spawned = 0
31
-
32
- @on_worker_error_callbacks = []
33
- @on_worker_start_callbacks = []
34
- @on_worker_shutdown_callbacks = [proc{ |worker| despawn_worker(worker) }]
35
- @on_worker_sleep_callbacks = [proc{ @workers_waiting.increment }]
36
- @on_worker_wakeup_callbacks = [proc{ @workers_waiting.decrement }]
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
- @started = true
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
- @started = false
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
- 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?
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 queue_empty?
76
- @queue.empty?
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
- !reached_max_workers? || @workers_waiting.count > 0
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
- def spawn_worker
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
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