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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 9a9c845f04a84fb0ae22818bec9b471bac197308
4
- data.tar.gz: bdadd6aea6d4c068821a24d0afeb316d3f481f8b
3
+ metadata.gz: 715c70888317b1db49c5644d30e1197d6cd3dcb6
4
+ data.tar.gz: 15094b1d5774c55b9bb21d469405c65f7ea28942
5
5
  SHA512:
6
- metadata.gz: 9fbbb0d9a5ab5bc2c811154b2313a5ec834aef8efca4af9bb17b4cd8d75f17b6964a6170d34418e433efa3851f1aed12c4e5ada2404355c3aab3b3c0301f4a07
7
- data.tar.gz: 40f58a16103ca5f5722995ee640ce7b9740b7ceba2d22f027cc3f9fb5eb7832d39255b972196497558a460954794d3e48e300c46a0e81e538beb518181df9cd2
6
+ metadata.gz: 239d6296f2933a8e63a0747b99f1967720b1bb4137c2322b2bf3b90f0c3e07f521a4822e5ebeddd8e942a66d6bcc7f2e2bd00d62f62a68d3a4f9de588b426109
7
+ data.tar.gz: 04597306e45e9b2d83538bb1a1d4585ecb8a00f48229a6aeb5df2db7082c6de3645581441d2931941593d2f98633cf0d15b180120ee679aaae7b5d0511b745f9
data/.gitignore CHANGED
@@ -1 +1,3 @@
1
- Gemfile.lock
1
+ /Gemfile.lock
2
+ /doc
3
+ /.yardoc
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
- pool.launch 'gunzip --to-stdout', File.open('contents.txt.gz'), STDOUT
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 # start and finish times of this job
15
- attr_reader :inio, :outio, :errio # fds for child's stdin/stdout/stderr
16
-
17
- # runs cmd, passes instr on its stdin, and fills outio and
18
- # errio with the command's output.
19
- # TODO: should specify args using keywords rather than position.
20
- def initialize pool, cmd, inio=nil, outio=nil, errio=nil, timeout=nil
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 = pool
23
- @inio = inio || StringIO.new
24
- @inio = StringIO.new(@inio.to_s) unless @inio.respond_to?(:readpartial)
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
- @pool._add(self)
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
- @killed = false
33
- @timed_out = false
59
+ @pool._add(self)
34
60
 
35
- @thrin = Thread.new { drain(@inio, @chin) }
36
- @throut = Thread.new { drain(@chout, @outio) }
37
- @threrr = Thread.new { drain(@cherr, @errio) }
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
- # TODO: inline outatime
43
- outatime unless @child.join(timeout)
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
- @inio.write *args
85
+ @stdin.write *args
53
86
  end
54
87
 
55
88
  def read *args
56
- @outio.read *args
89
+ @stdout.read *args
57
90
  end
58
91
 
59
92
  def output
60
- @outio.string
93
+ @stdout.string
61
94
  end
62
95
 
63
96
  def error
64
- @errio.string
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
@@ -1,3 +1,3 @@
1
1
  class JobPool
2
- VERSION = "0.5"
2
+ VERSION = "0.6" # :nodoc:
3
3
  end
@@ -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(:chin) { StringIO.new('small instring') }
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, chout, cherr)
39
+ job = JobPool::Job.new(pool, 'cat', stdin: bigin)
42
40
  job.stop
43
- expect(chout.string).to eq bigin.string
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
- process = JobPool::Job.new(pool, '/bin/sh -c "sleep 0.1 && printf done."', chin, chout, cherr)
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
- process.stop
58
- expect(process.start_time).not_to eq nil
59
- expect(process.stop_time).not_to eq nil
60
- claimed = process.stop_time - process.start_time
61
- expect(chout.string).to eq 'done.'
62
- expect(process.finished?).to eq true
63
- expect(process.success?).to eq true
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(chout.closed_read?).to eq true
73
- expect(cherr.closed_read?).to eq true
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
- process = JobPool::Job.new(pool, ['sleep', '0.5'], chin, chout, cherr)
78
+ job = JobPool::Job.new(pool, ['sleep', '0.5'], stdin: small_input)
79
79
 
80
- expect(process.finished?).to eq false
81
- expect(process.killed?).to eq false
82
- expect(process.success?).to eq false
83
- expect(process.timed_out?).to eq false
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
- process.kill
85
+ job.kill
86
86
 
87
- expect(process.finished?).to eq true
88
- expect(process.killed?).to eq true
89
- expect(process.success?).to eq false
90
- expect(process.timed_out?).to eq false
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(chout.closed_read?).to eq true
95
- expect(cherr.closed_read?).to eq true
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
- process = JobPool::Job.new(pool, ['ThisCmdDoes.Not.Exist.'], chin, chout, cherr)
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
- process = JobPool::Job.new(pool, ['sleep', '10'], chin, chout, cherr, 0.1)
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
- process = JobPool::Job.new(pool, ['sleep', '10'], chin, chout, cherr, 0)
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
@@ -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'], StringIO.new, StringIO.new, StringIO.new)
33
- pool.launch(['sleep', '20'], StringIO.new, StringIO.new, StringIO.new)
34
- pool.launch(['sleep', '20'], StringIO.new, StringIO.new, StringIO.new)
35
- pool.launch(['sleep', '20'], StringIO.new, StringIO.new, StringIO.new)
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'], StringIO.new, StringIO.new, StringIO.new)
47
- process2 = pool.launch(['sleep', '.1'], StringIO.new, StringIO.new, StringIO.new)
48
- process3 = pool.launch(['sleep', '.2'], StringIO.new, StringIO.new, StringIO.new)
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
- }.to raise_exception(ThreadsWait::ErrNoWaitingThread)
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
- outstr = StringIO.new
20
- pool.launch 'gunzip --to-stdout', File.open('spec/contents.txt.gz'), outstr
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(outstr.string).to eq "contents\n"
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.5'
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-10 00:00:00.000000000 Z
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: