parenting 0.1.5
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +49 -0
- data/lib/parenting/boss.rb +93 -0
- data/lib/parenting/chore.rb +78 -0
- data/lib/parenting/version.rb +3 -0
- data/lib/parenting.rb +9 -0
- metadata +49 -0
data/README.md
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# Parenting
|
2
|
+
|
3
|
+
Sometimes you have a lot of stuff to do, and you want to get it done fast.
|
4
|
+
What's the obvious thing to do? Have a bunch of kids, and tell *them* to do it, of course!
|
5
|
+
|
6
|
+
## Intent
|
7
|
+
|
8
|
+
This gem allows a straightforward model of multiprocessing: spin off multiple external programs
|
9
|
+
to do work, and then wait for them to finish. That use-case is already possible in a basic
|
10
|
+
way using `spawn`, but that gives you no control or interactivity with the spawned process.
|
11
|
+
|
12
|
+
Because you may have more tasks than can reasonably run simultaneously (memory restrictions,
|
13
|
+
processor count, etc), you usually would like to specify an upper-limit on how many of those
|
14
|
+
tasks may be running simultaneously. If you have chores that are disproportionately long, you
|
15
|
+
usually want to minimize the net run-time - Parenting allows you to specify a 'cost' for each chore,
|
16
|
+
which it will use to sort them into longest-job first (provably minimzing the net run-time).
|
17
|
+
|
18
|
+
The most important detail is that your chores will probably want to do some kind of logging,
|
19
|
+
so that your main process can tell what is going on in each of them. The natural way to do this
|
20
|
+
is to allow the external processes to log via stderr, and to do something with each line of log
|
21
|
+
so produced - each chore takes a callable for how to thread-safely handle that output.
|
22
|
+
|
23
|
+
You can pass input to the child process via the `:stdin` option, but it is not intended for
|
24
|
+
bulk interaction - that string is fed to the process immediately, and the pipe is then closed.
|
25
|
+
|
26
|
+
## Thread-Safety
|
27
|
+
|
28
|
+
Parenting uses threads internally to allow jobs to finish in arbitrary order. None of the callbacks
|
29
|
+
you initialize chores with will be used outside of the main thread however, and all data structures
|
30
|
+
passed into the options hash will be dup'd, so you can safely reuse them for multiple chores.
|
31
|
+
|
32
|
+
## Usage
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
|
36
|
+
# build a coordinator that allows 4 children at a time
|
37
|
+
boss = Parenting::Boss.new(4)
|
38
|
+
|
39
|
+
['ls', 'ls -l', 'ls -a', 'echo hello'].each do |cmd|
|
40
|
+
boss.add_chore({
|
41
|
+
:command => cmd,
|
42
|
+
:on_success => lambda { STDERR.puts "#{cmd} succeeded" },
|
43
|
+
:on_failure => lambda { STDERR.puts "#{cmd} failed" },
|
44
|
+
:on_stderr => lambda { |ln| STDERR.puts "#{cmd} produced: #{ln}" }
|
45
|
+
})
|
46
|
+
end
|
47
|
+
|
48
|
+
boss.run!
|
49
|
+
```
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module Parenting
|
4
|
+
class Boss
|
5
|
+
attr_accessor :max_children, :chores, :in_progress, :completed
|
6
|
+
|
7
|
+
def initialize(number_of_children)
|
8
|
+
self.max_children = number_of_children
|
9
|
+
self.chores = []
|
10
|
+
self.in_progress = []
|
11
|
+
self.completed = Set.new
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_chore(opts)
|
15
|
+
self.chores << Parenting::Chore.new(opts)
|
16
|
+
end
|
17
|
+
|
18
|
+
def assign_next_chore
|
19
|
+
return unless self.assignable_chores.any?
|
20
|
+
return if self.in_progress.length >= self.max_children
|
21
|
+
|
22
|
+
next_location = self.chores.find_index{|c| c.satisfied? self.completed }
|
23
|
+
next_chore = self.chores.delete_at next_location
|
24
|
+
next_chore.run!
|
25
|
+
|
26
|
+
self.in_progress << next_chore
|
27
|
+
end
|
28
|
+
|
29
|
+
def free_children?
|
30
|
+
self.in_progress.length < self.max_children
|
31
|
+
end
|
32
|
+
|
33
|
+
def done?
|
34
|
+
self.assignable_chores.empty? && self.in_progress.empty?
|
35
|
+
end
|
36
|
+
|
37
|
+
def handle_complaints
|
38
|
+
self.in_progress.each do |chore|
|
39
|
+
until chore.stderr.empty?
|
40
|
+
chore.on_stderr.call(chore.stderr.shift)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def check_children
|
46
|
+
remaining = []
|
47
|
+
self.in_progress.each do |chore|
|
48
|
+
if chore.complete?
|
49
|
+
chore.handle_completion
|
50
|
+
self.completed << chore.name if chore.name and not chore.failed?
|
51
|
+
else
|
52
|
+
remaining << chore
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
self.in_progress = remaining
|
57
|
+
|
58
|
+
self.in_progress.each do |chore|
|
59
|
+
until chore.completed.empty?
|
60
|
+
self.completed << chore.shift
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def assignable_chores
|
66
|
+
self.chores.select do |c|
|
67
|
+
c.satisfied? self.completed
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def run!
|
72
|
+
# get the chores into longest job first - this is to minimize net runtime
|
73
|
+
self.chores = self.chores.sort_by{|c| - c.cost.to_f}
|
74
|
+
|
75
|
+
# queue up the first set of chores
|
76
|
+
while self.assignable_chores.any? and self.free_children?
|
77
|
+
self.assign_next_chore
|
78
|
+
end
|
79
|
+
|
80
|
+
# watch the children, and assign new chores if any get free
|
81
|
+
until self.done?
|
82
|
+
sleep 0.05
|
83
|
+
|
84
|
+
self.handle_complaints
|
85
|
+
self.check_children
|
86
|
+
|
87
|
+
while self.free_children? and self.assignable_chores.any?
|
88
|
+
self.assign_next_chore
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
module Parenting
|
2
|
+
class Chore
|
3
|
+
attr_accessor :on_success, :on_failure, :on_stderr, :exit_status
|
4
|
+
attr_accessor :command, :stdin, :stdout, :stderr
|
5
|
+
attr_accessor :cost, :thread, :result
|
6
|
+
attr_accessor :deps, :name, :completed
|
7
|
+
|
8
|
+
def initialize(opts)
|
9
|
+
[:on_success, :on_failure, :on_stderr].each do |cb|
|
10
|
+
self.send :"#{cb}=", opts.fetch(cb).dup
|
11
|
+
end
|
12
|
+
|
13
|
+
self.name = opts[:name] || nil
|
14
|
+
self.deps = opts[:deps] || []
|
15
|
+
self.completed = Queue.new
|
16
|
+
|
17
|
+
self.command = opts.fetch(:command).dup
|
18
|
+
self.command = [self.command] unless self.command.is_a? Array
|
19
|
+
self.cost = opts[:cost] || nil
|
20
|
+
self.stdin = opts[:stdin] || nil
|
21
|
+
self.stdout = nil
|
22
|
+
self.stderr = Queue.new
|
23
|
+
self.result = :working
|
24
|
+
end
|
25
|
+
|
26
|
+
def satisfied?(completed)
|
27
|
+
self.deps.empty? || self.deps.all?{|d| completed.include?(d)}
|
28
|
+
end
|
29
|
+
|
30
|
+
def run!
|
31
|
+
self.thread = Thread.new do
|
32
|
+
cmd = [self.command].flatten
|
33
|
+
Open3.popen3(* cmd) do |i, o, e, t|
|
34
|
+
i.write(self.stdin); i.close
|
35
|
+
|
36
|
+
e.each_line do |line|
|
37
|
+
self.stderr << line
|
38
|
+
end
|
39
|
+
e.close
|
40
|
+
|
41
|
+
self.stdout = o.read
|
42
|
+
o.close
|
43
|
+
|
44
|
+
result = t.value
|
45
|
+
self.exit_status = result.exitstatus
|
46
|
+
|
47
|
+
if result.success?
|
48
|
+
self.result = :success
|
49
|
+
else
|
50
|
+
self.result = :failure
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def complete?
|
57
|
+
self.result == :success || self.result == :failure
|
58
|
+
end
|
59
|
+
|
60
|
+
def done_with(name)
|
61
|
+
self.completed << name
|
62
|
+
end
|
63
|
+
|
64
|
+
def handle_completion
|
65
|
+
if self.result == :success
|
66
|
+
self.on_success.call(self)
|
67
|
+
elsif self.result == :failure
|
68
|
+
self.on_failure.call(self)
|
69
|
+
else
|
70
|
+
raise "This should not happen"
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def failed?
|
75
|
+
self.result == :failure
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
data/lib/parenting.rb
ADDED
metadata
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: parenting
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.5
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Eric Mueller
|
9
|
+
autorequire:
|
10
|
+
bindir: scripts
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-08-23 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Manage multiple child-processes via green threads
|
15
|
+
email: emueller@emcien.com
|
16
|
+
executables: []
|
17
|
+
extensions: []
|
18
|
+
extra_rdoc_files: []
|
19
|
+
files:
|
20
|
+
- README.md
|
21
|
+
- lib/parenting.rb
|
22
|
+
- lib/parenting/chore.rb
|
23
|
+
- lib/parenting/boss.rb
|
24
|
+
- lib/parenting/version.rb
|
25
|
+
homepage: http://github.com/emcien/parenting
|
26
|
+
licenses: []
|
27
|
+
post_install_message:
|
28
|
+
rdoc_options: []
|
29
|
+
require_paths:
|
30
|
+
- lib
|
31
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
32
|
+
none: false
|
33
|
+
requirements:
|
34
|
+
- - ! '>='
|
35
|
+
- !ruby/object:Gem::Version
|
36
|
+
version: '0'
|
37
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ! '>='
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: '1'
|
43
|
+
requirements: []
|
44
|
+
rubyforge_project:
|
45
|
+
rubygems_version: 1.8.24
|
46
|
+
signing_key:
|
47
|
+
specification_version: 3
|
48
|
+
summary: Put those child-processes to WORK
|
49
|
+
test_files: []
|