in-parallel 0.1.8 → 0.1.9

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: 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