parenting 0.1.5

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.
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: []