dat-worker-pool 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/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