in-parallel 0.1.8 → 0.1.9

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: fde05adab05cca1454b0c4f9187cddc062f32fe9
4
- data.tar.gz: 12ae2280dcfd30cc7875a863c05abeff93e9eade
3
+ metadata.gz: a6277b0e376bda7d26d7f16760f16764569bdf01
4
+ data.tar.gz: f0798ecc56cd7e02cd1784fbc28f4b19a3e76118
5
5
  SHA512:
6
- metadata.gz: fe93efe91c47632f57062d29f903d88b723a15f4333cf7bcdd7c8802ff7712481c37a52a70f92f736ac10662e2bee393a5e52ca6f9003603babb7c9273a026d5
7
- data.tar.gz: 37bcdff90122259424d7e2523f9a33cf2bbe56755bfd277802a2a5b386d73aef850b2c07422b5d4286c165bed8208015289db4f51ba4a6a32b534ab425f51203
6
+ metadata.gz: b3cb0370b0254ce1a801b32d7fcab2bb80acfeec6d0253a8752560c1da8ddf53de94e8a71d3e61369d204be44617451d26f909126ee484b61d0302c1b47d377d
7
+ data.tar.gz: 2f769c82a0fb813eb3ebfbeb0f09a8fe1a00086ea1e0220bca5913c1c47d5a617d625070627d9e7a619fdb42aeb9a370c9a6aedb53b333111fee2d47840644cc
@@ -1,3 +1,3 @@
1
1
  module InParallel
2
- VERSION = Version = '0.1.8'
2
+ VERSION = Version = '0.1.9'
3
3
  end
data/lib/in_parallel.rb CHANGED
@@ -4,8 +4,8 @@ module InParallel
4
4
  # How many seconds between outputting to stdout that we are waiting for child processes.
5
5
  # 0 or < 0 means no signaling.
6
6
  @@signal_interval = 30
7
+ @@timeout = 1800
7
8
  @@process_infos = []
8
- @@raise_error = nil
9
9
  def self.process_infos
10
10
  @@process_infos
11
11
  end
@@ -21,42 +21,30 @@ module InParallel
21
21
  @@main_pid
22
22
  end
23
23
 
24
+ def self.timeout
25
+ @@timeout
26
+ end
27
+
28
+ def self.timeout=(value)
29
+ @@timeout = value
30
+ end
31
+
24
32
  # Example - will spawn 2 processes, (1 for each method) wait until they both complete, and log STDOUT:
25
33
  # InParallel.run_in_parallel {
26
34
  # @result_1 = method1
27
35
  # @result_2 = method2
28
36
  # }
29
37
  # NOTE: Only supports assigning instance variables within the block, not local variables
30
- def self.run_in_parallel(&block)
38
+ def self.run_in_parallel(timeout = @@timeout, kill_all_on_error = false, &block)
31
39
  if Process.respond_to?(:fork)
32
40
  proxy = BlankBindingParallelProxy.new(block.binding)
33
41
  proxy.instance_eval(&block)
34
- return wait_for_processes(proxy, block.binding)
42
+ return wait_for_processes(proxy, block.binding, timeout, kill_all_on_error)
35
43
  end
36
44
  puts 'Warning: Fork is not supported on this OS, executing block normally'
37
45
  block.call
38
46
  end
39
47
 
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
- target_obj = eval('self', target_obj)
44
- proxy_obj ||= target_obj
45
- vars = (proxy_obj.instance_variables)
46
- results = []
47
- results_map.keys.each { |tmp_result|
48
- results << results_map[tmp_result]
49
- vars.each {|var|
50
- if proxy_obj.instance_variable_get(var) == tmp_result
51
- target_obj.instance_variable_set(var, results_map[tmp_result])
52
- break
53
- end
54
- }
55
- }
56
- results
57
- end
58
- private_class_method :result_lookup
59
-
60
48
  # Example - Will spawn a process in the background to run puppet agent on two agents and return immediately:
61
49
  # Parallel.run_in_background {
62
50
  # @result_1 = method1
@@ -91,24 +79,35 @@ module InParallel
91
79
 
92
80
  # Waits for all processes to complete and logs STDOUT and STDERR in chunks from any processes
93
81
  # that were triggered from this Parallel class
94
- def self.wait_for_processes(proxy = self, binding = nil)
82
+ # @param [Object] proxy - The instance of the proxy class that the method was executed within
83
+ # (probably only useful when called by run_in_background)
84
+ # @param [Object] binding - The binding of the block to assign return values to instance variables
85
+ # (probably only useful when called by run_in_background)
86
+ # @param [Int] timeout Time in seconds to wait before giving up on a child process
87
+ # @param [Boolean] kill_all_on_error Whether to wait for all processes to complete, or fail immediately -
88
+ # killing all other forked processes - when one process errors.
89
+ def self.wait_for_processes(proxy = self, binding = nil, timeout = nil, kill_all_on_error = false)
90
+ raise_error = nil
91
+ timeout ||= @@timeout
95
92
  trap(:INT) do
