job_pool 0.5 → 0.6
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.
- checksums.yaml +4 -4
- data/.gitignore +3 -1
- data/README.md +18 -7
- data/Rakefile +8 -0
- data/lib/job_pool.rb +3 -0
- data/lib/job_pool/job.rb +59 -31
- data/lib/job_pool/version.rb +1 -1
- data/spec/job_pool/job_spec.rb +32 -33
- data/spec/job_pool_spec.rb +9 -8
- data/spec/readme_spec.rb +6 -4
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA1:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 715c70888317b1db49c5644d30e1197d6cd3dcb6
|
|
4
|
+
data.tar.gz: 15094b1d5774c55b9bb21d469405c65f7ea28942
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 239d6296f2933a8e63a0747b99f1967720b1bb4137c2322b2bf3b90f0c3e07f521a4822e5ebeddd8e942a66d6bcc7f2e2bd00d62f62a68d3a4f9de588b426109
|
|
7
|
+
data.tar.gz: 04597306e45e9b2d83538bb1a1d4585ecb8a00f48229a6aeb5df2db7082c6de3645581441d2931941593d2f98633cf0d15b180120ee679aaae7b5d0511b745f9
|
data/.gitignore
CHANGED
data/README.md
CHANGED
|
@@ -35,7 +35,7 @@ pool = JobPool.new
|
|
|
35
35
|
Then fire off a job. This one waits a bit and then ROT-13s its input.
|
|
36
36
|
|
|
37
37
|
```ruby
|
|
38
|
-
job = pool.launch("sleep 5; tr A-Za-z N-ZA-Mn-za-m", "the secrets")
|
|
38
|
+
job = pool.launch("sleep 5; tr A-Za-z N-ZA-Mn-za-m", stdin: "the secrets")
|
|
39
39
|
pool.count => 1
|
|
40
40
|
job.output => ""
|
|
41
41
|
(after five seconds)
|
|
@@ -47,14 +47,16 @@ job.output => "gur frpergf"
|
|
|
47
47
|
|
|
48
48
|
You can specify IO objects to read from and write to:
|
|
49
49
|
|
|
50
|
-
TODO: this works, but it closes your stdout! That's problematic.
|
|
51
|
-
Maybe add a mode that doesn't close the output stream when done?
|
|
52
|
-
Or just use a different example?
|
|
53
|
-
|
|
54
50
|
```ruby
|
|
55
|
-
|
|
51
|
+
source = File.open('contents.txt.gz')
|
|
52
|
+
desdtination = File.open('/tmp/out', 'w')
|
|
53
|
+
pool.launch 'gunzip --to-stdout', stdin: source, stdout: destination
|
|
56
54
|
```
|
|
57
55
|
|
|
56
|
+
Note that if you specify STDIN or STDOUT, job_pool will close the stream
|
|
57
|
+
when the child terminates. This is almost certainly not what you want.
|
|
58
|
+
TODO: add an option... `keep_open: :stdout`, `keep_open: [:stdin, :stdout]`
|
|
59
|
+
|
|
58
60
|
#### Killing a Job
|
|
59
61
|
|
|
60
62
|
If you want to terminate a job, just kill it:
|
|
@@ -73,7 +75,7 @@ Pass the number of seconds to wait, default is 2 seconds.
|
|
|
73
75
|
|
|
74
76
|
#### Timeouts
|
|
75
77
|
|
|
76
|
-
TODO
|
|
78
|
+
TODO: add a timeout example
|
|
77
79
|
|
|
78
80
|
#### Limiting Running Processes
|
|
79
81
|
|
|
@@ -103,6 +105,15 @@ TODO: friggin documentation!
|
|
|
103
105
|
TODO: include an example of a job queue
|
|
104
106
|
|
|
105
107
|
|
|
108
|
+
## Documentation
|
|
109
|
+
|
|
110
|
+
I tried to use Ruby's automated documentation tools but it didn't stick.
|
|
111
|
+
RDoc didn't have reliable markdown support and lacked param name checking.
|
|
112
|
+
YARD produces uglier output (especially noframes) and its lack of :nodoc:
|
|
113
|
+
makes my docs too noisy. For now I'll just write documentation how I want
|
|
114
|
+
to see it and hope some tool catches up.
|
|
115
|
+
|
|
116
|
+
|
|
106
117
|
## License
|
|
107
118
|
|
|
108
119
|
MIT, enjoy!
|
data/Rakefile
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
require 'bundler/gem_tasks'
|
|
2
2
|
require 'rspec/core/rake_task'
|
|
3
|
+
require 'yard'
|
|
3
4
|
|
|
4
5
|
RSpec::Core::RakeTask.new(:spec)
|
|
5
6
|
task :default => ['spec']
|
|
6
7
|
task :test => ['spec']
|
|
8
|
+
|
|
9
|
+
YARD::Rake::YardocTask.new do |t|
|
|
10
|
+
t.files = ['lib/**/*.rb']
|
|
11
|
+
# t.options = ['--any', '--extra', '--opts']
|
|
12
|
+
t.stats_options = ['--list-undoc']
|
|
13
|
+
end
|
|
14
|
+
task :doc => ['yard']
|
data/lib/job_pool.rb
CHANGED
|
@@ -6,10 +6,13 @@ require 'job_pool/job'
|
|
|
6
6
|
# TODO: rewrite wait_next
|
|
7
7
|
|
|
8
8
|
class JobPool
|
|
9
|
+
# Indicates that the maximum allowed jobs are already running. Set in [initialize], thrown by [launch].
|
|
9
10
|
class TooManyJobsError < StandardError; end
|
|
10
11
|
|
|
11
12
|
attr_accessor :max_jobs
|
|
12
13
|
|
|
14
|
+
# ## Options
|
|
15
|
+
# * max_jobs: the maximum number of jobs that can be running at any one time, or nil if unlimited.
|
|
13
16
|
def initialize(options={})
|
|
14
17
|
@mutex ||= Mutex.new
|
|
15
18
|
|
data/lib/job_pool/job.rb
CHANGED
|
@@ -11,36 +11,64 @@ class JobPool; end
|
|
|
11
11
|
# A job keeps track of the child process that gets forked.
|
|
12
12
|
# job is the Ruby data structure, process is the Unix process.
|
|
13
13
|
class JobPool::Job
|
|
14
|
-
attr_reader :start_time, :stop_time
|
|
15
|
-
attr_reader :
|
|
16
|
-
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
20
|
-
|
|
14
|
+
attr_reader :start_time, :stop_time # start and finish times of this job
|
|
15
|
+
attr_reader :stdin, :stdout, :stderr # fds for child's stdin/stdout/stderr
|
|
16
|
+
|
|
17
|
+
# **internal**: Use [JobPool#launch], don't call this method directly.
|
|
18
|
+
#
|
|
19
|
+
# Starts a process.
|
|
20
|
+
#
|
|
21
|
+
# ## Parameters
|
|
22
|
+
#
|
|
23
|
+
# * pool [JobPool]: The pool that will contain this job.
|
|
24
|
+
# * command [String, Array]: The command to run. Can be specified either
|
|
25
|
+
# as a string or an array of arguments for Process.spawn.
|
|
26
|
+
#
|
|
27
|
+
# ## Options
|
|
28
|
+
#
|
|
29
|
+
# * stdin [IO, String]: The child's input. If an IO object isn't supplied,
|
|
30
|
+
# an IOString will be created by calling the parameter's to_s method.
|
|
31
|
+
# * stdout [IO]: the IO object to receive the child's output.
|
|
32
|
+
# * stderr [IO]: the IO object to receive the child's stderr.
|
|
33
|
+
# * timeout [seconds]: the number of seconds to wait before killing the job.
|
|
34
|
+
#
|
|
35
|
+
# If `stdin`, `stdout`, or `stderr` are omitted, an empty IOString will be created.
|
|
36
|
+
# If output and error are IOStrings, the [output] method will return the child's
|
|
37
|
+
# stdout, and [error] will return its stderr.
|
|
38
|
+
#
|
|
39
|
+
# ## Examples
|
|
40
|
+
#
|
|
41
|
+
# * Simple invocation: `job = Job.new pool, 'echo hi'`
|
|
42
|
+
# * Redirect outpout to a file: `Job.new pool, 'wkhtmltopdf', stdout: File.new('/tmp/out.pdf', 'w')`
|
|
43
|
+
# * Passing an array and options: `Job.new pool, ['cat', '/tmp/infile', {pgroup: true}]`
|
|
44
|
+
|
|
45
|
+
def initialize pool, command, options={}
|
|
21
46
|
@start_time = Time.now
|
|
22
|
-
@pool
|
|
23
|
-
@
|
|
24
|
-
@
|
|
25
|
-
@outio = outio || StringIO.new
|
|
26
|
-
@errio = errio || StringIO.new
|
|
27
|
-
@chin, @chout, @cherr, @child = Open3.popen3(*cmd)
|
|
47
|
+
@pool = pool
|
|
48
|
+
@killed = false
|
|
49
|
+
@timed_out = false
|
|
28
50
|
|
|
29
|
-
@
|
|
51
|
+
@stdin = options[:stdin] || StringIO.new
|
|
52
|
+
@stdin = StringIO.new(@stdin.to_s) unless @stdin.respond_to?(:readpartial)
|
|
53
|
+
@stdout = options[:stdout] || StringIO.new
|
|
54
|
+
@stderr = options[:stderr] || StringIO.new
|
|
55
|
+
|
|
56
|
+
@chin, @chout, @cherr, @child = Open3.popen3(*command)
|
|
30
57
|
@chout.binmode
|
|
31
58
|
|
|
32
|
-
@
|
|
33
|
-
@timed_out = false
|
|
59
|
+
@pool._add(self)
|
|
34
60
|
|
|
35
|
-
@thrin = Thread.new { drain(@
|
|
36
|
-
@throut = Thread.new { drain(@chout, @
|
|
37
|
-
@threrr = Thread.new { drain(@cherr, @
|
|
61
|
+
@thrin = Thread.new { drain(@stdin, @chin) }
|
|
62
|
+
@throut = Thread.new { drain(@chout, @stdout) }
|
|
63
|
+
@threrr = Thread.new { drain(@cherr, @stderr) }
|
|
38
64
|
|
|
39
65
|
# ensure cleanup is called when the child exits. (crazy that this requires a whole new thread!)
|
|
40
66
|
@cleanup_thread = Thread.new do
|
|
41
|
-
if timeout
|
|
42
|
-
|
|
43
|
-
|
|
67
|
+
if options[:timeout]
|
|
68
|
+
unless @child.join(timeout)
|
|
69
|
+
@timed_out = true
|
|
70
|
+
kill
|
|
71
|
+
end
|
|
44
72
|
else
|
|
45
73
|
@child.join
|
|
46
74
|
end
|
|
@@ -48,20 +76,25 @@ class JobPool::Job
|
|
|
48
76
|
end
|
|
49
77
|
end
|
|
50
78
|
|
|
79
|
+
# @param [Hash] opts the options to create a message with.
|
|
80
|
+
# @option opts [String] :subject The subject
|
|
81
|
+
# @option opts [String] :from ('nobody') From address
|
|
82
|
+
# @option opts [String] :to Recipient email
|
|
83
|
+
# @option opts [String] :body ('') The email's body
|
|
51
84
|
def write *args
|
|
52
|
-
@
|
|
85
|
+
@stdin.write *args
|
|
53
86
|
end
|
|
54
87
|
|
|
55
88
|
def read *args
|
|
56
|
-
@
|
|
89
|
+
@stdout.read *args
|
|
57
90
|
end
|
|
58
91
|
|
|
59
92
|
def output
|
|
60
|
-
@
|
|
93
|
+
@stdout.string
|
|
61
94
|
end
|
|
62
95
|
|
|
63
96
|
def error
|
|
64
|
-
@
|
|
97
|
+
@stderr.string
|
|
65
98
|
end
|
|
66
99
|
|
|
67
100
|
def finished?
|
|
@@ -131,11 +164,6 @@ private
|
|
|
131
164
|
@cleanup_thread.join unless Thread.current == @cleanup_thread
|
|
132
165
|
end
|
|
133
166
|
|
|
134
|
-
def outatime
|
|
135
|
-
@timed_out = true
|
|
136
|
-
kill
|
|
137
|
-
end
|
|
138
|
-
|
|
139
167
|
# reads every last drop, then closes both files. must be threadsafe.
|
|
140
168
|
def drain reader, writer
|
|
141
169
|
begin
|
data/lib/job_pool/version.rb
CHANGED
data/spec/job_pool/job_spec.rb
CHANGED
|
@@ -5,7 +5,7 @@ require 'job_pool/job'
|
|
|
5
5
|
# while rspec spec/job_pool/job_spec.rb ; do : ; done
|
|
6
6
|
|
|
7
7
|
describe JobPool::Job do
|
|
8
|
-
class FakeJobPool
|
|
8
|
+
class FakeJobPool # :nodoc: massively oversimplified job pool used in some testing
|
|
9
9
|
def initialize
|
|
10
10
|
@jobs = []
|
|
11
11
|
end
|
|
@@ -24,11 +24,9 @@ describe JobPool::Job do
|
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
let(:pool) { FakeJobPool.new }
|
|
27
|
-
let(:
|
|
28
|
-
let(:chout) { StringIO.new }
|
|
29
|
-
let(:cherr) { StringIO.new }
|
|
27
|
+
let(:small_input) { StringIO.new('small instring') }
|
|
30
28
|
|
|
31
|
-
def time_this_block &block
|
|
29
|
+
def time_this_block &block # :nodoc:
|
|
32
30
|
start = Time.now
|
|
33
31
|
block.call
|
|
34
32
|
finish = Time.now
|
|
@@ -38,9 +36,9 @@ describe JobPool::Job do
|
|
|
38
36
|
|
|
39
37
|
it "has a working drain method" do
|
|
40
38
|
bigin = StringIO.new('x' * 1024 * 1024) # at least 1 MB of data to test drain loop
|
|
41
|
-
job = JobPool::Job.new(pool, 'cat', bigin
|
|
39
|
+
job = JobPool::Job.new(pool, 'cat', stdin: bigin)
|
|
42
40
|
job.stop
|
|
43
|
-
expect(
|
|
41
|
+
expect(job.output).to eq bigin.string
|
|
44
42
|
expect(job.finished?).to eq true
|
|
45
43
|
end
|
|
46
44
|
|
|
@@ -48,19 +46,20 @@ describe JobPool::Job do
|
|
|
48
46
|
# pile a bunch of checks into this test so we only have to sleep once
|
|
49
47
|
expect(pool.count).to eq 0
|
|
50
48
|
claimed = nil
|
|
49
|
+
job = nil
|
|
51
50
|
|
|
52
51
|
elapsed = time_this_block do
|
|
53
52
|
# echo -n doesn't work here because of platform variations
|
|
54
53
|
# and for some reason jruby requires the explicit subshell; mri launches it automatically
|
|
55
|
-
|
|
54
|
+
job = JobPool::Job.new(pool, '/bin/sh -c "sleep 0.1 && printf done."', stdin: small_input)
|
|
56
55
|
expect(pool.count).to eq 1
|
|
57
|
-
|
|
58
|
-
expect(
|
|
59
|
-
expect(
|
|
60
|
-
claimed =
|
|
61
|
-
expect(
|
|
62
|
-
expect(
|
|
63
|
-
expect(
|
|
56
|
+
job.stop
|
|
57
|
+
expect(job.start_time).not_to eq nil
|
|
58
|
+
expect(job.stop_time).not_to eq nil
|
|
59
|
+
claimed = job.stop_time - job.start_time
|
|
60
|
+
expect(job.output).to eq 'done.'
|
|
61
|
+
expect(job.finished?).to eq true
|
|
62
|
+
expect(job.success?).to eq true
|
|
64
63
|
end
|
|
65
64
|
|
|
66
65
|
# ensure process elapsed time is in the ballpark
|
|
@@ -69,36 +68,37 @@ describe JobPool::Job do
|
|
|
69
68
|
expect(claimed).to be <= elapsed
|
|
70
69
|
|
|
71
70
|
expect(pool.count).to eq 0
|
|
72
|
-
expect(
|
|
73
|
-
expect(
|
|
71
|
+
expect(job.stdout.closed_read?).to eq true
|
|
72
|
+
expect(job.stderr.closed_read?).to eq true
|
|
74
73
|
end
|
|
75
74
|
|
|
76
75
|
it "has a working kill method" do
|
|
76
|
+
job = nil
|
|
77
77
|
elapsed = time_this_block do
|
|
78
|
-
|
|
78
|
+
job = JobPool::Job.new(pool, ['sleep', '0.5'], stdin: small_input)
|
|
79
79
|
|
|
80
|
-
expect(
|
|
81
|
-
expect(
|
|
82
|
-
expect(
|
|
83
|
-
expect(
|
|
80
|
+
expect(job.finished?).to eq false
|
|
81
|
+
expect(job.killed?).to eq false
|
|
82
|
+
expect(job.success?).to eq false
|
|
83
|
+
expect(job.timed_out?).to eq false
|
|
84
84
|
|
|
85
|
-
|
|
85
|
+
job.kill
|
|
86
86
|
|
|
87
|
-
expect(
|
|
88
|
-
expect(
|
|
89
|
-
expect(
|
|
90
|
-
expect(
|
|
87
|
+
expect(job.finished?).to eq true
|
|
88
|
+
expect(job.killed?).to eq true
|
|
89
|
+
expect(job.success?).to eq false
|
|
90
|
+
expect(job.timed_out?).to eq false
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
expect(elapsed).to be < 0.5
|
|
94
|
-
expect(
|
|
95
|
-
expect(
|
|
94
|
+
expect(job.stdout.closed_read?).to eq true
|
|
95
|
+
expect(job.stderr.closed_read?).to eq true
|
|
96
96
|
end
|
|
97
97
|
|
|
98
98
|
it "handles invalid commands" do
|
|
99
99
|
expect {
|
|
100
100
|
expect(pool.count).to eq 0
|
|
101
|
-
|
|
101
|
+
job = JobPool::Job.new(pool, ['ThisCmdDoes.Not.Exist.'], stdin: small_input)
|
|
102
102
|
raise "we shouldn't get here"
|
|
103
103
|
}.to raise_error(/[Nn]o such file/)
|
|
104
104
|
expect(pool.count).to eq 0
|
|
@@ -106,15 +106,14 @@ describe JobPool::Job do
|
|
|
106
106
|
|
|
107
107
|
it "has a working timeout" do
|
|
108
108
|
elapsed = time_this_block do
|
|
109
|
-
|
|
109
|
+
job = JobPool::Job.new(pool, ['sleep', '10'], stdin: small_input, timeout: 0.1)
|
|
110
110
|
end
|
|
111
111
|
expect(elapsed).to be < 0.2
|
|
112
112
|
end
|
|
113
113
|
|
|
114
|
-
# TODO: should probably define exactly what happens in this case
|
|
115
114
|
it "accepts a 0-length timeout" do
|
|
116
115
|
elapsed = time_this_block do
|
|
117
|
-
|
|
116
|
+
job = JobPool::Job.new(pool, ['sleep', '10'], stdin: small_input, timeout: 0)
|
|
118
117
|
end
|
|
119
118
|
expect(elapsed).to be < 0.2
|
|
120
119
|
end
|
data/spec/job_pool_spec.rb
CHANGED
|
@@ -29,10 +29,10 @@ describe JobPool do
|
|
|
29
29
|
after { expect(pool.count).to eq 0 }
|
|
30
30
|
|
|
31
31
|
it "counts and kills multiple processes" do
|
|
32
|
-
pool.launch(['sleep', '20']
|
|
33
|
-
pool.launch(['sleep', '20']
|
|
34
|
-
pool.launch(['sleep', '20']
|
|
35
|
-
pool.launch(['sleep', '20']
|
|
32
|
+
pool.launch(['sleep', '20'])
|
|
33
|
+
pool.launch(['sleep', '20'])
|
|
34
|
+
pool.launch(['sleep', '20'])
|
|
35
|
+
pool.launch(['sleep', '20'])
|
|
36
36
|
expect(pool.count).to eq 4
|
|
37
37
|
pool.first.kill
|
|
38
38
|
expect(pool.count).to eq 3
|
|
@@ -43,9 +43,9 @@ describe JobPool do
|
|
|
43
43
|
it "waits for multiple processes" do
|
|
44
44
|
# these sleep durations might be too small, depends on machine load and scheduling.
|
|
45
45
|
# if you're seeing threads finishing in the wrong order, try increasing them 10X.
|
|
46
|
-
process1 = pool.launch(['sleep', '.3']
|
|
47
|
-
process2 = pool.launch(['sleep', '.1']
|
|
48
|
-
process3 = pool.launch(['sleep', '.2']
|
|
46
|
+
process1 = pool.launch(['sleep', '.3'])
|
|
47
|
+
process2 = pool.launch(['sleep', '.1'])
|
|
48
|
+
process3 = pool.launch(['sleep', '.2'])
|
|
49
49
|
expect(pool.count).to eq 3
|
|
50
50
|
|
|
51
51
|
child = pool.wait_next
|
|
@@ -65,7 +65,8 @@ describe JobPool do
|
|
|
65
65
|
it "handles waiting for zero processes" do
|
|
66
66
|
expect {
|
|
67
67
|
child = pool.wait_next
|
|
68
|
-
|
|
68
|
+
# if I don't use a string, rdoc claims ThreadsWait is my class. Bug?
|
|
69
|
+
}.to raise_exception(Object.const_get 'ThreadsWait::ErrNoWaitingThread')
|
|
69
70
|
end
|
|
70
71
|
end
|
|
71
72
|
|
data/spec/readme_spec.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'job_pool'
|
|
|
4
4
|
describe 'README' do
|
|
5
5
|
it "can do the first example" do
|
|
6
6
|
pool = JobPool.new
|
|
7
|
-
job = pool.launch("sleep 0.1; tr A-Za-z N-ZA-Mn-za-m", "the secrets")
|
|
7
|
+
job = pool.launch("sleep 0.1; tr A-Za-z N-ZA-Mn-za-m", stdin: "the secrets")
|
|
8
8
|
expect(job.output).to eq ''
|
|
9
9
|
expect(pool.count).to eq 1
|
|
10
10
|
sleep(0.2)
|
|
@@ -16,10 +16,12 @@ describe 'README' do
|
|
|
16
16
|
pool = JobPool.new
|
|
17
17
|
# can't use `expect { ... }.to output('contents').to_stdout`
|
|
18
18
|
# because the test's stdout gets closed
|
|
19
|
-
|
|
20
|
-
|
|
19
|
+
source = File.open('spec/contents.txt.gz')
|
|
20
|
+
destination = File.open("/tmp/test-#{$$}-out.txt", 'w')
|
|
21
|
+
pool.launch 'gunzip --to-stdout', stdin: source, stdout: destination
|
|
21
22
|
pool.wait_next
|
|
22
|
-
expect(
|
|
23
|
+
expect(File.read "/tmp/test-#{$$}-out.txt").to eq "contents\n"
|
|
24
|
+
File.delete "/tmp/test-#{$$}-out.txt"
|
|
23
25
|
end
|
|
24
26
|
|
|
25
27
|
it "can do the killer example" do
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: job_pool
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: '0.
|
|
4
|
+
version: '0.6'
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Scott Bronson
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2015-10-
|
|
11
|
+
date: 2015-10-12 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: bundler
|
|
@@ -104,3 +104,4 @@ test_files:
|
|
|
104
104
|
- spec/job_pool_spec.rb
|
|
105
105
|
- spec/readme_spec.rb
|
|
106
106
|
- spec/spec_helper.rb
|
|
107
|
+
has_rdoc:
|