dat-worker-pool 0.1.0 → 0.2.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.
@@ -11,6 +11,7 @@ Gem::Specification.new do |gem|
11
11
  gem.description = "A simple thread pool for processing generic 'work'"
12
12
  gem.summary = "A simple thread pool for processing generic 'work'"
13
13
  gem.homepage = "http://github.com/redding/dat-worker-pool"
14
+ gem.license = 'MIT'
14
15
 
15
16
  gem.files = `git ls-files`.split($/)
16
17
  gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
@@ -37,6 +37,22 @@ class DatWorkerPool
37
37
  @workers_waiting.count
38
38
  end
39
39
 
40
+ def worker_available?
41
+ !reached_max_workers? || @workers_waiting.count > 0
42
+ end
43
+
44
+ def all_spawned_workers_are_busy?
45
+ @workers_waiting.count <= 0
46
+ end
47
+
48
+ def reached_max_workers?
49
+ @mutex.synchronize{ @spawned >= @max_workers }
50
+ end
51
+
52
+ def queue_empty?
53
+ @queue.empty?
54
+ end
55
+
40
56
  # Check if all workers are busy before adding the work. When the work is
41
57
  # added, a worker will stop waiting (if it was idle). Because of that, we
42
58
  # can't reliably check if all workers are busy. We might think all workers are
@@ -44,9 +60,9 @@ class DatWorkerPool
44
60
  # would spawn a worker to do nothing.
45
61
  def add_work(work_item)
46
62
  return if work_item.nil?
47
- new_worker_needed = all_workers_are_busy?
63
+ new_worker_needed = all_spawned_workers_are_busy?
48
64
  @queue.push work_item
49
- self.spawn_worker if new_worker_needed && havent_reached_max_workers?
65
+ self.spawn_worker if new_worker_needed && !reached_max_workers?
50
66
  end
51
67
 
52
68
  # Shutdown each worker and then the queue. Shutting down the queue will
@@ -55,9 +71,9 @@ class DatWorkerPool
55
71
  # finish.
56
72
  # **NOTE** Any work that is left on the queue isn't processed. The controlling
57
73
  # application for the worker pool should gracefully handle these items.
58
- def shutdown(timeout)
74
+ def shutdown(timeout = nil)
59
75
  begin
60
- SystemTimer.timeout(timeout, TimeoutError) do
76
+ proc = OptionalTimeoutProc.new(timeout, true) do
61
77
  @workers.each(&:shutdown)
62
78
  @queue.shutdown
63
79
 
@@ -68,51 +84,44 @@ class DatWorkerPool
68
84
  # `each`).
69
85
  @workers.first.join until @workers.empty?
70
86
  end
87
+ proc.call
71
88
  rescue TimeoutError => exception
72
89
  exception.message.replace "Timed out shutting down the worker pool"
73
90
  @debug ? raise(exception) : self.logger.error(exception.message)
74
91
  end
75
92
  end
76
93
 
77
- # public, because workers need to call it for themselves
78
- def despawn_worker(worker)
79
- @mutex.synchronize do
80
- @spawned -= 1
81
- @workers.delete worker
82
- end
83
- end
84
-
85
94
  protected
86
95
 
87
96
  def spawn_worker
88
97
  @mutex.synchronize do
89
- worker = Worker.new(self, @queue, @workers_waiting) do |work_item|
90
- do_work(work_item)
98
+ Worker.new(@queue).tap do |w|
99
+ w.on_work = proc{ |work_item| do_work(work_item) }
100
+ w.on_waiting = proc{ @workers_waiting.increment }
101
+ w.on_continuing = proc{ @workers_waiting.decrement }
102
+ w.on_shutdown = proc{ |worker| despawn_worker(worker) }
103
+
104
+ @workers << w
105
+ @spawned += 1
106
+
107
+ w.start
91
108
  end
92
- @workers << worker
93
- @spawned += 1
94
- worker
95
109
  end
96
110
  end