96
93
  puts "Warning, recieved interrupt. Processing child results and exiting."
97
- @@process_infos.each { |process_info|
98
- # Send INT to each child process so it returns and can print stdout and stderr to console before exiting.
99
- Process.kill("INT", process_info[:pid])
100
- }
94
+ kill_child_processes
101
95
  end
102
96
  return unless Process.respond_to?(:fork)
103
97
  # Custom process to wait so that we can do things like time out, and kill child processes if
104
98
  # one process returns with an error before the others complete.
105
- results_map = {}
106
- timer = Time.now
99
+ results_map = Array.new(@@process_infos.count)
100
+ start_time = Time.now
101
+ timer = start_time
107
102
  while !@@process_infos.empty? do
108
103
  if @@signal_interval > 0 && Time.now > timer + @@signal_interval
109
104
  puts 'Waiting for child processes.'
110
105
  timer = Time.now
111
106
  end
107
+ if Time.now > start_time + timeout
108
+ kill_child_processes
109
+ raise_error = ::RuntimeError.new("Child process ran longer than timeout of #{timeout}")
110
+ end
112
111
  @@process_infos.each {|process_info|
113
112
  # wait up to half a second for each thread to see if it is complete, if not, check the next thread.
114
113
  # returns immediately if the process has completed.
@@ -121,48 +120,42 @@ module InParallel
121
120
  puts File.new(process_info[:std_out], 'r').readlines
122
121
  puts "------ Completed output for #{process_info[:method_sym]} - #{process_info[:pid]}\n"
123
122
  result = process_info[:result].read
124
- results_map[process_info[:tmp_result]] = (result.nil? || result.empty?) ? result : Marshal.load(result)
123
+ marshalled_result = (result.nil? || result.empty?) ? result : Marshal.load(result)
124
+ if marshalled_result.is_a?(Exception)
125
+ raise_error = marshalled_result.dup
126
+ kill_child_processes if kill_all_on_error
127
+ marshalled_result = nil
128
+ end
129
+ results_map[process_info[:index]] = {process_info[:tmp_result] => marshalled_result}
125
130
  File.delete(process_info[:std_out])
126
131
  # Kill all other processes and let them log their stdout before re-raising
127
132
  # if a child process raised an error.
128
- if results_map[process_info[:tmp_result]].is_a?(Exception)
129
- @@process_infos.each{|p_info|
130
- begin
131
- Process.kill('SIGINT', p_info[:pid]) unless p_info[:pid] == process_info[:pid]
132
- rescue StandardError
133
- end
134
- }
135
- end
136
133
  ensure
137
134
  # close the read end pipe
138
135
  process_info[:result].close unless process_info[:result].closed?
139
136
  @@process_infos.delete(process_info)
140
- @@raise_error = results_map[process_info[:tmp_result]] if results_map[process_info[:tmp_result]].is_a?(Exception)
141
- break
142
137
  end
143
138
  end
144
139
  }
145
140
  end
146
141
 
147
- # Reset the error in case the error is rescued
148
- begin
149
- raise @@raise_error unless @@raise_error.nil?
150
- ensure
151
- @@raise_error = nil
152
- end
153
-
154
142
  results = []
155
143
 
156
144
  # pass in the 'self' from the block.binding which is the instance of the class
157
145
  # that contains the initial binding call.
158
- # This gives us access to the local and instance variables from that context.
146
+ # This gives us access to the instance variables from that context.
159
147
  results = result_lookup(proxy, binding, results_map) if binding
160
148
 
149
+ # If there are background_objs AND results, don't return the background obj results
150
+ # (which would mess up expected results from each_in_parallel),
151
+ # but do process their results in case they are assigned to instance variables
161
152
  @@background_objs.each {|obj|
162
- results = results.concat result_lookup(obj[:proxy], obj[:target], results_map)
153
+ result_lookup(obj[:proxy], obj[:target], results_map)
163
154
  }
164
155
  @@background_objs.clear
165
156
 
157
+ raise raise_error unless raise_error.nil?
158
+
166
159
  return results
167
160
  end
168
161
 
@@ -176,6 +169,7 @@ module InParallel
176
169
  exit_status = 0
177
170
  trap(:INT) do
178
171
  puts("Warning: Interrupt received in child process; exiting #{Process.pid}")
172
+ kill_child_processes
179
173
  return
180
174
  end
181
175
  write_file = File.new("tmp/parallel_process_#{Process.pid}", 'w')
@@ -228,12 +222,45 @@ module InParallel
228
222
  :method_sym => method_sym,
229
223
  :std_out => "tmp/parallel_process_#{pid}",
230
224
  :result => read_result,
231
- :tmp_result => "unresolved_parallel_result_#{@@result_id}" }
225
+ :tmp_result => "unresolved_parallel_result_#{@@result_id}",
226
+ :index => @@process_infos.count }
232
227
  @@process_infos.push(process_info)
