parenting 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,3 @@
1
+ module Parenting
2
+ VERSION = "0.1.5"
3
+ end
data/lib/parenting.rb ADDED
@@ -0,0 +1,9 @@
1
+ require 'open3'
2
+ require 'thread'
3
+ require 'io/wait'
4
+ require_relative './parenting/boss'
5
+ require_relative './parenting/chore'
6
+ require_relative './parenting/version'
7
+
8
+ module Parenting
9
+ end
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: []