dat-worker-pool 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,19 @@
1
+ *.gem
2
+ *.log
3
+ *.rbc
4
+ .rbx/
5
+ .bundle
6
+ .config
7
+ .yardoc
8
+ Gemfile.lock
9
+ InstalledFiles
10
+ _yardoc
11
+ coverage
12
+ doc/
13
+ lib/bundler/man
14
+ pkg
15
+ rdoc
16
+ spec/reports
17
+ test/tmp
18
+ test/version_tmp
19
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem 'rake'
6
+ gem 'pry'
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013-Present Collin Redding and Kelly Redding
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # DatWorkerPool
2
+
3
+ A simple thread pool for processing generic 'work'.
4
+
5
+ ## Usage
6
+
7
+ TODO: Write code samples and usage instructions here
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ gem 'dat-worker-pool'
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install dat-worker-pool
22
+
23
+ ## Contributing
24
+
25
+ 1. Fork it
26
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
27
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
28
+ 4. Push to the branch (`git push origin my-new-feature`)
29
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
@@ -0,0 +1,24 @@
1
+ # -*- encoding: utf-8 -*-
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "dat-worker-pool/version"
5
+
6
+ Gem::Specification.new do |gem|
7
+ gem.name = "dat-worker-pool"
8
+ gem.version = DatWorkerPool::VERSION
9
+ gem.authors = ["Collin Redding", "Kelly Redding"]
10
+ gem.email = ["collin.redding@me.com", "kelly@kellyredding.com"]
11
+ gem.description = "A simple thread pool for processing generic 'work'"
12
+ gem.summary = "A simple thread pool for processing generic 'work'"
13
+ gem.homepage = "http://github.com/redding/dat-worker-pool"
14
+
15
+ gem.files = `git ls-files`.split($/)
16
+ gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
17
+ gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
18
+ gem.require_paths = ["lib"]
19
+
20
+ gem.add_dependency("SystemTimer", ["~> 1.2"])
21
+
22
+ gem.add_development_dependency("assert")
23
+
24
+ end
@@ -0,0 +1,142 @@
1
+ require 'logger'
2
+ require 'system_timer'
3
+ require 'thread'
4
+
5
+ require 'dat-worker-pool/version'
6
+ require 'dat-worker-pool/queue'
7
+ require 'dat-worker-pool/worker'
8
+
9
+ class DatWorkerPool
10
+
11
+ TimeoutError = Class.new(RuntimeError)
12
+
13
+ attr_reader :logger, :spawned
14
+
15
+ def initialize(min = 0, max = 1, debug = false, &do_work_proc)
16
+ @min_workers = min
17
+ @max_workers = max
18
+ @debug = debug
19
+ @logger = Logger.new(@debug)
20
+ @do_work_proc = do_work_proc
21
+
22
+ @queue = Queue.new
23
+ @workers_waiting = WorkersWaiting.new
24
+
25
+ @mutex = Mutex.new
26
+ @workers = []
27
+ @spawned = 0
28
+
29
+ @min_workers.times{ self.spawn_worker }
30
+ end
31
+
32
+ def work_items
33
+ @queue.work_items
34
+ end
35
+
36
+ def waiting
37
+ @workers_waiting.count
38
+ end
39
+
40
+ # Check if all workers are busy before adding the work. When the work is
41
+ # added, a worker will stop waiting (if it was idle). Because of that, we
42
+ # can't reliably check if all workers are busy. We might think all workers are
43
+ # busy because we just woke up a sleeping worker to process this work. Then we
44
+ # would spawn a worker to do nothing.
45
+ def add_work(work_item)
46
+ return if work_item.nil?
47
+ new_worker_needed = all_workers_are_busy?
48
+ @queue.push work_item
49
+ self.spawn_worker if new_worker_needed && havent_reached_max_workers?
50
+ end
51
+
52
+ # Shutdown each worker and then the queue. Shutting down the queue will
53
+ # signal any workers waiting on it to wake up, so they can start shutting
54
+ # down. If a worker is processing work, then it will be joined and allowed to
55
+ # finish.
56
+ # **NOTE** Any work that is left on the queue isn't processed. The controlling
57
+ # application for the worker pool should gracefully handle these items.
58
+ def shutdown(timeout)
59
+ begin
60
+ SystemTimer.timeout(timeout, TimeoutError) do
61
+ @workers.each(&:shutdown)
62
+ @queue.shutdown
63
+
64
+ # use this pattern instead of `each` -- we don't want to call `join` on
65
+ # every worker (especially if they are shutting down on their own), we
66
+ # just want to make sure that any who haven't had a chance to finish
67
+ # get to (this is safe, otherwise you might get a dead thread in the
68
+ # `each`).
69
+ @workers.first.join until @workers.empty?
70
+ end
71
+ rescue TimeoutError => exception
72
+ exception.message.replace "Timed out shutting down the worker pool"
73
+ @debug ? raise(exception) : self.logger.error(exception.message)
74
+ end
75
+ end
76
+
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
+ protected
86
+
87
+ def spawn_worker
88
+ @mutex.synchronize do
89
+ worker = Worker.new(self, @queue, @workers_waiting) do |work_item|
90
+ do_work(work_item)
91
+ end
92
+ @workers << worker
93
+ @spawned += 1
94
+ worker
95
+ end
96
+ end
97
+
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")
105
+ end
106
+ end
107
+
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
116
+ end
117
+
118
+ class WorkersWaiting
119
+ attr_reader :count
120
+
121
+ def initialize
122
+ @mutex = Mutex.new
123
+ @count = 0
124
+ end
125
+
126
+ def increment
127
+ @mutex.synchronize{ @count += 1 }
128
+ end
129
+
130
+ def decrement
131
+ @mutex.synchronize{ @count -= 1 }
132
+ end
133
+
134
+ end
135
+
136
+ module Logger
137
+ def self.new(debug)
138
+ debug ? ::Logger.new(STDOUT) : ::Logger.new(File.open('/dev/null', 'w'))
139
+ end
140
+ end
141
+
142
+ end
@@ -0,0 +1,50 @@
1
+ require 'thread'
2
+
3
+ class DatWorkerPool
4
+
5
+ class Queue
6
+
7
+ def initialize
8
+ @work_items = []
9
+ @shutdown = false
10
+ @mutex = Mutex.new
11
+ @condition_variable = ConditionVariable.new
12
+ end
13
+
14
+ def work_items
15
+ @mutex.synchronize{ @work_items }
16
+ end
17
+
18
+ # Add the work_item and wake up the first worker (the `signal`) that's
19
+ # waiting (because of `wait_for_work_item`)
20
+ def push(work_item)
21
+ raise "Unable to add work while shutting down" if @shutdown
22
+ @mutex.synchronize do
23
+ @work_items << work_item
24
+ @condition_variable.signal
25
+ end
26
+ end
27
+
28
+ def pop
29
+ @mutex.synchronize{ @work_items.shift }
30
+ end
31
+
32
+ def empty?
33
+ @mutex.synchronize{ @work_items.empty? }
34
+ end
35
+
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
+ # wake up any workers who are idle (because of `wait_for_work_item`)
43
+ def shutdown
44
+ @shutdown = true
45
+ @mutex.synchronize{ @condition_variable.broadcast }
46
+ end
47
+
48
+ end
49
+
50
+ end
@@ -0,0 +1,3 @@
1
+ class DatWorkerPool
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,48 @@
1
+ require 'thread'
2
+
3
+ class DatWorkerPool
4
+
5
+ class Worker
6
+
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 }
14
+ end
15
+
16
+ def shutdown
17
+ @shutdown = true
18
+ end
19
+
20
+ def join(*args)
21
+ @thread.join(*args) if @thread
22
+ end
23
+
24
+ protected
25
+
26
+ def work_loop
27
+ loop do
28
+ self.wait_for_work
29
+ break if @shutdown
30
+ @block.call @queue.pop
31
+ end
32
+ 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
44
+ end
45
+
46
+ end
47
+
48
+ end
data/log/.gitkeep ADDED
File without changes
data/test/helper.rb ADDED
@@ -0,0 +1,10 @@
1
+ # this file is automatically required when you run `assert`
2
+ # put any test helpers here
3
+
4
+ # add the root dir to the load path
5
+ $LOAD_PATH.unshift(File.expand_path("../..", __FILE__))
6
+
7
+ # require pry for debugging (`binding.pry`)
8
+ require 'pry'
9
+
10
+ # TODO: put test helpers here...
@@ -0,0 +1,33 @@
1
+ require 'assert'
2
+ require 'dat-worker-pool'
3
+
4
+ class UseWorkerPoolTests < Assert::Context
5
+
6
+ desc "defining a custom worker pool"
7
+ setup do
8
+ @mutex = Mutex.new
9
+ @results = []
10
+ @work_pool = DatWorkerPool.new(1, 2, !!ENV['DEBUG']) do |work|
11
+ @mutex.synchronize{ @results << (work * 100) }
12
+ end
13
+ end
14
+
15
+ should "be able to add work, have it processed and stop the pool" do
16
+ @work_pool.add_work 1
17
+ @work_pool.add_work 5
18
+ @work_pool.add_work 2
19
+ @work_pool.add_work 4
20
+ @work_pool.add_work 3
21
+
22
+ sleep 0.1 # allow the worker threads to run
23
+
24
+ @work_pool.shutdown(1)
25
+
26
+ assert_includes 100, @results
27
+ assert_includes 200, @results
28
+ assert_includes 300, @results
29
+ assert_includes 400, @results
30
+ assert_includes 500, @results
31
+ end
32
+
33
+ end
@@ -0,0 +1,128 @@
1
+ require 'assert'
2
+ require 'dat-worker-pool'
3
+
4
+ class DatWorkerPool
5
+
6
+ class BaseTests < Assert::Context
7
+ desc "DatWorkerPool"
8
+ setup do
9
+ @work_pool = DatWorkerPool.new{ }
10
+ end
11
+ subject{ @work_pool }
12
+
13
+ should have_readers :logger, :spawned
14
+ should have_imeths :add_work, :shutdown, :despawn_worker
15
+ should have_imeths :work_items, :waiting
16
+
17
+ end
18
+
19
+ class WorkerBehaviorTests < BaseTests
20
+ desc "workers"
21
+ setup do
22
+ @work_pool = DatWorkerPool.new(1, 2, true){|work| sleep(work) }
23
+ end
24
+
25
+ should "be created as needed and only go up to the maximum number allowed" do
26
+ # the minimum should be spawned and waiting
27
+ assert_equal 1, @work_pool.spawned
28
+ assert_equal 1, @work_pool.waiting
29
+
30
+ # the minimum should be spawned, but no longer waiting
31
+ @work_pool.add_work 5
32
+ assert_equal 1, @work_pool.spawned
33
+ assert_equal 0, @work_pool.waiting
34
+
35
+ # an additional worker should be spawned
36
+ @work_pool.add_work 5
37
+ assert_equal 2, @work_pool.spawned
38
+ assert_equal 0, @work_pool.waiting
39
+
40
+ # no additional workers are spawned, the work waits to be processed
41
+ @work_pool.add_work 5
42
+ assert_equal 2, @work_pool.spawned
43
+ assert_equal 0, @work_pool.waiting
44
+ end
45
+
46
+ should "go back to waiting when they finish working" do
47
+ assert_equal 1, @work_pool.spawned
48
+ assert_equal 1, @work_pool.waiting
49
+
50
+ @work_pool.add_work 1
51
+ assert_equal 1, @work_pool.spawned
52
+ assert_equal 0, @work_pool.waiting
53
+
54
+ sleep 1 # allow the worker to run
55
+
56
+ assert_equal 1, @work_pool.spawned
57
+ assert_equal 1, @work_pool.waiting
58
+ end
59
+
60
+ end
61
+
62
+ class AddWorkAndProcessItTests < BaseTests
63
+ desc "add_work and process"
64
+ setup do
65
+ @result = nil
66
+ @work_pool = DatWorkerPool.new(1){|work| @result = (2 / work) }
67
+ end
68
+
69
+ should "have added the work and processed it by calling the passed block" do
70
+ subject.add_work 2
71
+ sleep 0.1 # ensure worker thread get's a chance to run
72
+ assert_equal 1, @result
73
+ end
74
+
75
+ should "swallow exceptions, so workers don't end unexpectedly" do
76
+ subject.add_work 0
77
+ worker = subject.instance_variable_get("@workers").first
78
+ sleep 0.1
79
+
80
+ assert_equal 1, subject.spawned
81
+ assert_equal 1, subject.waiting
82
+ assert worker.instance_variable_get("@thread").alive?
83
+ end
84
+
85
+ end
86
+
87
+ class ShutdownTests < BaseTests
88
+ desc "shutdown"
89
+ setup do
90
+ @mutex = Mutex.new
91
+ @finished = []
92
+ @work_pool = DatWorkerPool.new(1, 2, true) do |work|
93
+ sleep 1
94
+ @mutex.synchronize{ @finished << work }
95
+ end
96
+ @work_pool.add_work 'a'
97
+ @work_pool.add_work 'b'
98
+ @work_pool.add_work 'c'
99
+ end
100
+
101
+ should "allow any work that has been picked up to be processed" do
102
+ # make sure the workers haven't processed any work
103
+ assert_equal [], @finished
104
+
105
+ subject.shutdown(5)
106
+
107
+ # NOTE, the last work shouldn't have been processed, as it wasn't
108
+ # picked up by a worker
109
+ assert_includes 'a', @finished
110
+ assert_includes 'b', @finished
111
+ assert_not_includes 'c', @finished
112
+
113
+ assert_equal 0, subject.spawned
114
+ assert_equal 0, subject.waiting
115
+ assert_includes 'c', subject.work_items
116
+ end
117
+
118
+ should "timeout if the workers take to long to finish" do
119
+ # make sure the workers haven't processed any work
120
+ assert_equal [], @finished
121
+ assert_raises(DatWorkerPool::TimeoutError) do
122
+ subject.shutdown(0.1)
123
+ end
124
+ end
125
+
126
+ end
127
+
128
+ end
@@ -0,0 +1,68 @@
1
+ require 'assert'
2
+ require 'dat-worker-pool/queue'
3
+
4
+ class DatWorkerPool::Queue
5
+
6
+ class BaseTests < Assert::Context
7
+ desc "DatWorkerPool::Queue"
8
+ setup do
9
+ @queue = DatWorkerPool::Queue.new
10
+ end
11
+ subject{ @queue }
12
+
13
+ should have_imeths :work_items, :push, :pop, :empty?
14
+ should have_imeths :wait_for_work_item, :shutdown
15
+
16
+ should "allow pushing work items onto the queue with #push" do
17
+ subject.push 'work'
18
+ assert_equal [ 'work' ], subject.work_items
19
+ end
20
+
21
+ should "raise an exception if trying to push work when shutdown" do
22
+ subject.shutdown
23
+ assert_raises(RuntimeError){ subject.push('work') }
24
+ end
25
+
26
+ should "pop work items off the queue with #pop" do
27
+ subject.push 'work1'
28
+ subject.push 'work2'
29
+
30
+ assert_equal 2, subject.work_items.size
31
+ assert_equal 'work1', subject.pop
32
+ assert_equal 1, subject.work_items.size
33
+ assert_equal 'work2', subject.pop
34
+ assert_equal 0, subject.work_items.size
35
+ end
36
+
37
+ should "return whether the queue is empty or not with #empty?" do
38
+ assert subject.empty?
39
+ subject.push 'work'
40
+ assert_not subject.empty?
41
+ subject.pop
42
+ assert subject.empty?
43
+ end
44
+
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)
52
+ end
53
+
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
60
+ 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)
64
+ end
65
+
66
+ end
67
+
68
+ end
@@ -0,0 +1,31 @@
1
+ require 'assert'
2
+ require 'dat-worker-pool/worker'
3
+
4
+ require 'dat-worker-pool'
5
+ require 'dat-worker-pool/queue'
6
+
7
+ class DatWorkerPool::Worker
8
+
9
+ class BaseTests < Assert::Context
10
+ desc "DatWorkerPool::Worker"
11
+ setup do
12
+ @pool = DatWorkerPool.new{ }
13
+ @queue = DatWorkerPool::Queue.new
14
+ @workers_waiting = DatWorkerPool::WorkersWaiting.new
15
+ @worker = DatWorkerPool::Worker.new(@pool, @queue, @workers_waiting){ }
16
+ end
17
+ subject{ @worker }
18
+
19
+ should have_imeths :shutdown, :join
20
+
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
25
+ subject.shutdown
26
+ assert_not_nil subject.join(1)
27
+ end
28
+
29
+ end
30
+
31
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dat-worker-pool
3
+ version: !ruby/object:Gem::Version
4
+ hash: 27
5
+ prerelease:
6
+ segments:
7
+ - 0
8
+ - 1
9
+ - 0
10
+ version: 0.1.0
11
+ platform: ruby
12
+ authors:
13
+ - Collin Redding
14
+ - Kelly Redding
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2013-07-15 00:00:00 Z
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ prerelease: false
23
+ version_requirements: &id001 !ruby/object:Gem::Requirement
24
+ none: false
25
+ requirements:
26
+ - - ~>
27
+ - !ruby/object:Gem::Version
28
+ hash: 11
29
+ segments:
30
+ - 1
31
+ - 2
32
+ version: "1.2"
33
+ requirement: *id001
34
+ name: SystemTimer
35
+ type: :runtime
36
+ - !ruby/object:Gem::Dependency
37
+ prerelease: false
38
+ version_requirements: &id002 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ hash: 3
44
+ segments:
45
+ - 0
46
+ version: "0"
47
+ requirement: *id002
48
+ name: assert
49
+ type: :development
50
+ description: A simple thread pool for processing generic 'work'
51
+ email:
52
+ - collin.redding@me.com
53
+ - kelly@kellyredding.com
54
+ executables: []
55
+
56
+ extensions: []
57
+
58
+ extra_rdoc_files: []
59
+
60
+ files:
61
+ - .gitignore
62
+ - Gemfile
63
+ - LICENSE.txt
64
+ - README.md
65
+ - Rakefile
66
+ - dat-worker-pool.gemspec
67
+ - lib/dat-worker-pool.rb
68
+ - lib/dat-worker-pool/queue.rb
69
+ - lib/dat-worker-pool/version.rb
70
+ - lib/dat-worker-pool/worker.rb
71
+ - log/.gitkeep
72
+ - test/helper.rb
73
+ - test/system/use_worker_pool_tests.rb
74
+ - test/unit/dat-worker-pool_tests.rb
75
+ - test/unit/queue_tests.rb
76
+ - test/unit/worker_tests.rb
77
+ - tmp/.gitkeep
78
+ homepage: http://github.com/redding/dat-worker-pool
79
+ licenses: []
80
+
81
+ post_install_message:
82
+ rdoc_options: []
83
+
84
+ require_paths:
85
+ - lib
86
+ required_ruby_version: !ruby/object:Gem::Requirement
87
+ none: false
88
+ requirements:
89
+ - - ">="
90
+ - !ruby/object:Gem::Version
91
+ hash: 3
92
+ segments:
93
+ - 0
94
+ version: "0"
95
+ required_rubygems_version: !ruby/object:Gem::Requirement
96
+ none: false
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ hash: 3
101
+ segments:
102
+ - 0
103
+ version: "0"
104
+ requirements: []
105
+
106
+ rubyforge_project:
107
+ rubygems_version: 1.8.15
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: A simple thread pool for processing generic 'work'
111
+ test_files:
112
+ - test/helper.rb
113
+ - test/system/use_worker_pool_tests.rb
114
+ - test/unit/dat-worker-pool_tests.rb
115
+ - test/unit/queue_tests.rb
116
+ - test/unit/worker_tests.rb