97
111
 
98
- def do_work(work_item)
99
- begin
100
- @do_work_proc.call(work_item)
101
- rescue Exception => exception
102
- self.logger.error "Exception raised while doing work!"
103
- self.logger.error "#{exception.class}: #{exception.message}"
104
- self.logger.error exception.backtrace.join("\n")
112
+ def despawn_worker(worker)
113
+ @mutex.synchronize do
114
+ @spawned -= 1
115
+ @workers.delete worker
105
116
  end
106
117
  end
107
118
 
108
- def all_workers_are_busy?
109
- @workers_waiting.count <= 0
110
- end
111
-
112
- def havent_reached_max_workers?
113
- @mutex.synchronize do
114
- @spawned < @max_workers
115
- end
119
+ def do_work(work_item)
120
+ @do_work_proc.call(work_item)
121
+ rescue Exception => exception
122
+ self.logger.error "Exception raised while doing work!"
123
+ self.logger.error "#{exception.class}: #{exception.message}"
124
+ self.logger.error exception.backtrace.join("\n")
116
125
  end
117
126
 
118
127
  class WorkersWaiting
@@ -133,6 +142,26 @@ class DatWorkerPool
133
142
 
134
143
  end
135
144
 
145
+ class OptionalTimeoutProc
146
+ def initialize(timeout, reraise = false, &proc)
147
+ @timeout = timeout
148
+ @reraise = reraise
149
+ @proc = proc
150
+ end
151
+
152
+ def call
153
+ if @timeout
154
+ begin
155
+ SystemTimer.timeout(@timeout, TimeoutError, &@proc)
156
+ rescue TimeoutError
157
+ raise if @reraise
158
+ end
159
+ else
160
+ @proc.call
161
+ end
162
+ end
163
+ end
164
+
136
165
  module Logger
137
166
  def self.new(debug)
138
167
  debug ? ::Logger.new(STDOUT) : ::Logger.new(File.open('/dev/null', 'w'))
@@ -26,19 +26,17 @@ class DatWorkerPool
26
26
  end
27
27
 
28
28
  def pop
29
- @mutex.synchronize{ @work_items.shift }
29
+ return if @shutdown
30
+ @mutex.synchronize do
31
+ @condition_variable.wait(@mutex) while !@shutdown && @work_items.empty?
32
+ @work_items.shift
33
+ end
30
34
  end
31
35
 
32
36
  def empty?
33
37
  @mutex.synchronize{ @work_items.empty? }
34
38
  end
35
39
 
36
- # wait to be signaled by `push`
37
- def wait_for_work_item
38
- return if @shutdown
39
- @mutex.synchronize{ @condition_variable.wait(@mutex) }
40
- end
41
-
42
40
  # wake up any workers who are idle (because of `wait_for_work_item`)
43
41
  def shutdown
44
42
  @shutdown = true
@@ -1,3 +1,3 @@
1
1
  class DatWorkerPool
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -3,14 +3,25 @@ require 'thread'
3
3
  class DatWorkerPool
4
4
 
5
5
  class Worker
6
+ attr_writer :on_work, :on_waiting, :on_continuing, :on_shutdown
6
7
 
7
- def initialize(pool, queue, workers_waiting, &block)
8
- @pool = pool
9
- @queue = queue
10
- @workers_waiting = workers_waiting
11
- @block = block
12
- @shutdown = false
13
- @thread = Thread.new{ work_loop }
8
+ def initialize(queue)
9
+ @queue = queue
10
+ @on_work = proc{ |work_item| }
11
+ @on_waiting = proc{ |worker| }
12
+ @on_continuing = proc{ |worker| }
13
+ @on_shutdown = proc{ |worker| }
14
+
15
+ @shutdown = false
16
+ @thread = nil
17
+ end
18
+
19
+ def start
20
+ @thread ||= Thread.new{ work_loop }
21
+ end
22
+
23
+ def running?
24
+ @thread && @thread.alive?
14
25
  end