233
228
  @@result_id += 1
234
229
  process_info
235
230
  end
236
231
 
232
+ def self.kill_child_processes
233
+ @@process_infos.each { |process_info|
234
+ # Send INT to each child process so it returns and can print stdout and stderr to console before exiting.
235
+ begin
236
+ Process.kill("INT", process_info[:pid])
237
+ rescue Errno::ESRCH
238
+ # If one of the other processes has completed in the very short time before we try to kill it, handle the exception
239
+ end
240
+ }
241
+ end
242
+ private_class_method :kill_child_processes
243
+
244
+ # Private method to lookup results from the results_map and replace the
245
+ # temp values with actual return values
246
+ def self.result_lookup(proxy_obj, target_obj, results_map)
247
+ target_obj = eval('self', target_obj)
248
+ proxy_obj ||= target_obj
249
+ vars = (proxy_obj.instance_variables)
250
+ results = []
251
+ results_map.each { |tmp_result|
252
+ results << tmp_result.values[0]
253
+ vars.each {|var|
254
+ if proxy_obj.instance_variable_get(var) == tmp_result.keys[0]
255
+ target_obj.instance_variable_set(var, tmp_result.values[0])
256
+ break
257
+ end
258
+ }
259
+ }
260
+ results
261
+ end
262
+ private_class_method :result_lookup
263
+
237
264
  # Proxy class used to wrap each method execution in a block and run it in parallel
238
265
  # A block from Parallel.run_in_parallel is executed with a binding of an instance of this class
239
266
  class BlankBindingParallelProxy < BasicObject
@@ -264,10 +291,14 @@ module InParallel
264
291
  # }
265
292
  # NOTE - Only instance variables can be assigned the return values of the methods within the block.
266
293
  # Local variables will not be assigned any values.
294
+ # @param [Int] timeout Time in seconds to wait before giving up on a child process
295
+ # @param [Boolean] kill_all_on_error Whether to wait for all processes to complete, or fail immediately -
296
+ # killing all other forked processes - when one process errors.
267
297
  # @param [Block] block This method will yield to a block of code passed by the caller
268
298
  # @return [Array<Result>, Result] the return values of each method within the block
269
- def run_in_parallel(&block)
270
- InParallelExecutor.run_in_parallel(&block)
299
+ def run_in_parallel(timeout=nil, kill_all_on_error = false, &block)
300
+ timeout ||= InParallelExecutor.timeout
301
+ InParallelExecutor.run_in_parallel(timeout, kill_all_on_error, &block)
271
302
  end
272
303
 
273
304
  # Forks a process for each method within a block and returns immediately
@@ -297,8 +328,12 @@ module InParallel
297
328
 
298
329
  # Waits for all processes started by run_in_background to complete execution, then prints STDOUT
299
330
  # and assigns return values to instance variables. See :run_in_background
331
+ # @param [Int] timeout Time in seconds to wait before giving up on a child process
332
+ # @param [Boolean] kill_all_on_error Whether to wait for all processes to complete, or fail immediately -
333
+ # killing all other forked processes - when one process errors.
300
334
  # @return [Array<Result>, Result] the temporary return values of each method within the block
301
- def wait_for_processes
302
- InParallelExecutor.wait_for_processes
335
+ def wait_for_processes(timeout=nil, kill_all_on_error = false)
336
+ timeout ||= InParallelExecutor.timeout
337
+ InParallelExecutor.wait_for_processes(nil, nil, timeout, kill_all_on_error)
303
338
  end
304
339
  end
@@ -8,8 +8,9 @@ module Enumerable
8
8
  # my_method(int)
9
9
  # }
10
10
  # @param [String] identifier - Optional identifier for logging purposes only. Will use the block location by default.
11
+ # @param [Int] timeout - Seconds to wait for a forked process to complete before timing out
11
12
  # @return [Array<Object>] results - the return value of each block execution.
12
- def each_in_parallel(identifier=nil, &block)
13
+ def each_in_parallel(identifier=nil, timeout=(InParallel::InParallelExecutor.timeout), kill_all_on_error = false, &block)
13
14
  if Process.respond_to?(:fork) && count > 1
14
15
  identifier ||= "#{caller_locations[0]}"
15
16
  each do |item|
@@ -17,7 +18,7 @@ module Enumerable
17
18
  puts "'each_in_parallel' forked process for '#{identifier}' - PID = '#{out[:pid]}'\n"
18
19
  end
19
20
  # return the array of values, no need to look up from the map.
20
- return InParallel::InParallelExecutor.wait_for_processes(nil, block.binding)
21
+ return InParallel::InParallelExecutor.wait_for_processes(nil, block.binding, timeout, kill_all_on_error)
21
22
  end
22
23
  puts 'Warning: Fork is not supported on this OS, executing block normally' unless Process.respond_to? :fork
23
24
  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.8
4
+ version: 0.1.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - samwoods1
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-06-08 00:00:00.000000000 Z
11
+ date: 2016-06-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler