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 +4 -4
- data/MAINTAINERS.md +8 -0
- data/README.md +21 -19
- data/in_parallel/version.rb +2 -2
- data/lib/in_parallel.rb +265 -210
- data/lib/parallel_enumerable.rb +2 -2
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a79b9831824dee514b121f2911693c825c8bda13
|
4
|
+
data.tar.gz: 630845c4f1c5d2f22b83cb6e2a7467118371bf91
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
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
|
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
|
-
###
|
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
|
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
|
-
###
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
115
|
-
'each_in_parallel' spawned process for '/Users/samwoods/parallel_test/test
|
116
|
-
'each_in_parallel' spawned process for '/Users/samwoods/parallel_test/test
|
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
|
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
|
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
|
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
|
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
|
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
|
130
|
+
------ Completed output for /Users/samwoods/parallel_test/test.rb:77:in `block (2 levels) in <top (required)>' - 51602
|
129
131
|
```
|
data/in_parallel/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
|
-
|
2
|
-
VERSION = Version = '0.1.
|
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
|
-
|
4
|
-
|
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
|
-
|
14
|
-
@@result_id = 0
|
16
|
+
@@pids = []
|
15
17
|
|
16
|
-
|
18
|
+
@@main_pid = Process.pid
|
17
19
|
|
18
|
-
|
19
|
-
|
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
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
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
|
-
|
52
|
+
}
|
147
53
|
}
|
54
|
+
results
|
148
55
|
end
|
56
|
+
private_class_method :result_lookup
|
149
57
|
|
150
|
-
#
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
158
|
-
|
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
|
-
|
161
|
-
|
162
|
-
|
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
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
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
|
-
|
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
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
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
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
240
|
+
def initialize(obj)
|
241
|
+
@object = obj
|
242
|
+
@result_id = 0
|
243
|
+
end
|
237
244
|
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
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
|
data/lib/parallel_enumerable.rb
CHANGED
@@ -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 =
|
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
|
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
|
+
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-
|
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
|