comp_tree 0.5.2 → 0.7.0

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.
Files changed (56) hide show
  1. data/CHANGES +24 -0
  2. data/README +19 -52
  3. data/Rakefile +1 -138
  4. data/comp_tree.gemspec +33 -30
  5. data/install.rb +3 -3
  6. data/lib/comp_tree/algorithm.rb +117 -156
  7. data/lib/comp_tree/driver.rb +39 -154
  8. data/lib/comp_tree/error.rb +18 -23
  9. data/lib/comp_tree/node.rb +46 -50
  10. data/lib/comp_tree.rb +56 -0
  11. data/rakelib/jumpstart/ruby.rb +51 -0
  12. data/{contrib/quix/lib/quix → rakelib/jumpstart}/simple_installer.rb +11 -13
  13. data/test/common.rb +29 -0
  14. data/test/test_basic.rb +189 -0
  15. data/test/test_circular.rb +34 -31
  16. data/test/test_drain.rb +38 -0
  17. data/test/test_exception.rb +37 -86
  18. data/test/test_flood.rb +14 -0
  19. data/test/test_grind.rb +77 -0
  20. data/test/test_sequential.rb +21 -0
  21. metadata +45 -58
  22. data/contrib/quix/Rakefile +0 -16
  23. data/contrib/quix/install.rb +0 -3
  24. data/contrib/quix/lib/quix/builtin/dir/casefold_brackets.rb +0 -7
  25. data/contrib/quix/lib/quix/builtin/kernel/tap.rb +0 -9
  26. data/contrib/quix/lib/quix/builtin/module/include.rb +0 -21
  27. data/contrib/quix/lib/quix/builtin/module/private.rb +0 -41
  28. data/contrib/quix/lib/quix/config.rb +0 -37
  29. data/contrib/quix/lib/quix/cygwin.rb +0 -60
  30. data/contrib/quix/lib/quix/diagnostic.rb +0 -44
  31. data/contrib/quix/lib/quix/enumerable.rb +0 -33
  32. data/contrib/quix/lib/quix/fileutils.rb +0 -37
  33. data/contrib/quix/lib/quix/hash_struct.rb +0 -27
  34. data/contrib/quix/lib/quix/kernel.rb +0 -61
  35. data/contrib/quix/lib/quix/lazy_struct.rb +0 -55
  36. data/contrib/quix/lib/quix/string.rb +0 -38
  37. data/contrib/quix/lib/quix/subpackager.rb +0 -52
  38. data/contrib/quix/lib/quix/thread_local.rb +0 -32
  39. data/contrib/quix/lib/quix/vars.rb +0 -138
  40. data/contrib/quix/lib/quix.rb +0 -32
  41. data/contrib/quix/test/all.rb +0 -12
  42. data/contrib/quix/test/test_deps.rb +0 -25
  43. data/contrib/quix/test/test_include.rb +0 -47
  44. data/contrib/quix/test/test_private.rb +0 -86
  45. data/contrib/quix/test/test_root.rb +0 -19
  46. data/contrib/quix/test/test_struct.rb +0 -48
  47. data/contrib/quix/test/test_vars.rb +0 -187
  48. data/lib/comp_tree/bucket_ipc.rb +0 -151
  49. data/lib/comp_tree/diagnostic.rb +0 -44
  50. data/lib/comp_tree/misc.rb +0 -61
  51. data/lib/comp_tree/retriable_fork.rb +0 -42
  52. data/lib/comp_tree/tap.rb +0 -9
  53. data/lib/comp_tree/task_node.rb +0 -22
  54. data/test/all.rb +0 -12
  55. data/test/test_bucketipc.rb +0 -72
  56. data/test/test_comp_tree.rb +0 -364
data/CHANGES ADDED
@@ -0,0 +1,24 @@
1
+
2
+ = CompTree ChangeLog
3
+
4
+ == Version 0.7.0
5
+
6
+ * remove fork and discard_result options
7
+ * remove contrib/
8
+ * remove block arg to CompTree.new; use CompTree.build
9
+ * remove CompTree::Error nesting
10
+ * Driver#compute accepts integer as second option
11
+ * remove method_missing and eval tricks for node definitions;
12
+ new project 'pure' makes this obsolete
13
+ * split up tests
14
+ * allow non-symbols for node IDs
15
+ * check_circular now returns the loop instead of raising
16
+ * allow computation result of nil
17
+
18
+ == Version 0.5.2
19
+
20
+ * internal project cleanup (no relevant code changes).
21
+
22
+ == Version 0.5.0
23
+
24
+ * Initial release.
data/README CHANGED
@@ -5,71 +5,38 @@
5
5
 
