in-parallel 0.1.4 → 0.1.5

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: 954d90d6730f964c9b6af33e32f377681d762124
4
- data.tar.gz: 5714427d316d51853feb018ee79f6dc9fa1fa687
3
+ metadata.gz: a79b9831824dee514b121f2911693c825c8bda13
4
+ data.tar.gz: 630845c4f1c5d2f22b83cb6e2a7467118371bf91
5
5
  SHA512:
6
- metadata.gz: f8ff13104c07ec1b88d9b08b14f28bc939f61a1d5e26b47d927aab6aadb17ee7dedcf12794cbe62f87db2672bae85a8dc7fbf54ec08ba7a5e36b3374e5498908
7
- data.tar.gz: 6c371de92cac2770beca520bc4cb261f85d8d51e0b06c59f0a755cc18c1b2908af1fa59081296ac4b5f14d4108d004cf32e539416cec8cf242c14f4490bea573
6
+ metadata.gz: 97a010bf1fdd61118c873ef60f807b65da112edb9867aa5c68823713cdd0d7e3bf1dae8cf8dc4fc3cc36d47b2f9f1722048106ed21f3cce486ebc1fdf2274ce8
7
+ data.tar.gz: 14c0e06a6e09f135e56656cc5496cbd6bb15a7613b3964acd306c33ab8259de8062f125675607b851a1ea4a8323c64fc32c46e940f4405212adca2b6275d1afb
data/MAINTAINERS.md ADDED
@@ -0,0 +1,8 @@
1
+ # Reviewers/Maintainers For in-parallel
2
+
3
+ in-parallel is maintained by Puppet's Quality Assurance (QA) Team. These people
4
+ will be reviewing & merging PRs to the project.
5
+
6
+ | Name | Github | Email |
7
+ |:--------------:|:---------------------------------------------------:|:---------------------------:|
8
+ | Sam Woods | [samwoods1](https://github.com/samwoods1) | <sam.woods@puppet.com> |
data/README.md CHANGED
@@ -1,17 +1,19 @@
1
1
  # in-parallel
2
2
  A lightweight Ruby library with very simple syntax, making use of process.fork for parallelization
3
3
 
4
- The other Ruby librarys that do parallel execution all support one primary use case - crunching through a large queue of small tasks as quickly and efficiently as possible. This library primarily supports the use case of needing to run a few larger tasks in parallel and managing the stdout to make it easy to understand which processes are logging what. This library was created to be used by the Beaker test framework to enable parallel execution of some of the framework's tasks, and allow people within thier tests to execute code in parallel when wanted. This solution does not check to see how many processors you have, it just forks as many processes as you ask for. That means that it will handle a handful of parallel processes well, but could definitely overload your system with ruby processes if you try to spin up a LOT of processes. If you're looking for something simple and light-weight and on either linux or mac (forking processes is not supported on Windows), then this solution could be what you want.
4
+ Other popular Ruby librarys that do parallel execution support one primary use case - crunching through a large queue of small tasks as quickly and efficiently as possible. This library primarily supports the use case of executing a few larger tasks in parallel and managing the stdout and return values to make it easy to understand which processes are logging what, and what the outcome of the execution was. This library was created to be used by Puppet's Beaker test framework to enable parallel execution of some of the framework's tasks, and allow people within thier tests to execute code in parallel when wanted. This solution does not check to see how many processors you have, it just forks as many processes as you ask for. That means that it will handle a handful of parallel processes well, but could definitely overload your system with ruby processes if you try to spin up a LOT of processes. If you're looking for something simple and light-weight and on either linux or mac (forking processes is not supported on Windows), then this solution could be what you want.
5
5
 
6
- If you are looking for something a little more production ready, you should take a look at the [parallel](https://github.com/grosser/parallel) project. In the future this library will extend the in the parallel gem to take advantage of all of it's useful features as well.
6
+ If you are looking for something to support executing a lot of tasks in parallel as efficiently as possible, you should take a look at the [parallel](https://github.com/grosser/parallel) project.
7
7
 
8
8
  ## Methods:
9
9
 
10
- ### InParallel.run_in_parallel(&block)
10
+ ### run_in_parallel(&block)
11
11
  1. You can put whatever methods you want to execute in parallel into a block, and each method will be executed in parallel (unless the method is defined in kernel).
12
12
  1. Any methods further down the stack won't be affected, only the ones directly within the block.
13
13
  2. You can assign the results to instance variables and it just works, no dealing with an array or map of results.
14
14
  3. Log STDOUT and STDERR chunked per process to the console so that it is easy to see what happened in which process.
15
+ 4. Waits for each process in realtime and logs immediately upon completion of each process
16
+ 5. If an exception is raised by a child process, it will immediately be re-raised in the primary process and kill all other still running child processes
15
17
 
16
18
  ```ruby
17
19
  def method_with_param(name)
@@ -28,8 +30,8 @@ If you are looking for something a little more production ready, you should take
28
30
  end
29
31
 
30
32
  # Example:
31
- # will spawn 2 processes, (1 for each method) wait until they both complete,
32
- # and log chunked STDOUT/STDERR for each process:
33
+ # will spawn 2 processes, (1 for each method) wait until they both complete, log chunked STDOUT/STDERR for
34
+ # each process and assign the method return values to instance variables:
33
35
  InParallel.run_in_parallel {
34
36
  @result_1 = method_with_param('world')
35
37
  @result_2 = method_without_param
@@ -53,7 +55,7 @@ hello world
53
55
  hello world, bar
54
56
  ```
55
57
 
56
- ### InParallel.run_in_background(ignore_results = true, &block)
58
+ ### run_in_background(ignore_results = true, &block)
57
59
  1. This does basically the same thing as run_in_parallel, except it does not wait for execution of all processes to complete, it returns immediately.
58
60
  2. You can optionally ignore results completely (default) or delay evaluating the results until later
59
61
  3. You can run multiple blocks in the background and then at some later point evaluate all of the results
@@ -68,7 +70,7 @@ hello world, bar
68
70
  end
69
71
 
70
72
  # Example 1 - ignore results
71
- InParallel.run_in_background{
73
+ run_in_background{
72
74
  create_file_with_delay(TMP_FILE)
73
75
  }
74
76
 
@@ -79,13 +81,13 @@ hello world, bar
79
81
  puts(File.exists?(TMP_FILE)) # true
80
82
 
81
83
  # Example 2 - delay results
82
- InParallel.run_in_background(false){
84
+ run_in_background(false){
83
85
  @result = create_file_with_delay(TMP_FILE)
84
86
  }
85
87
 
86
88
  # Do something else
87
89
 
88
- InParallel.run_in_background(false){
90
+ run_in_background(false){
89
91
  @result2 = create_file_with_delay('/tmp/someotherfile.txt')
90
92
  }
91
93
 
@@ -93,7 +95,7 @@ hello world, bar
93
95
  puts @result >> "unresolved_parallel_result_0"
94
96
 
95
97
  # This assigns all instance variables within the block and writes STDOUT and STDERR from the process to console.
96
- InParallel.get_background_results
98
+ wait_for_processes
97
99
  puts @result # true
98
100
  puts @result2 # true
99
101
 
@@ -111,19 +113,19 @@ hello world, bar
111
113
  ```
112
114
  STDOUT:
113
115
  ```
114
- 'each_in_parallel' spawned process for '/Users/samwoods/parallel_test/test/paralell_spec.rb:77:in `block (2 levels) in <top (required)>'' - PID = '51600'
115
- 'each_in_parallel' spawned process for '/Users/samwoods/parallel_test/test/paralell_spec.rb:77:in `block (2 levels) in <top (required)>'' - PID = '51601'
116
- 'each_in_parallel' spawned process for '/Users/samwoods/parallel_test/test/paralell_spec.rb:77:in `block (2 levels) in <top (required)>'' - PID = '51602'
116
+ 'each_in_parallel' spawned process for '/Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>'' - PID = '51600'
117
+ 'each_in_parallel' spawned process for '/Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>'' - PID = '51601'
118
+ 'each_in_parallel' spawned process for '/Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>'' - PID = '51602'
117
119
 
118
- ------ Begin output for /Users/samwoods/parallel_test/test/paralell_spec.rb:77:in `block (2 levels) in <top (required)>' - 51600
120
+ ------ Begin output for /Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>' - 51600
119
121
  foo
120
- ------ Completed output for /Users/samwoods/parallel_test/test/paralell_spec.rb:77:in `block (2 levels) in <top (required)>' - 51600
122
+ ------ Completed output for /Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>' - 51600
121
123
 
122
- ------ Begin output for /Users/samwoods/parallel_test/test/paralell_spec.rb:77:in `block (2 levels) in <top (required)>' - 51601
124
+ ------ Begin output for /Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>' - 51601
123
125
  bar
124
- ------ Completed output for /Users/samwoods/parallel_test/test/paralell_spec.rb:77:in `block (2 levels) in <top (required)>' - 51601
126
+ ------ Completed output for /Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>' - 51601
125
127
 
126
- ------ Begin output for /Users/samwoods/parallel_test/test/paralell_spec.rb:77:in `block (2 levels) in <top (required)>' - 51602
128
+ ------ Begin output for /Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>' - 51602
127
129
  baz
128
- ------ Completed output for /Users/samwoods/parallel_test/test/paralell_spec.rb:77:in `block (2 levels) in <top (required)>' - 51602
130
+ ------ Completed output for /Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>' - 51602
129
131
  ```
@@ -1,3 +1,3 @@
1
- class InParallel
2
- VERSION = Version = '0.1.4'
1
+ module InParallel
2
+ VERSION = Version = '0.1.5'
3
3
  end
data/lib/in_parallel.rb CHANGED
@@ -1,246 +1,301 @@
1
1
  require_relative 'parallel_enumerable'
2
+ module InParallel
3
+ class InParallelExecutor
4
+ # How many seconds between outputting to stdout that we are waiting for child processes.
5
+ # 0 or < 0 means no signaling.
6
+ @@signal_interval = 30
7
+ @@process_infos = []
8
+ @@raise_error = nil
9
+ def self.process_infos
10
+ @@process_infos
11
+ end
2
12
 
3
- class InParallel
4
- # How many seconds between outputting to stdout that we are waiting for child processes.
5
- # 0 or < 0 means no signaling.
6
- @@signal_interval = 30
7
- @@process_infos = []
8
- @@raise_error = nil
9
- def self.process_infos
10
- @@process_infos
11
- end
13
+ @@background_objs = []
14
+ @@result_id = 0
12
15
 
13
- @@background_objs = []
14
- @@result_id = 0
16
+ @@pids = []
15
17
 
16
- @@pids = []
18
+ @@main_pid = Process.pid
17
19
 
18
- # Example - will spawn 2 processes, (1 for each method) wait until they both complete, and log STDOUT:
19
- # InParallel.run_in_parallel {
20
- # @result_1 = on agents[0], 'puppet agent -t'
21
- # @result_2 = on agents[1], 'puppet agent -t'
22
- # }
23
- # NOTE: Only supports assigning instance variables within the block, not local variables
24
- def self.run_in_parallel(&block)
25
- if Process.respond_to?(:fork)
26
- proxy = BlankBindingParallelProxy.new(self)
27
- proxy.instance_eval(&block)
28
- results_map = wait_for_processes
29
- # pass in the 'self' from the block.binding which is the instance of the class
30
- # that contains the initial binding call.
31
- # This gives us access to the local and instance variables from that context.
32
- return result_lookup(proxy, eval("self", block.binding), results_map)
20
+ def self.main_pid
21
+ @@main_pid
33
22
  end
34
- puts 'Warning: Fork is not supported on this OS, executing block normally'
35
- block.call
36
- end
37
-
38
- # Private method to lookup results from the results_map and replace the
39
- # temp values with actual return values
40
- def self.result_lookup(proxy_obj, target_obj, results_map)
41
- vars = (proxy_obj.instance_variables)
42
- results_map.keys.each { |tmp_result|
43
- vars.each {|var|
44
- if proxy_obj.instance_variable_get(var) == tmp_result
45
- target_obj.instance_variable_set(var, results_map[tmp_result])
46
- break
47
- end
48
- }
49
- }
50
- end
51
- private_class_method :result_lookup
52
23
 
53
- # Example - Will spawn a process in the background to run puppet agent on two agents and return immediately:
54
- # Parallel.run_in_background {
55
- # @result = on agents[0], 'puppet agent -t'
56
- # @result_2 = on agents[1], 'puppet agent -t'
57
- # }
58
- # # Do something else here before waiting for the process to complete
59
- #
60
- # # Optionally wait for the processes to complete before continuing.
61
- # # Otherwise use run_in_background(true) to clean up the process status and output immediately.
62
- # Parrallel.get_background_results(self)
63
- # NOTE: must call get_background_results to allow instance variables in calling object to be set,
64
- # otherwise @result will evaluate to "unresolved_parallel_result_0"
65
- def self.run_in_background(ignore_result = true, &block)
66
- if Process.respond_to?(:fork)
67
- proxy = BlankBindingParallelProxy.new(self)
68
- proxy.instance_eval(&block)
69
-
70
- if ignore_result
71
- Process.detach(@@process_infos.last[:pid])
72
- @@process_infos.pop
73
- else
74
- @@background_objs << {:proxy => proxy, :target => eval("self", block.binding)}
75
- return process_infos.last[:tmp_result]
24
+ # Example - will spawn 2 processes, (1 for each method) wait until they both complete, and log STDOUT:
25
+ # InParallel.run_in_parallel {
26
+ # @result_1 = method1
27
+ # @result_2 = method2
28
+ # }
29
+ # NOTE: Only supports assigning instance variables within the block, not local variables
30
+ def self.run_in_parallel(&block)
31
+ if Process.respond_to?(:fork)
32
+ proxy = BlankBindingParallelProxy.new(block.binding)
33
+ proxy.instance_eval(&block)
34
+ return wait_for_processes(proxy, block.binding)
76
35
  end
77
- return
36
+ puts 'Warning: Fork is not supported on this OS, executing block normally'
37
+ block.call
78
38
  end
79
- puts 'Warning: Fork is not supported on this OS, executing block normally'
80
- result = block.call
81
- return nil if ignore_result
82
- result
83
- end
84
-
85
- def self.get_background_results
86
- results_map = wait_for_processes
87
- # pass in the 'self' from the block.binding which is the instance of the class
88
- # that contains the initial binding call.
89
- # This gives us access to the instance variables from that context.
90
- @@background_objs.each {|obj|
91
- return result_lookup(obj[:proxy], obj[:target], results_map)
92
- }
93
- end
94
39
 
95
- # Waits for all processes to complete and logs STDOUT and STDERR in chunks from any processes
96
- # that were triggered from this Parallel class
97
- def self.wait_for_processes
98
- trap(:INT) do
99
- puts "Warning, recieved interrupt. Processing child results and exiting."
100
- @@process_infos.each { |process_info|
101
- # Send INT to each child process so it returns and can print stdout and stderr to console before exiting.
102
- Process.kill("INT", process_info[:pid])
103
- }
104
- end
105
- return unless Process.respond_to?(:fork)
106
- # Custom process to wait so that we can do things like time out, and kill child processes if
107
- # one process returns with an error before the others complete.
108
- results_map = {}
109
- timer = Time.now
110
- while !@@process_infos.empty? do
111
- if @@signal_interval > 0 && Time.now > timer + @@signal_interval
112
- puts 'Waiting for child processes.'
113
- timer = Time.now
114
- end
115
- @@process_infos.each {|process_info|
116
- # wait up to half a second for each thread to see if it is complete, if not, check the next thread.
117
- # returns immediately if the process has completed.
118
- thr = process_info[:wait_thread].join(0.5)
119
- unless thr.nil?
120
- # the process completed, get the result and rethrow on error.
121
- begin
122
- # Print the STDOUT and STDERR for each process with signals for start and end
123
- puts "\n------ Begin output for #{process_info[:method_sym]} - #{process_info[:pid]}\n"
124
- puts File.new(process_info[:std_out], 'r').readlines
125
- puts "------ Completed output for #{process_info[:method_sym]} - #{process_info[:pid]}\n"
126
- result = process_info[:result].read
127
- results_map[process_info[:tmp_result]] = (result.nil? || result.empty?) ? result : Marshal.load(result)
128
- File.delete(process_info[:std_out])
129
- # Kill all other processes and let them log their stdout before re-raising
130
- # if a child process raised an error.
131
- if results_map[process_info[:tmp_result]].is_a?(StandardError)
132
- @@process_infos.each{|p_info|
133
- begin
134
- Process.kill(0, p_info[:pid]) unless p_info[:pid] == process_info[:pid]
135
- rescue StandardError
136
- end
137
- }
138
- end
139
- ensure
140
- # close the read end pipe
141
- process_info[:result].close unless process_info[:result].closed?
142
- @@process_infos.delete(process_info)
143
- @@raise_error = results_map[process_info[:tmp_result]] if results_map[process_info[:tmp_result]].is_a?(StandardError)
40
+ # Private method to lookup results from the results_map and replace the
41
+ # temp values with actual return values
42
+ def self.result_lookup(proxy_obj, target_obj, results_map)
43
+ vars = (proxy_obj.instance_variables)
44
+ results = []
45
+ results_map.keys.each { |tmp_result|
46
+ results << results_map[tmp_result]
47
+ vars.each {|var|
48
+ if proxy_obj.instance_variable_get(var) == tmp_result
49
+ target_obj.instance_variable_set(var, results_map[tmp_result])
144
50
  break
145
51
  end
146
- end
52
+ }
147
53
  }
54
+ results
148
55
  end
56
+ private_class_method :result_lookup
149
57
 
150
- # Reset the error in case the error is rescued
151
- begin
152
- raise @@raise_error unless @@raise_error.nil?
153
- ensure
154
- @@raise_error = nil
155
- end
58
+ # Example - Will spawn a process in the background to run puppet agent on two agents and return immediately:
59
+ # Parallel.run_in_background {
60
+ # @result_1 = method1
61
+ # @result_2 = method2
62
+ # }
63
+ # # Do something else here before waiting for the process to complete
64
+ #
65
+ # # Optionally wait for the processes to complete before continuing.
66
+ # # Otherwise use run_in_background(true) to clean up the process status and output immediately.
67
+ # wait_for_processes(self)
68
+ # NOTE: must call get_background_results to allow instance variables in calling object to be set,
69
+ # otherwise @result_1 will evaluate to "unresolved_parallel_result_0"
70
+ def self.run_in_background(ignore_result = true, &block)
71
+ if Process.respond_to?(:fork)
72
+ proxy = BlankBindingParallelProxy.new(block.binding)
73
+ proxy.instance_eval(&block)
156
74
 
157
- return results_map
158
- end
75
+ if ignore_result
76
+ Process.detach(@@process_infos.last[:pid])
77
+ @@process_infos.pop
78
+ else
79
+ @@background_objs << {:proxy => proxy, :target => eval("self", block.binding)}
80
+ return process_infos.last[:tmp_result]
81
+ end
82
+ return
83
+ end
84
+ puts 'Warning: Fork is not supported on this OS, executing block normally'
85
+ result = block.call
86
+ return nil if ignore_result
87
+ result
88
+ end
159
89
 
160
- # private method to execute some code in a separate process and store the STDOUT and STDERR for later retrieval
161
- def self._execute_in_parallel(method_sym, obj = self, &block)
162
- ret_val = nil
163
- # Communicate the return value of the method or block
164
- read_result, write_result = IO.pipe
165
- pid = fork do
166
- exit_status = 0
90
+ # Waits for all processes to complete and logs STDOUT and STDERR in chunks from any processes
91
+ # that were triggered from this Parallel class
92
+ def self.wait_for_processes(proxy = nil, binding = nil)
167
93
  trap(:INT) do
168
- raise StandardError.new("Warning: Interrupt received; exiting...")
94
+ puts "Warning, recieved interrupt. Processing child results and exiting."
95
+ @@process_infos.each { |process_info|
96
+ # Send INT to each child process so it returns and can print stdout and stderr to console before exiting.
97
+ Process.kill("INT", process_info[:pid])
98
+ }
99
+ end
100
+ return unless Process.respond_to?(:fork)
101
+ # Custom process to wait so that we can do things like time out, and kill child processes if
102
+ # one process returns with an error before the others complete.
103
+ results_map = {}
104
+ timer = Time.now
105
+ while !@@process_infos.empty? do
106
+ if @@signal_interval > 0 && Time.now > timer + @@signal_interval
107
+ puts 'Waiting for child processes.'
108
+ timer = Time.now
109
+ end
110
+ @@process_infos.each {|process_info|
111
+ # wait up to half a second for each thread to see if it is complete, if not, check the next thread.
112
+ # returns immediately if the process has completed.
113
+ thr = process_info[:wait_thread].join(0.5)
114
+ unless thr.nil?
115
+ # the process completed, get the result and rethrow on error.
116
+ begin
117
+ # Print the STDOUT and STDERR for each process with signals for start and end
118
+ puts "\n------ Begin output for #{process_info[:method_sym]} - #{process_info[:pid]}\n"
119
+ puts File.new(process_info[:std_out], 'r').readlines
120
+ puts "------ Completed output for #{process_info[:method_sym]} - #{process_info[:pid]}\n"
121
+ result = process_info[:result].read
122
+ results_map[process_info[:tmp_result]] = (result.nil? || result.empty?) ? result : Marshal.load(result)
123
+ File.delete(process_info[:std_out])
124
+ # Kill all other processes and let them log their stdout before re-raising
125
+ # if a child process raised an error.
126
+ if results_map[process_info[:tmp_result]].is_a?(Exception)
127
+ @@process_infos.each{|p_info|
128
+ begin
129
+ Process.kill('SIGINT', p_info[:pid]) unless p_info[:pid] == process_info[:pid]
130
+ rescue StandardError
131
+ end
132
+ }
133
+ end
134
+ ensure
135
+ # close the read end pipe
136
+ process_info[:result].close unless process_info[:result].closed?
137
+ @@process_infos.delete(process_info)
138
+ @@raise_error = results_map[process_info[:tmp_result]] if results_map[process_info[:tmp_result]].is_a?(Exception)
139
+ break
140
+ end
141
+ end
142
+ }
169
143
  end
170
- Dir.mkdir('tmp') unless Dir.exists? 'tmp'
171
- write_file = File.new("tmp/parallel_process_#{Process.pid}", 'w')
172
-
173
- # IO buffer is 64kb, which isn't much... if debug logging is turned on,
174
- # this can be exceeded before a process completes.
175
- # Storing output in file rather than using IO.pipe
176
- STDOUT.reopen(write_file)
177
- STDERR.reopen(write_file)
178
144
 
145
+ # Reset the error in case the error is rescued
179
146
  begin
180
- # close subprocess's copy of read_result since it only needs to write
181
- read_result.close
182
- ret_val = obj.instance_eval(&block)
183
- # Write the result to the write_result IO stream.
184
- # Have to serialize the value so it can be transmitted via IO
185
- if(!ret_val.nil? && ret_val.singleton_methods && ret_val.class != TrueClass && ret_val.class != FalseClass && ret_val.class != Fixnum)
186
- #in case there are other types that can't be duped
147
+ raise @@raise_error unless @@raise_error.nil?
148
+ ensure
149
+ @@raise_error = nil
150
+ end
151
+
152
+ results = []
153
+
154
+ # pass in the 'self' from the block.binding which is the instance of the class
155
+ # that contains the initial binding call.
156
+ # This gives us access to the local and instance variables from that context.
157
+ results = result_lookup(proxy, eval("self", binding), results_map) if proxy && binding
158
+
159
+ @@background_objs.each {|obj|
160
+ results = results.concat result_lookup(obj[:proxy], obj[:target], results_map)
161
+ }
162
+
163
+ return results
164
+ end
165
+
166
+ # private method to execute some code in a separate process and store the STDOUT and STDERR for later retrieval
167
+ def self._execute_in_parallel(method_sym, obj = self, &block)
168
+ ret_val = nil
169
+ # Communicate the return value of the method or block
170
+ read_result, write_result = IO.pipe
171
+ pid = fork do
172
+ exit_status = 0
173
+ trap(:INT) do
174
+ puts("Warning: Interrupt received in child process; exiting #{Process.pid}")
175
+ return
176
+ end
177
+ Dir.mkdir('tmp') unless Dir.exists? 'tmp'
178
+ write_file = File.new("tmp/parallel_process_#{Process.pid}", 'w')
179
+
180
+ # IO buffer is 64kb, which isn't much... if debug logging is turned on,
181
+ # this can be exceeded before a process completes.
182
+ # Storing output in file rather than using IO.pipe
183
+ STDOUT.reopen(write_file)
184
+ STDERR.reopen(write_file)
185
+
186
+ begin
187
+ # close subprocess's copy of read_result since it only needs to write
188
+ read_result.close
189
+ ret_val = obj.instance_eval(&block)
190
+ # Write the result to the write_result IO stream.
191
+ # Have to serialize the value so it can be transmitted via IO
192
+ if(!ret_val.nil? && ret_val.singleton_methods && ret_val.class != TrueClass && ret_val.class != FalseClass && ret_val.class != Fixnum)
193
+ #in case there are other types that can't be duped
194
+ begin
195
+ ret_val = ret_val.dup
196
+ rescue StandardError => err
197
+ puts "Warning: return value from child process #{ret_val} " +
198
+ "could not be transferred to parent process: #{err.message}"
199
+ end
200
+ end
201
+ # In case there are other types that can't be dumped
187
202
  begin
188
- ret_val = ret_val.dup
203
+ Marshal.dump(ret_val, write_result) unless ret_val.nil?
189
204
  rescue StandardError => err
190
205
  puts "Warning: return value from child process #{ret_val} " +
191
206
  "could not be transferred to parent process: #{err.message}"
192
207
  end
208
+ rescue Exception => err
209
+ puts "Error in process #{pid}: #{err.message}"
210
+ # Return the error if an error is rescued so we can re-throw in the main process.
211
+ Marshal.dump(err, write_result)
212
+ exit_status = 1
213
+ ensure
214
+ write_result.close
215
+ exit exit_status
193
216
  end
194
- # In case there are other types that can't be dumped
195
- begin
196
- Marshal.dump(ret_val, write_result) unless ret_val.nil?
197
- rescue StandardError => err
198
- puts "Warning: return value from child process #{ret_val} " +
199
- "could not be transferred to parent process: #{err.message}"
200
- end
201
- rescue StandardError => err
202
- puts "Error in process #{pid}: #{err.message}"
203
- # Return the error if an error is rescued so we can re-throw in the main process.
204
- Marshal.dump(err, write_result)
205
- exit_status = 1
206
- ensure
207
- write_result.close
208
- exit exit_status
209
217
  end
218
+ write_result.close
219
+ # Process.detach returns a thread that will be nil if the process is still running and thr if not.
220
+ # This allows us to check to see if processes have exited without having to call the blocking Process.wait functions.
221
+ wait_thread = Process.detach(pid)
222
+ # store the IO object with the STDOUT and waiting thread for each pid
223
+ process_info = { :wait_thread => wait_thread,
224
+ :pid => pid,
225
+ :method_sym => method_sym,
226
+ :std_out => "tmp/parallel_process_#{pid}",
227
+ :result => read_result,
228
+ :tmp_result => "unresolved_parallel_result_#{@@result_id}" }
229
+ @@process_infos.push(process_info)
230
+ @@result_id += 1
231
+ process_info
210
232
  end
211
- write_result.close
212
- # Process.detach returns a thread that will be nil if the process is still running and thr if not.
213
- # This allows us to check to see if processes have exited without having to call the blocking Process.wait functions.
214
- wait_thread = Process.detach(pid)
215
- # store the IO object with the STDOUT and waiting thread for each pid
216
- process_info = { :wait_thread => wait_thread,
217
- :pid => pid,
218
- :method_sym => method_sym,
219
- :std_out => "tmp/parallel_process_#{pid}",
220
- :result => read_result,
221
- :tmp_result => "unresolved_parallel_result_#{@@result_id}" }
222
- @@process_infos.push(process_info)
223
- @@result_id += 1
224
- process_info
225
- end
226
233
 
227
- # Proxy class used to wrap each method execution in a block and run it in parallel
228
- # A block from Parallel.run_in_parallel is executed with a binding of an instance of this class
229
- class BlankBindingParallelProxy < BasicObject
230
- # Don't worry about running methods like puts or other basic stuff in parallel
231
- include ::Kernel
234
+ # Proxy class used to wrap each method execution in a block and run it in parallel
235
+ # A block from Parallel.run_in_parallel is executed with a binding of an instance of this class
236
+ class BlankBindingParallelProxy < BasicObject
237
+ # Don't worry about running methods like puts or other basic stuff in parallel
238
+ include ::Kernel
232
239
 
233
- def initialize(obj)
234
- @object = obj
235
- @result_id = 0
236
- end
240
+ def initialize(obj)
241
+ @object = obj
242
+ @result_id = 0
243
+ end
237
244
 
238
- # All methods within the block should show up as missing (unless defined in :Kernel)
239
- def method_missing(method_sym, *args, &block)
240
- out = ::InParallel._execute_in_parallel(method_sym) {@object.send(method_sym, *args, &block)}
241
- puts "Forked process for '#{method_sym}' - PID = '#{out[:pid]}'\n"
242
- out[:tmp_result]
245
+ # All methods within the block should show up as missing (unless defined in :Kernel)
246
+ def method_missing(method_sym, *args, &block)
247
+ if InParallelExecutor.main_pid == ::Process.pid
248
+ out = InParallelExecutor._execute_in_parallel("'#{method_sym.to_s}' #{caller_locations[0].to_s}", @object.eval('self')) {send(method_sym, *args, &block)}
249
+ puts "Forked process for '#{method_sym}' - PID = '#{out[:pid]}'\n"
250
+ out[:tmp_result]
251
+ end
252
+ end
243
253
  end
254
+ end
255
+
256
+ # Executes each method within a block in a different process
257
+ # Example - Will spawn a process in the background to execute each method
258
+ # Parallel.run_in_parallel {
259
+ # @result_1 = method1
260
+ # @result_2 = method2
261
+ # }
262
+ # NOTE - Only instance variables can be assigned the return values of the methods within the block.
263
+ # Local variables will not be assigned any values.
264
+ # @param [Block] block This method will yield to a block of code passed by the caller
265
+ # @return [Array<Result>, Result] the return values of each method within the block
266
+ def run_in_parallel(&block)
267
+ InParallelExecutor.run_in_parallel(&block)
268
+ end
269
+
270
+ # Forks a process for each method within a block and returns immediately
271
+ # Example 1 - Will fork a process in the background to execute each method and return immediately:
272
+ # Parallel.run_in_background {
273
+ # @result_1 = method1
274
+ # @result_2 = method2
275
+ # }
276
+ #
277
+ # Example 2 - Will fork a process in the background to execute each method, return immediately, then later
278
+ # wait for the process to complete, printing it's STDOUT and assigning return values to instance variables:
279
+ # Parallel.run_in_background(false) {
280
+ # @result_1 = method1
281
+ # @result_2 = method2
282
+ # }
283
+ # # Do something else here before waiting for the process to complete
284
+ #
285
+ # wait_for_processes
286
+ # NOTE: must call wait_for_processes to allow instance variables within the block to be set,
287
+ # otherwise results will evaluate to "unresolved_parallel_result_X"
288
+ # @param [Boolean] ignore_result True if you do not care about the STDOUT or return value of the methods executing in the background
289
+ # @param [Block] block This method will yield to a block of code passed by the caller
290
+ # @return [Array<Result>, Result] the return values of each method within the block
291
+ def run_in_background(ignore_result = true, &block)
292
+ InParallelExecutor.run_in_background(ignore_result, &block)
293
+ end
244
294
 
295
+ # Waits for all processes started by run_in_background to complete execution, then prints STDOUT
296
+ # and assigns return values to instance variables. See :run_in_background
297
+ # @return [Array<Result>, Result] the temporary return values of each method within the block
298
+ def wait_for_processes
299
+ InParallelExecutor.wait_for_processes
245
300
  end
246
301
  end
@@ -8,11 +8,11 @@ module Enumerable
8
8
  if Process.respond_to?(:fork) && count > 1
9
9
  method_sym ||= "#{caller_locations[0]}"
10
10
  each do |item|
11
- out = InParallel._execute_in_parallel(method_sym) {block.call(item)}
11
+ out = InParallelExecutor._execute_in_parallel(method_sym) {block.call(item)}
12
12
  puts "'each_in_parallel' forked process for '#{method_sym}' - PID = '#{out[:pid]}'\n"
13
13
  end
14
14
  # return the array of values, no need to look up from the map.
15
- return InParallel.wait_for_processes.values
15
+ return InParallel.wait_for_processes
16
16
  end
17
17
  puts 'Warning: Fork is not supported on this OS, executing block normally' unless Process.respond_to? :fork
18
18
  block.call
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: in-parallel
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - samwoods1
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-05-31 00:00:00.000000000 Z
11
+ date: 2016-06-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -59,6 +59,7 @@ extra_rdoc_files: []
59
59
  files:
60
60
  - Gemfile
61
61
  - LICENSE
62
+ - MAINTAINERS.md
62
63
  - README.md
63
64
  - Rakefile
64
65
  - in_parallel.gemspec