15
26
 
16
27
  def shutdown
@@ -18,29 +29,22 @@ class DatWorkerPool
18
29
  end
19
30
 
20
31
  def join(*args)
21
- @thread.join(*args) if @thread
32
+ @thread.join(*args) if running?
22
33
  end
23
34
 
24
35
  protected
25
36
 
26
37
  def work_loop
27
38
  loop do
28
- self.wait_for_work
39
+ @on_waiting.call(self)
40
+ work_item = @queue.pop
41
+ @on_continuing.call(self)
29
42
  break if @shutdown
30
- @block.call @queue.pop
43
+ @on_work.call(work_item) if work_item
31
44
  end
32
45
  ensure
33
- @pool.despawn_worker(self)
34
- end
35
-
36
- # Wait for work to process by checking if the queue is empty.
37
- def wait_for_work
38
- while @queue.empty?
39
- return if @shutdown
40
- @workers_waiting.increment
41
- @queue.wait_for_work_item
42
- @workers_waiting.decrement
43
- end
46
+ @on_shutdown.call(self)
47
+ @thread = nil
44
48
  end
45
49
 
46
50
  end
@@ -3,7 +3,7 @@ require 'dat-worker-pool'
3
3
 
4
4
  class DatWorkerPool
5
5
 
6
- class BaseTests < Assert::Context
6
+ class UnitTests < Assert::Context
7
7
  desc "DatWorkerPool"
8
8
  setup do
9
9
  @work_pool = DatWorkerPool.new{ }
@@ -13,34 +13,49 @@ class DatWorkerPool
13
13
  should have_readers :logger, :spawned
14
14
  should have_imeths :add_work, :shutdown, :despawn_worker
15
15
  should have_imeths :work_items, :waiting
16
+ should have_imeths :worker_available?, :all_spawned_workers_are_busy?
17
+ should have_imeths :reached_max_workers?
18
+ should have_imeths :queue_empty?
16
19
 
17
20
  end
18
21
 
19
- class WorkerBehaviorTests < BaseTests
22
+ class WorkerBehaviorTests < UnitTests
20
23
  desc "workers"
21
24
  setup do
22
- @work_pool = DatWorkerPool.new(1, 2, true){|work| sleep(work) }
25
+ @work_pool = DatWorkerPool.new(1, 2, true){ |work| sleep(work) }
23
26
  end
24
27
 
25
28
  should "be created as needed and only go up to the maximum number allowed" do
26
29
  # the minimum should be spawned and waiting
27
30
  assert_equal 1, @work_pool.spawned
28
31
  assert_equal 1, @work_pool.waiting
32
+ assert_equal true, @work_pool.worker_available?
33
+ assert_equal false, @work_pool.all_spawned_workers_are_busy?
34
+ assert_equal false, @work_pool.reached_max_workers?
29
35
 
30
36
  # the minimum should be spawned, but no longer waiting
31
37
  @work_pool.add_work 5
32
38
  assert_equal 1, @work_pool.spawned
33
39
  assert_equal 0, @work_pool.waiting
40
+ assert_equal true, @work_pool.worker_available?
41
+ assert_equal true, @work_pool.all_spawned_workers_are_busy?
42
+ assert_equal false, @work_pool.reached_max_workers?
34
43
 
35
44
  # an additional worker should be spawned
36
45
  @work_pool.add_work 5
37
46
  assert_equal 2, @work_pool.spawned
38
47
  assert_equal 0, @work_pool.waiting
48
+ assert_equal false, @work_pool.worker_available?
49
+ assert_equal true, @work_pool.all_spawned_workers_are_busy?
50
+ assert_equal true, @work_pool.reached_max_workers?
39
51
 
40
52
  # no additional workers are spawned, the work waits to be processed
41
53
  @work_pool.add_work 5
42
54
  assert_equal 2, @work_pool.spawned
43
55
  assert_equal 0, @work_pool.waiting