6
6
  require 'comp_tree'
7
7
 
8
- CompTree::Driver.new { |driver|
8
+ CompTree.build do |driver|
9
9
 
10
- # Define a function named 'area' taking these three arguments.
11
- driver.define_area(:width, :height, :offset) { |width, height, offset|
12
- width*height - offset
10
+ # Define a function named 'area' taking these two arguments.
11
+ driver.define(:area, :width, :height) { |width, height|
12
+ width*height
13
13
  }
14
14
 
15
15
  # Define a function 'width' which takes a 'border' argument.
16
- driver.define_width(:border) { |border|
17
- 2 + border
16
+ driver.define(:width, :border) { |border|
17
+ 7 + border
18
18
  }
19
19
 
20
20
  # Ditto for 'height'.
21
- driver.define_height(:border) { |border|
22
- 3 + border
21
+ driver.define(:height, :border) { |border|
22
+ 5 + border
23
23
  }
24
24
 
25
25
  # Define a constant function 'border'.
26
- driver.define_border {
27
- 5
28
- }
29
-
30
- # Ditto for 'offset'.
31
- driver.define_offset {
32
- 7
26
+ driver.define(:border) {
27
+ 2
33
28
  }
34
29
 
35
30
  # Compute the area using four parallel threads.
36
- area = driver.compute(:area, :threads => 4)
31
+ puts driver.compute(:area, :threads => 4)
32
+ # => 63
37
33
 
38
34
  # We've done this computation.
39
- if area == (2 + 5)*(3 + 5) - 7
40
- puts "It worked!"
41
- else
42
- puts "Send bug report to ..."
43
- end
44
- }
35
+ puts((7 + 2)*(5 + 2))
36
+ # => 63
37
+ end
45
38
 
46
- === Alternative Forms for Function Definitions
47
-
48
- This form evals a lambda, saving you the repeat parameter list:
49
-
50
- driver.define_area :width, :height, :offset, %{
51
- width*height - offset
52
- }
53
-
54
- driver.define_width :border, %{
55
- 2 + border
56
- }
57
-
58
- Notice the '<code>%</code>' before the brace. The lambda is created
59
- just once, during the time of definition.
60
-
61
- Finally there is the raw form which uses no +eval+ or
62
- +method_missing+ tricks:
63
-
64
- driver.define(:area, :width, :height, :offset) { |width, height, offset|
65
- width*height - offset
66
- }
67
-
68
- driver.define(:width, :border) { |border|
69
- 2 + border
70
- }
71
-
72
- == Important Notes
39
+ == Notes
73
40
 
74
41
  The user should have a basic understanding of <em>functional
75
42
  programming</em> (see for example
@@ -82,7 +49,7 @@ function you define must explicitly depend on the data it uses.
82
49
  #
83
50
  # BAD example: depending on state -- offset not listed as a parameter
84
51
  #
85
- driver.define_area(:width, :height) { |width, height|
52
+ driver.define(:area, :width, :height) { |width, height|
86
53
  width*height - offset
87
54
  }
88
55
 
@@ -96,9 +63,9 @@ to affect a state (to produce a <em>side effect</em>).
96
63
  #
97
64
  # BAD example: affecting state
98
65
  #
99
- driver.define_area(:width, :height, :offset) { |width, height, offset|
66
+ driver.define(:area, :width, :height) { |width, height|
100
67
  ACCUMULATOR.add "more data"
101
- width*height - offset
68
+ width*height
102
69
  }
103
70
 
104
71
  Given a tree where nodes are modifying _ACCUMULATOR_, the end state of
data/Rakefile CHANGED
@@ -1,140 +1,3 @@
1
- $LOAD_PATH.unshift "contrib/quix/lib"
2
1
 
3
- require 'rake/gempackagetask'
4
- require 'rake/contrib/rubyforgepublisher'
2
+ # rakelib generates tasks
5
3
 
6
- $VERBOSE = nil
7
- require 'rdoc/rdoc'
8
- $VERBOSE = true
9
-
10
- require 'fileutils'
11
- include FileUtils
12
-
13
- GEMSPEC = eval(File.read("comp_tree.gemspec"))
14
- DOC_DIR = "html"
15
-
16
- ######################################################################
17
- # clean
18
-
19
- task :clean => [:clobber, :clean_doc] do
20
- end
21
-
22
- task :clean_doc do
23
- rm_rf(DOC_DIR)
24
- end
25
-
26
- ######################################################################
27
- # test
28
-
29
- task :test do
30
- require 'test/all'
31
- end
32
-
33
- ######################################################################
34
- # package
35
-
36
- task :package => :clean
37
-
38
- Rake::GemPackageTask.new(GEMSPEC) { |t|
39
- t.need_tar = true
40
- }
41
-
42
- ######################################################################
43
- # doc
44
-
45
- task :doc => :clean_doc do
46
- files = %w(README) + %w(driver error node task_node).map { |name|
47
- "lib/comp_tree/#{name}.rb"
48
- }
49
-
50
- options = [
51
- "-o", DOC_DIR,
52
- "--title", "comp_tree: #{GEMSPEC.summary}",
53
- "--main", "README"
54
- ]
55
-
56
- RDoc::RDoc.new.document(files + options)
57
- end
58
-
59
- ######################################################################
60
- # git
61
-
62
- def git(*args)
63
- cmd = ["git"] + args
64
- sh(*cmd)
65
- end
66
-
67
- task :init_contrib do
68
- unless `git remote`.split.include? "quix"
69
- git(*%w!remote add -f quix git@github.com:quix/quix.git!)
70
- end
71
- end
72
-
73
- task :add_contrib_first_time => :init_contrib do
74
- git(*%w!merge --squash -s ours --no-commit quix/master!)
75
- git(*%w!read-tree --prefix=contrib/quix -u quix/master!)
76
- git("commit", "-m", "add quix utils")
77
- end
78
-
79
- task :run_pull_contrib do
80
- git(*%w!pull --no-commit -s subtree quix master!)
81
- end
82
-
83
- task :pull_contrib => [ :init_contrib, :run_pull_contrib ]
84
-
85
- ######################################################################
86
- # publisher
87
-
88
- task :publish => :doc do
89
- Rake::RubyForgePublisher.new('comptree', 'quix').upload
90
- end
91
-
92
- ######################################################################
93
- # release
94
-
95
- task :prerelease => :clean do
96
- rm_rf(DOC_DIR)
97
- rm_rf("pkg")
98
- unless `git status` =~ %r!nothing to commit \(working directory clean\)!
99
- raise "Directory not clean"
100
- end
101
- unless `ping -c2 github.com` =~ %r!0% packet loss!i
102
- raise "No ping for github.com"
103
- end
104
- end
105
-
106
- def rubyforge(command, file)
107
- sh("rubyforge",
108
- command,
109
- GEMSPEC.rubyforge_project,
110
- GEMSPEC.rubyforge_project,
111
- GEMSPEC.version.to_s,
112
- file)
113
- end
114
-
115
- task :finish_release do
116
- gem, tgz = %w(gem tgz).map { |ext|
117
- "pkg/#{GEMSPEC.name}-#{GEMSPEC.version}.#{ext}"
118
- }
119
- gem_md5, tgz_md5 = [gem, tgz].map { |file|
120
- "#{file}.md5".tap { |md5|
121
- sh("md5sum #{file} > #{md5}")
122
- }
123
- }
124
-
125
- rubyforge("add_release", gem)
126
- rubyforge("add_file", gem_md5)
127
- rubyforge("add_file", tgz)
128
- rubyforge("add_file", tgz_md5)
129
-
130
- git("tag", "comp_tree-" + GEMSPEC.version.to_s)
131
- git(*%w(push --tags origin master))
132
- end
133
-
134
- task :release =>
135
- [
136
- :prerelease,
137
- :package,
138
- :publish,
139
- :finish_release,
140
- ]
data/comp_tree.gemspec CHANGED
@@ -1,38 +1,41 @@
1
1
 
2
- Gem::Specification.new { |t|
3
- t.author = "James M. Lawrence"
4
- t.email = "quixoticsycophant@gmail.com"
5
- t.summary = "Parallel Computation Tree"
6
- t.name = "comp_tree"
7
- t.rubyforge_project = "comptree"
8
- t.homepage = "comptree.rubyforge.org"
9
- t.version = "0.5.2"
10
- t.description = "Build a computation tree and execute it with N " +
11
- "parallel threads. Optionally fork computation nodes into new processes."
2
+ Gem::Specification.new { |g|
3
+ g.author = "James M. Lawrence"
4
+ g.email = "quixoticsycophant@gmail.com"
5
+ g.summary = "Parallel Computation Tree"
6
+ g.name = "comp_tree"
7
+ g.rubyforge_project = "comptree"
8
+ g.homepage = "comptree.rubyforge.org"
9
+ g.version = "0.7.0"
10
+ g.description =
11
+ "Build a computation tree and execute it with N parallel threads."
12
12
 
13
- t.files = %w{README comp_tree.gemspec} +
14
- Dir["./**/*.rb"] +
15
- Dir["./**/Rakefile"]
13
+ readme = "README"
16
14
 
17
- rdoc_exclude = %w{
18
- test
19
- contrib
20
- install
21
- quix
22
- fork
23
- diagnostic
24
- algorithm
25
- bucket
26
- comp_tree\.rb
15
+ g.files = %W[
16
+ CHANGES
17
+ #{readme}
18
+ Rakefile
19
+ #{g.name}.gemspec
20
+ install.rb
21
+ ] + %w[lib rakelib test].inject(Array.new) { |acc, dir|
22
+ acc + Dir[dir + "/**/*.rb"]
27
23
  }
28
- t.has_rdoc = true
29
- t.extra_rdoc_files = %w{README}
30
- t.rdoc_options += [
24
+ g.has_rdoc = true
25
+ rdoc_files = [
26
+ readme,
27
+ "lib/comp_tree.rb",
28
+ "lib/comp_tree/driver.rb",
29
+ "lib/comp_tree/error.rb"
30
+ ]
31
+ g.extra_rdoc_files += [readme]
32
+
33
+ g.rdoc_options += [
31
34
  "--main",
32
- "README",
35
+ readme,
33
36
  "--title",
34
- "comp_tree: #{t.summary}",
35
- ] + rdoc_exclude.inject(Array.new) { |acc, pattern|
36
- acc + ["--exclude", pattern]
37
+ "#{g.name}: #{g.summary}"
38
+ ] + (g.files - rdoc_files).inject(Array.new) { |acc, file|
39
+ acc + ["--exclude", file]
37
40
  }
38
41
  }
data/install.rb CHANGED
@@ -1,3 +1,3 @@
1
- $LOAD_PATH.unshift "./contrib/quix/lib"
2
- require 'quix/simple_installer'
3
- CompTree::SimpleInstaller.new.run
1
+ $LOAD_PATH.unshift "rakelib"
2
+ require 'jumpstart/simple_installer'
3
+ Jumpstart::SimpleInstaller.new.run
@@ -1,210 +1,171 @@
1
1
 
2
- require 'comp_tree/diagnostic'
3
- require 'comp_tree/retriable_fork'
4
-
5
2
  module CompTree
6
3
  module Algorithm
7
- include Diagnostic
4
+ module_function
5
+
6
+ def loop_with(leave, again)
7
+ catch(leave) {
8
+ while true
9
+ catch(again) {
10
+ yield
11
+ }
12
+ end
13
+ }
14
+ end
8
15
 
9
- def compute_multithreaded(root, num_threads, use_fork, buckets)
10
- trace "Computing #{root.name} with #{num_threads} threads"
11
- result = nil
12
- mutex = Mutex.new
16
+ def compute_multithreaded(root, num_threads)
17
+ #trace "Computing #{root.name} with #{num_threads} threads"
18
+ finished = nil
19
+ tree_mutex = Mutex.new
13
20
  node_finished_condition = ConditionVariable.new
14
21
  thread_wake_condition = ConditionVariable.new
15
- threads = []
16
-
17
- # workaround: jruby gives "run" status for waiting on
18
- # condition variable
19
- num_threads_ready = 0
20
-
21
- num_threads.times { |thread_index|
22
- threads << Thread.new {
22
+ num_threads_in_use = 0
23
+
24
+ threads = (0...num_threads).map { |thread_index|
25
+ Thread.new {
23
26
  #
24
27
  # wait for main thread
25
28
  #
26
- mutex.synchronize {
27
- trace "Thread #{thread_index} waiting to start"
28
- num_threads_ready += 1
29
- thread_wake_condition.wait(mutex)
29
+ tree_mutex.synchronize {
30
+ #trace "Thread #{thread_index} waiting to start"
31
+ num_threads_in_use += 1
32
+ thread_wake_condition.wait(tree_mutex)
30
33
  }
31
34
 
32
- while true
33
- trace "Thread #{thread_index} node search"
34
-
35
- #
36
- # Done! Thread will exit.
37
- #
38
- break if mutex.synchronize {
39
- result
40
- }
41
-
42
- #
43
- # Lock the tree and find a node. The node we
44
- # obtain, if any, is already locked.
45
- #
46
- node = mutex.synchronize {
47
- find_node(root)
35
+ loop_with(:leave, :again) {
36
+ node = tree_mutex.synchronize {
37
+ #trace "Thread #{thread_index} acquired tree lock; begin search"
38
+ if finished
39
+ #trace "Thread #{thread_index} detected finish"
40
+ num_threads_in_use -= 1
41
+ throw :leave
42
+ else
43
+ #
44
+ # Find a node. The node we obtain, if any, will be locked.
45
+ #
46
+ node = find_node(root)
47
+ if node
48
+ #trace "Thread #{thread_index} found node #{node.name}"
49
+ node
50
+ else
51
+ #trace "Thread #{thread_index}: no node found; sleeping."
52
+ thread_wake_condition.wait(tree_mutex)
53
+ throw :again
54
+ end
55
+ end
48
56
  }
49
57
 
50
- if node
51
- trace "Thread #{thread_index} found node #{node.name}"
52
-
53
- node_result =
54
- compute_node(
55
- node,
56
- use_fork,
57
- buckets ? buckets[thread_index] : nil)
58
-
59
- mutex.synchronize {
60
- node.result = node_result
61
- }
62
-
58
+ #trace "Thread #{thread_index} computing node"
59
+ #debug {
60
+ # node.trace_compute
61
+ #}
62
+ node.compute
63
+ #trace "Thread #{thread_index} node computed; waiting for tree lock"
64
+
65
+ tree_mutex.synchronize {
66
+ #trace "Thread #{thread_index} acquired tree lock"
67
+ #debug {
68
+ # name = "#{node.name}" + ((node == root) ? " (ROOT NODE)" : "")
69
+ # initial = "Thread #{thread_index} compute result for #{name}: "
70
+ # status = node.computed.is_a?(Exception) ? "error" : "success"
71
+ # trace initial + status
72
+ # trace "Thread #{thread_index} node result: #{node.result}"
73
+ #}
74
+
75
+ if node.computed.is_a? Exception
76
+ #
77
+ # An error occurred; we are done.
78
+ #
79
+ finished = node.computed
80
+ elsif node == root
81
+ #
82
+ # Root node was computed; we are done.
83
+ #
84
+ finished = true
85
+ end
86
+
63
87
  #
64
88
  # remove locks for this node (shared lock and own lock)
65
89
  #
66
- mutex.synchronize {
67
- node.unlock
68
- if node == root
69
- #
70
- # Root node was computed; we are done.
71
- #
72
- trace "Thread #{thread_index} got final answer"
73
- result = root.result
74
- end
75
- node_finished_condition.signal
76
- }
77
- else
78
- trace "Thread #{thread_index}: no node found; sleeping."
79
- mutex.synchronize {
80
- thread_wake_condition.wait(mutex)
81
- }
82
- end
83
- end
84
- trace "Thread #{thread_index} exiting"
90
+ node.unlock
91
+
92
+ #
93
+ # Tell the main thread that another node was computed.
94
+ #
95
+ node_finished_condition.signal
96
+ }
97
+ }
98
+ #trace "Thread #{thread_index} exiting"
85
99
  }
86
100
  }
87
101
 
88
- trace "Main: waiting for threads to launch and block."
89
- while true
90
- break if mutex.synchronize {
91
- num_threads_ready == num_threads
92
- }
102
+ #trace "Main: waiting for threads to launch and block."
103
+ until tree_mutex.synchronize { num_threads_in_use == num_threads }
93
104
  Thread.pass
94
105
  end
95
-
96
- trace "Main: entering main loop"
97
- mutex.synchronize {
98
- while true
99
- trace "Main: waking threads"
106
+
107
+ tree_mutex.synchronize {
108
+ #trace "Main: entering main loop"
109
+ until num_threads_in_use == 0
110
+ #trace "Main: waking threads"
100
111
  thread_wake_condition.broadcast
101
112
 
102
- if result
103
- trace "Main: detected finish."
113
+ if finished
114
+ #trace "Main: detected finish."
104
115
  break
105
116
  end
106
117
 
107
- trace "Main: waiting for a node"
108
- node_finished_condition.wait(mutex)
109
- trace "Main: got a node"
118
+ #trace "Main: waiting for a node"
119
+ node_finished_condition.wait(tree_mutex)
120
+ #trace "Main: got a node"
110
121
  end
111
122
  }
112
123
 
113
- trace "Main: waiting for threads to finish."
114
- catch(:done) {
115
- while true
116
- mutex.synchronize {
117
- throw :done if threads.all? { |thread|
118
- thread.status == false
119
- }
120
- thread_wake_condition.broadcast
121
- }
122
- Thread.pass
123
- end
124
+ #trace "Main: waiting for threads to finish."
125
+ loop_with(:leave, :again) {
126
+ tree_mutex.synchronize {
127
+ if threads.all? { |thread| thread.status == false }
128
+ throw :leave
129
+ end
130
+ thread_wake_condition.broadcast
131
+ }
132
+ Thread.pass
124
133
  }
125
134
 
126
- trace "Main: computation done."
127
- result
135
+ #trace "Main: computation done."
136
+ if finished.is_a? Exception
137
+ raise finished
138
+ else
139
+ root.result
140
+ end
128
141
  end
129
142
 
130
143
  def find_node(node)
131
- # --- only called inside mutex
132
- trace "Looking for a node, starting with #{node.name}"
133
- if node.result
144
+ # --- only called inside shared tree mutex
145
+ #trace "Looking for a node, starting with #{node.name}"
146
+ if node.computed
134
147
  #
135
148
  # already computed
136
149
  #
137
- trace "#{node.name} has been computed"
150
+ #trace "#{node.name} has been computed"
138
151
  nil
139
- elsif node.children_results and node.try_lock
152
+ elsif (children_results = node.find_children_results) and node.try_lock
140
153
  #
141
154
  # Node is not computed and its children are computed;
142
155
  # and we have the lock. Ready to compute.
143
156
  #
157
+ node.children_results = children_results
144
158
  node
145
159
  else
146
160
  #
147
161
  # locked or children not computed; recurse to children
148
162
  #
149
- trace "Checking #{node.name}'s children"
163
+ #trace "Checking #{node.name}'s children"
150
164
  node.each_child { |child|
151
- if next_node = find_node(child)
152
- return next_node
153
- end
165
+ next_node = find_node(child) and return next_node
154
166
  }
155
167
  nil
156
168
  end
157
169
  end
158
-
159
- def compute_node(node, use_fork, bucket)
160
- if use_fork
161
- trace "About to fork for node #{node.name}"
162
- if bucket
163
- #
164
- # Use our assigned bucket to transfer the result.
165
- #
166
- fork_node(node) {
167
- node.trace_compute
168
- bucket.contents = node.compute
169
- }
170
- bucket.contents
171
- else
172
- #
173
- # No bucket -- discarding result
174
- #
175
- fork_node(node) {
176
- node.trace_compute
177
- node.compute
178
- }
179
- true
180
- end
181
- else
182
- #
183
- # No fork
184
- #
185
- node.trace_compute
186
- node.compute
187
- end
188
- end
189
-
190
- def fork_node(node)
191
- trace "About to fork for node #{node.name}"
192
- process_id = RetriableFork.fork {
193
- trace "Fork: process #{Process.pid}"
194
- node.trace_compute
195
- yield
196
- trace "Fork: computation done"
197
- }
198
- trace "Waiting for process #{process_id}"
199
- Process.wait(process_id)
200
- trace "Process #{process_id} finished"
201
- exitstatus = $?.exitstatus
202
- if exitstatus != 0
203
- trace "Process #{process_id} returned #{exitstatus}; exiting."
204
- exit(1)
205
- end
206
- end
207
-
208
- extend self
209
170
  end
210
171
  end