dat-worker-pool 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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