56
+ assert_equal false, @work_pool.worker_available?
57
+ assert_equal true, @work_pool.all_spawned_workers_are_busy?
58
+ assert_equal true, @work_pool.reached_max_workers?
44
59
  end
45
60
 
46
61
  should "go back to waiting when they finish working" do
@@ -59,7 +74,20 @@ class DatWorkerPool
59
74
 
60
75
  end
61
76
 
62
- class AddWorkAndProcessItTests < BaseTests
77
+ class AddWorkWithNoWorkersTests < UnitTests
78
+ setup do
79
+ @work_pool = DatWorkerPool.new(0, 0){ |work| }
80
+ end
81
+
82
+ should "return whether or not the queue is empty" do
83
+ assert_equal true, @work_pool.queue_empty?
84
+ @work_pool.add_work 'test'
85
+ assert_equal false, @work_pool.queue_empty?
86
+ end
87
+
88
+ end
89
+
90
+ class AddWorkAndProcessItTests < UnitTests
63
91
  desc "add_work and process"
64
92
  setup do
65
93
  @result = nil
@@ -84,7 +112,7 @@ class DatWorkerPool
84
112
 
85
113
  end
86
114
 
87
- class ShutdownTests < BaseTests
115
+ class ShutdownTests < UnitTests
88
116
  desc "shutdown"
89
117
  setup do
90
118
  @mutex = Mutex.new
@@ -101,7 +129,6 @@ class DatWorkerPool
101
129
  should "allow any work that has been picked up to be processed" do
102
130
  # make sure the workers haven't processed any work
103
131
  assert_equal [], @finished
104
-
105
132
  subject.shutdown(5)
106
133
 
107
134
  # NOTE, the last work shouldn't have been processed, as it wasn't
@@ -123,6 +150,13 @@ class DatWorkerPool
123
150
  end
124
151
  end
125
152
 
153
+ should "allow jobs to finish by not providing a shutdown timeout" do
154
+ assert_equal [], @finished
155
+ subject.shutdown
156
+ assert_includes 'a', @finished
157
+ assert_includes 'b', @finished
158
+ end
159
+
126
160
  end
127
161
 
128
162
  end
@@ -3,7 +3,7 @@ require 'dat-worker-pool/queue'
3
3
 
4
4
  class DatWorkerPool::Queue
5
5
 
6
- class BaseTests < Assert::Context
6
+ class UnitTests < Assert::Context
7
7
  desc "DatWorkerPool::Queue"
8
8
  setup do
9
9
  @queue = DatWorkerPool::Queue.new
@@ -11,7 +11,7 @@ class DatWorkerPool::Queue
11
11
  subject{ @queue }
12
12
 
13
13
  should have_imeths :work_items, :push, :pop, :empty?
14
- should have_imeths :wait_for_work_item, :shutdown
14
+ should have_imeths :shutdown
15
15
 
16
16
  should "allow pushing work items onto the queue with #push" do
17
17
  subject.push 'work'
@@ -34,6 +34,12 @@ class DatWorkerPool::Queue
34
34
  assert_equal 0, subject.work_items.size
35
35
  end
36
36
 
37
+ should "return nothing with pop when the queue has been shutdown" do
38
+ subject.push 'work1'
39
+ subject.shutdown
40
+ assert_nil subject.pop
41
+ end
42
+
37
43
  should "return whether the queue is empty or not with #empty?" do
38
44
  assert subject.empty?
39
45
  subject.push 'work'
@@ -42,25 +48,32 @@ class DatWorkerPool::Queue
42
48
  assert subject.empty?
43
49
  end
44
50
 
45
- should "sleep a thread with #wait_for_work_item and " \
46
- "wake it up with #push" do
47
- thread = Thread.new{ subject.wait_for_work_item }
48
- thread.join(0.1) # ensure the thread runs enough to start waiting
49
- subject.push 'work'
50
- # if this returns nil, then the thread never finished
51
- assert_not_nil thread.join(1)
51
+ end
52
+
53
+ class SignallingTests < UnitTests
54
+ desc "mutex and condition variable behavior"
55
+ setup do
56
+ @thread = Thread.new do
57
+ Thread.current['work_item'] = @queue.pop || 'got nothing'
58
+ end
59
+ end
60
+
61
+ should "have threads wait for a work item to be added when using pop" do
62
+ assert_equal "sleep", @thread.status
63
+ end
64
+
65
+ should "wakeup threads when work is pushed onto the queue" do
66
+ subject.push 'some work'
67
+ sleep 0.1
68
+ assert !@thread.alive?
69
+ assert_equal 'some work', @thread['work_item']
52
70
  end
53
71
 
54
- should "sleep threads with #wait_for_work_item and " \
55
- "wake them all up with #shutdown" do
56
- thread1 = Thread.new{ subject.wait_for_work_item }
57
- thread1.join(0.1) # ensure the thread runs enough to start waiting
58
- thread2 = Thread.new{ subject.wait_for_work_item }
59
- thread2.join(0.1) # ensure the thread runs enough to start waiting
72
+ should "wakeup thread when the queue is shutdown" do
60
73
  subject.shutdown
61
- # if these returns nil, then the threads never finished
62
- assert_not_nil thread1.join(1)
63
- assert_not_nil thread2.join(1)
74
+ sleep 0.1
75
+ assert !@thread.alive?
76
+ assert_equal 'got nothing', @thread['work_item']
64
77
  end
65
78
 
66
79
  end
@@ -6,24 +6,108 @@ require 'dat-worker-pool/queue'
6
6
 
7
7
  class DatWorkerPool::Worker
8
8
 
9
- class BaseTests < Assert::Context
9
+ class UnitTests < Assert::Context
10
10
  desc "DatWorkerPool::Worker"
11
11
  setup do
12
- @pool = DatWorkerPool.new{ }
13
12
  @queue = DatWorkerPool::Queue.new
14
- @workers_waiting = DatWorkerPool::WorkersWaiting.new
15
- @worker = DatWorkerPool::Worker.new(@pool, @queue, @workers_waiting){ }
13
+ @work_done = []
14
+ @worker = DatWorkerPool::Worker.new(@queue).tap do |w|
15
+ w.on_work = proc{ |work| @work_done << work }
16
+ end
17
+ end
18
+ teardown do
19
+ @worker.shutdown
20
+ @queue.shutdown
21
+ @worker.join
16
22
  end
17
23
  subject{ @worker }
18
24
 
19
- should have_imeths :shutdown, :join
25
+ should have_writers :on_waiting, :on_continuing, :on_shutdown
26
+ should have_imeths :start, :shutdown, :join, :running?
27
+
28
+ should "start a thread with it's work loop with #start" do
29
+ thread = nil
30
+ assert_nothing_raised{ thread = subject.start }
31
+ assert_instance_of Thread, thread
32
+ assert thread.alive?
33
+ assert subject.running?
34
+ end
35
+
36
+ should "call the block it's passed when it get's work from the queue" do
37
+ subject.start
38
+ @queue.push 'one'
39
+ subject.join 0.1 # trigger the worker's thread to run
40
+ @queue.push 'two'
41
+ subject.join 0.1 # trigger the worker's thread to run
42
+ assert_equal [ 'one', 'two' ], @work_done
43
+ end
44
+
45
+ should "flag itself for exiting it's work loop with #shutdown and " \
46
+ "end it's thread once it's queue is shutdown" do
47
+ thread = subject.start
48
+ subject.join 0.1 # trigger the worker's thread to run, allow it to get into it's
49
+ # work loop
50
+ assert_nothing_raised{ subject.shutdown }
51
+ @queue.shutdown
52
+
53
+ subject.join 0.1 # trigger the worker's thread to run, should exit
54
+ assert_not thread.alive?
55
+ assert_not subject.running?
56
+ end
57
+
58
+ end
59
+
60
+ class CallbacksTests < UnitTests
61
+ desc "callbacks"
62
+ setup do
63
+ @worker = DatWorkerPool::Worker.new(@queue).tap do |w|
64
+ w.on_work = proc{ |work| sleep 0.2 }
65
+ end
66
+ end
67
+
68
+ should "call the on waiting callback, yielding itself, when " \
69
+ "it's waiting on work from the queue" do
70
+ waiting, yielded_worker = nil, nil
71
+ subject.on_waiting = proc do |worker|
72
+ waiting = true
73
+ yielded_worker = worker
74
+ end
75
+ subject.start
76
+ subject.join 0.1 # trigger the worker's thread to run
77
+
78
+ assert_equal true, waiting
79
+ assert_equal subject, yielded_worker
80
+ end
81
+
82
+ should "call the on continuing callback, yielding itself, when " \
83
+ "it's done waiting for work from the queue" do
84
+ waiting, yielded_worker = nil, nil
85
+ subject.on_continuing = proc do |worker|
86
+ waiting = false
87
+ yielded_worker = worker
88
+ end
89
+ subject.start
90
+ @queue.push 'some work'
91
+ subject.join 0.1 # trigger the worker's thread to run
20
92
 
21
- should "trigger exiting it's work loop with #shutdown and " \
22
- "join it's thread with #join" do
23
- @queue.shutdown # ensure the thread is not waiting on the queue
24
- subject.join(0.1) # ensure the thread is looping for work
93
+ assert_equal false, waiting
94
+ assert_equal subject, yielded_worker
95
+ end
96
+
97
+ should "call the on shutdown callback, yielding itself, when " \
98
+ "it's shutdown" do
99
+ shutdown, yielded_worker = nil, nil
100
+ subject.on_shutdown = proc do |worker|
101
+ shutdown = true
102
+ yielded_worker = worker
103
+ end
104
+ subject.start
25
105
  subject.shutdown
26
- assert_not_nil subject.join(1)
106
+ @queue.shutdown
107
+ subject.join 0.1 # trigger the worker's thread to run
108
+
109
+ assert_equal true, shutdown
110
+ assert_equal subject, yielded_worker
27
111
  end
28
112
 
29
113
  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: 27
4
+ hash: 23
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 0.1.0
10
+ version: 0.2.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Collin Redding
@@ -16,10 +16,9 @@ autorequire:
16
16
  bindir: bin
17
17
  cert_chain: []
18
18
 
19
- date: 2013-07-15 00:00:00 Z
19
+ date: 2013-10-01 00:00:00 Z
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
22
- prerelease: false
23
22
  version_requirements: &id001 !ruby/object:Gem::Requirement
24
23
  none: false
25
24
  requirements:
@@ -30,11 +29,11 @@ dependencies:
30
29
  - 1
31
30
  - 2
32
31
  version: "1.2"
32
+ type: :runtime
33
33
  requirement: *id001
34
+ prerelease: false
34
35
  name: SystemTimer
35
- type: :runtime
36
36
  - !ruby/object:Gem::Dependency
37
- prerelease: false
38
37
  version_requirements: &id002 !ruby/object:Gem::Requirement
39
38
  none: false
40
39
  requirements:
@@ -44,9 +43,10 @@ dependencies:
44
43
  segments:
45
44
  - 0
46
45
  version: "0"
46
+ type: :development
47
47
  requirement: *id002
48
+ prerelease: false
48
49
  name: assert
49
- type: :development
50
50
  description: A simple thread pool for processing generic 'work'
51
51
  email:
52
52
  - collin.redding@me.com
@@ -76,8 +76,8 @@ files:
76
76
  - test/unit/worker_tests.rb
77
77
  - tmp/.gitkeep
78
78
  homepage: http://github.com/redding/dat-worker-pool
79
- licenses: []
80
-
79
+ licenses:
80
+ - MIT
81
81
  post_install_message:
82
82
  rdoc_options: []
83
83