job_pool 0.5 → 0.6

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