process-group 1.0.1 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +2 -0
- data/README.md +58 -39
- data/lib/process/group.rb +53 -11
- data/lib/process/group/version.rb +1 -1
- data/spec/process/group/fork_spec.rb +34 -39
- data/spec/process/group/interrupt_spec.rb +106 -110
- data/spec/process/group/io_spec.rb +22 -26
- data/spec/process/group/load_spec.rb +17 -21
- data/spec/process/group/process_spec.rb +29 -31
- data/spec/process/group/spawn_spec.rb +67 -77
- data/spec/process/group/wait_spec.rb +85 -0
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 98b4baed52fa9d14e546993096a2e68ca5e2550e
|
4
|
+
data.tar.gz: 630111d7484ecdc7e850c48afc3c437767f3695b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 57f6dcf3d08a2b85315633c630cca1ace63bfcf678db80882125c1c72773e4377e85503677a58f6d0dfd7917404d3e9085da8de7f09d4c250838f33192fcee2a
|
7
|
+
data.tar.gz: 29f4993f4ded486db893cad0855a6c4af59d4f89ded8f638d99459d4188917c4b9156135f97b98c968e147cb8d31bfb5c7ec1026e37e505a825a78db115fd508
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -12,86 +12,105 @@
|
|
12
12
|
|
13
13
|
Add this line to your application's Gemfile:
|
14
14
|
|
15
|
-
|
15
|
+
gem 'process-group'
|
16
16
|
|
17
17
|
And then execute:
|
18
18
|
|
19
|
-
|
19
|
+
$ bundle
|
20
20
|
|
21
21
|
Or install it yourself as:
|
22
22
|
|
23
|
-
|
23
|
+
$ gem install process-group
|
24
24
|
|
25
25
|
## Usage
|
26
26
|
|
27
27
|
The simplest concurrent usage is as follows:
|
28
28
|
|
29
29
|
# Create a new process group:
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
30
|
+
Process::Group.wait do |group|
|
31
|
+
# Run the command (non-blocking):
|
32
|
+
group.run("sleep 1") do |exit_status|
|
33
|
+
# Running in a separate fiber, will execute this code once the process completes:
|
34
|
+
puts "Command finished with status: #{exit_status}"
|
35
|
+
end
|
36
|
+
|
37
|
+
# Do something else here:
|
38
|
+
sleep(1)
|
39
|
+
|
40
|
+
# Wait for all processes in group to finish.
|
36
41
|
end
|
37
|
-
|
38
|
-
# Do something else here:
|
39
|
-
sleep(1)
|
40
|
-
|
41
|
-
# Wait for all processes in group to finish:
|
42
|
-
group.wait
|
43
42
|
|
44
|
-
The `group.wait` call is an explicit
|
43
|
+
The `group.wait` call is an explicit synchronization point, and if it completes successfully, all processes/fibers have finished successfully. If an error is raised in a fiber, it will be passed back out through `group.wait` and this is the only failure condition. Even if this occurs, all children processes are guaranteed to be cleaned up.
|
45
44
|
|
46
45
|
### Explicit Fibers
|
47
46
|
|
48
47
|
Items within a single fiber will execute sequentially. Processes (e.g. via `Group#spawn`) will run concurrently in multiple fibers.
|
49
48
|
|
50
|
-
|
49
|
+
Process::Group.wait do |group|
|
50
|
+
# Explicity manage concurrency in this fiber:
|
51
|
+
Fiber.new do
|
52
|
+
# These processes will be run sequentially:
|
53
|
+
group.spawn("sleep 1")
|
54
|
+
group.spawn("sleep 1")
|
55
|
+
end.resume
|
51
56
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
group.spawn("sleep 1")
|
56
|
-
group.spawn("sleep 1")
|
57
|
-
end.resume
|
58
|
-
|
59
|
-
# Implicitly run this task concurrently as the above fiber:
|
60
|
-
group.run("sleep 2")
|
61
|
-
|
62
|
-
# Wait for fiber to complete:
|
63
|
-
group.wait
|
57
|
+
# Implicitly run this task concurrently as the above fiber:
|
58
|
+
group.run("sleep 2")
|
59
|
+
end
|
64
60
|
|
65
61
|
`Group#spawn` is theoretically identical to `Process#spawn` except the processes are run concurrently if possible.
|
66
62
|
|
67
|
-
###
|
63
|
+
### Explicit Wait
|
68
64
|
|
69
|
-
|
65
|
+
The recommended approach to use process group is to call `Process::Group.wait` with a block which invokes tasks. This block is wrapped in appropriate `rescue Interrupt` and `ensure` blocks which guarantee that the process group is cleaned up:
|
66
|
+
|
67
|
+
Process::Group.wait do |group|
|
68
|
+
group.run("sleep 10")
|
69
|
+
end
|
70
|
+
|
71
|
+
It is also possible to invoke this machinery and reuse the process group simply by instantiating the group and calling wait explicitly:
|
70
72
|
|
71
73
|
group = Process::Group.new
|
72
74
|
|
73
|
-
|
75
|
+
group.wait do
|
76
|
+
group.run("sleep 10")
|
77
|
+
end
|
78
|
+
|
79
|
+
It is also possible to queue tasks for execution outside the wait block. But by design, it's only possible to execute tasks within the wait block. Tasks added outside a wait block will be queued up for execution when `#wait` is invoked:
|
80
|
+
|
81
|
+
group = Process::Group.new
|
74
82
|
|
75
|
-
|
76
|
-
group.run(env, "sleep 1", chdir: "/tmp")
|
83
|
+
group.run("sleep 10")
|
77
84
|
|
85
|
+
# Run command here:
|
78
86
|
group.wait
|
79
87
|
|
88
|
+
### Specify Options
|
89
|
+
|
90
|
+
You can specify options to `Group#run` and `Group#spawn` just like `Process::spawn`:
|
91
|
+
|
92
|
+
Process::Group.wait do |group|
|
93
|
+
env = {'FOO' => 'BAR'}
|
94
|
+
|
95
|
+
# Arguments are essentially the same as Process::spawn.
|
96
|
+
group.run(env, "sleep 1", chdir: "/tmp")
|
97
|
+
end
|
98
|
+
|
80
99
|
### Process Limit
|
81
100
|
|
82
|
-
The process group can be used as a way to spawn multiple processes, but sometimes you'd like to limit the number of parallel processes to something relating to the number of processors in the system.
|
101
|
+
The process group can be used as a way to spawn multiple processes, but sometimes you'd like to limit the number of parallel processes to something relating to the number of processors in the system. By default, there is no limit on the number of processes running concurrently.
|
83
102
|
|
84
103
|
# 'facter' gem - found a bit slow to initialise, but most widely supported.
|
85
104
|
require 'facter'
|
86
105
|
group = Process::Group.new(limit: Facter.processorcount)
|
87
|
-
|
106
|
+
|
88
107
|
# 'system' gem - found very fast, less wide support (but nothing really important).
|
89
108
|
require 'system'
|
90
109
|
group = Process::Group.new(limit: System::CPU.count)
|
91
|
-
|
110
|
+
|
92
111
|
# hardcoded - set to n (8 < n < 32) and let the OS scheduler worry about it.
|
93
112
|
group = Process::Group.new(limit: 32)
|
94
|
-
|
113
|
+
|
95
114
|
# unlimited - default.
|
96
115
|
group = Process::Group.new
|
97
116
|
|
@@ -101,7 +120,7 @@ It is possible to send a signal (kill) to the entire process group:
|
|
101
120
|
|
102
121
|
group.kill(:TERM)
|
103
122
|
|
104
|
-
If there are no running processes, this is a no-op (rather than an error).
|
123
|
+
If there are no running processes, this is a no-op (rather than an error). [Proper handling of SIGINT/SIGQUIT](http://www.cons.org/cracauer/sigint.html) explains how to use signals correctly.
|
105
124
|
|
106
125
|
#### Handling Interrupts
|
107
126
|
|
data/lib/process/group.rb
CHANGED
@@ -23,6 +23,12 @@ require 'fiber'
|
|
23
23
|
module Process
|
24
24
|
# A group of tasks which can be run asynchrnously using fibers. Someone must call Group#wait to ensure that all fibers eventually resume.
|
25
25
|
class Group
|
26
|
+
def self.wait(**options, &block)
|
27
|
+
group = Group.new(options)
|
28
|
+
|
29
|
+
group.wait(&block)
|
30
|
+
end
|
31
|
+
|
26
32
|
# Executes a command using Process.spawn with the given arguments and options.
|
27
33
|
class Command
|
28
34
|
def initialize(arguments, options, fiber = Fiber.current)
|
@@ -88,6 +94,9 @@ module Process
|
|
88
94
|
@fiber = nil
|
89
95
|
|
90
96
|
@pgid = nil
|
97
|
+
|
98
|
+
# Whether we can actively schedule tasks or not:
|
99
|
+
@waiting = false
|
91
100
|
end
|
92
101
|
|
93
102
|
# A table of currently running processes.
|
@@ -102,16 +111,20 @@ module Process
|
|
102
111
|
|
103
112
|
-@pgid
|
104
113
|
end
|
105
|
-
|
114
|
+
|
115
|
+
def queued?
|
116
|
+
@queue.size > 0
|
117
|
+
end
|
118
|
+
|
106
119
|
# Are there processes currently running?
|
107
120
|
def running?
|
108
121
|
@running.size > 0
|
109
122
|
end
|
110
|
-
|
123
|
+
|
111
124
|
# Run a process in a new fiber, arguments have same meaning as Process#spawn.
|
112
|
-
def run(*arguments)
|
125
|
+
def run(*arguments, **options)
|
113
126
|
Fiber.new do
|
114
|
-
exit_status = self.spawn(*arguments)
|
127
|
+
exit_status = self.spawn(*arguments, **options)
|
115
128
|
|
116
129
|
yield exit_status if block_given?
|
117
130
|
end.resume
|
@@ -141,20 +154,26 @@ module Process
|
|
141
154
|
not available?
|
142
155
|
end
|
143
156
|
|
144
|
-
# Wait for all running and queued processes to finish.
|
157
|
+
# Wait for all running and queued processes to finish. If you provide a block, it will be invoked before waiting, but within canonical signal handling machinery.
|
145
158
|
def wait
|
146
159
|
raise ArgumentError.new("Cannot call Process::Group#wait from child process!") unless @pid == Process.pid
|
147
160
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
schedule!
|
161
|
+
waiting do
|
162
|
+
yield(self) if block_given?
|
152
163
|
|
153
|
-
|
164
|
+
while running?
|
165
|
+
process, status = wait_one
|
166
|
+
|
167
|
+
schedule!
|
168
|
+
|
169
|
+
process.resume(status)
|
170
|
+
end
|
154
171
|
end
|
155
172
|
|
156
173
|
# No processes, process group is no longer valid:
|
157
174
|
@pgid = nil
|
175
|
+
|
176
|
+
return self
|
158
177
|
rescue Interrupt
|
159
178
|
# If the user interrupts the wait, interrupt the process group and wait for them to finish:
|
160
179
|
self.kill(:INT)
|
@@ -182,13 +201,33 @@ module Process
|
|
182
201
|
end
|
183
202
|
end
|
184
203
|
|
204
|
+
def to_s
|
205
|
+
"#<#{self.class} running=#{@running.size} queued=#{@queue.count} limit=#{@limit} pgid=#{@pgid}>"
|
206
|
+
end
|
207
|
+
|
185
208
|
private
|
186
209
|
|
210
|
+
# The waiting loop, schedule any outstanding tasks:
|
211
|
+
def waiting
|
212
|
+
@waiting = true
|
213
|
+
|
214
|
+
# Schedule any queued tasks:
|
215
|
+
schedule!
|
216
|
+
|
217
|
+
yield
|
218
|
+
ensure
|
219
|
+
@waiting = false
|
220
|
+
end
|
221
|
+
|
222
|
+
def waiting?
|
223
|
+
@waiting
|
224
|
+
end
|
225
|
+
|
187
226
|
# Append a process to the queue and schedule it for execution if possible.
|
188
227
|
def append!(process)
|
189
228
|
@queue << process
|
190
229
|
|
191
|
-
schedule!
|
230
|
+
schedule! if waiting?
|
192
231
|
|
193
232
|
Fiber.yield
|
194
233
|
end
|
@@ -214,6 +253,9 @@ module Process
|
|
214
253
|
# Wait for all children to exit but without resuming any controlling fibers.
|
215
254
|
def wait_all
|
216
255
|
wait_one while running?
|
256
|
+
|
257
|
+
# Clear any queued tasks:
|
258
|
+
@queue.clear
|
217
259
|
end
|
218
260
|
|
219
261
|
# Wait for one process, should only be called when a child process has finished, otherwise would block.
|
@@ -20,46 +20,41 @@
|
|
20
20
|
|
21
21
|
require 'process/group'
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
end
|
36
|
-
|
37
|
-
expect(result.exitstatus).to be == 1
|
38
|
-
end.resume
|
39
|
-
|
23
|
+
RSpec.describe Process::Group do
|
24
|
+
it "should fork and write to pipe" do
|
25
|
+
input, output = IO.pipe
|
26
|
+
|
27
|
+
Fiber.new do
|
28
|
+
result = subject.fork do
|
29
|
+
output.puts "Hello World"
|
30
|
+
|
31
|
+
exit(1)
|
32
|
+
end
|
33
|
+
|
34
|
+
# We need to close output so that input.read will encounter end of stream.
|
40
35
|
output.close
|
41
|
-
|
42
|
-
group.wait
|
43
|
-
|
44
|
-
expect(input.read).to be == "Hello World\n"
|
45
|
-
end
|
46
|
-
|
47
|
-
it "should not throw interrupt from fork" do
|
48
|
-
group = Process::Group.new
|
49
|
-
|
50
|
-
Fiber.new do
|
51
|
-
result = group.fork do
|
52
|
-
# Don't print out a backtrace when Ruby invariably exits due to the execption below:
|
53
|
-
$stderr.reopen('/dev/null', 'w')
|
54
|
-
|
55
|
-
raise Interrupt
|
56
|
-
end
|
57
36
|
|
58
|
-
|
59
|
-
|
37
|
+
expect(result.exitstatus).to be == 1
|
38
|
+
end.resume
|
60
39
|
|
61
|
-
|
62
|
-
|
63
|
-
|
40
|
+
subject.wait
|
41
|
+
|
42
|
+
expect(input.read).to be == "Hello World\n"
|
43
|
+
end
|
44
|
+
|
45
|
+
it "should not throw interrupt from fork" do
|
46
|
+
Fiber.new do
|
47
|
+
result = subject.fork do
|
48
|
+
# Don't print out a backtrace when Ruby invariably exits due to the execption below:
|
49
|
+
$stderr.reopen('/dev/null', 'w')
|
50
|
+
|
51
|
+
raise Interrupt
|
52
|
+
end
|
53
|
+
|
54
|
+
expect(result.exitstatus).not_to be == 0
|
55
|
+
end.resume
|
56
|
+
|
57
|
+
# Shouldn't raise any errors:
|
58
|
+
subject.wait
|
64
59
|
end
|
65
|
-
end
|
60
|
+
end
|
@@ -20,131 +20,127 @@
|
|
20
20
|
|
21
21
|
require 'process/group'
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
group = Process::Group.new
|
27
|
-
checkpoint = ""
|
23
|
+
RSpec.describe Process::Group do
|
24
|
+
it "should raise interrupt exception" do
|
25
|
+
checkpoint = ""
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
result = group.fork { sleep 0.1 }
|
33
|
-
|
34
|
-
expect(result).to be == 0
|
35
|
-
|
36
|
-
checkpoint += 'Y'
|
37
|
-
|
38
|
-
# Simulate the user pressing Ctrl-C after 0.5 seconds:
|
39
|
-
raise Interrupt
|
40
|
-
end.resume
|
41
|
-
|
42
|
-
Fiber.new do
|
43
|
-
checkpoint += 'A'
|
44
|
-
|
45
|
-
# This never returns:
|
46
|
-
result = group.fork { sleep 0.2 }
|
47
|
-
|
48
|
-
checkpoint += 'B'
|
49
|
-
end.resume
|
50
|
-
|
51
|
-
expect(group).to receive(:kill).with(:INT).once
|
52
|
-
expect(group).to receive(:kill).with(:TERM).once
|
53
|
-
|
54
|
-
expect do
|
55
|
-
group.wait
|
56
|
-
end.to raise_error(Interrupt)
|
57
|
-
|
58
|
-
expect(checkpoint).to be == 'XAY'
|
59
|
-
end
|
27
|
+
Fiber.new do
|
28
|
+
checkpoint += 'X'
|
60
29
|
|
61
|
-
|
62
|
-
group = Process::Group.new
|
63
|
-
checkpoint = ""
|
30
|
+
result = subject.fork { sleep 0.1 }
|
64
31
|
|
65
|
-
|
66
|
-
checkpoint += 'X'
|
67
|
-
|
68
|
-
result = group.fork { sleep 0.1 }
|
69
|
-
expect(result).to be == 0
|
70
|
-
|
71
|
-
checkpoint += 'Y'
|
72
|
-
|
73
|
-
# Raises a RuntimeError
|
74
|
-
fail "Error"
|
75
|
-
end.resume
|
32
|
+
expect(result).to be == 0
|
76
33
|
|
77
|
-
|
78
|
-
checkpoint += 'A'
|
79
|
-
|
80
|
-
# This never returns:
|
81
|
-
result = group.fork { sleep 0.2 }
|
82
|
-
|
83
|
-
checkpoint += 'B'
|
84
|
-
end.resume
|
34
|
+
checkpoint += 'Y'
|
85
35
|
|
86
|
-
|
87
|
-
|
36
|
+
# Simulate the user pressing Ctrl-C after a short time:
|
37
|
+
raise Interrupt
|
38
|
+
end.resume
|
39
|
+
|
40
|
+
Fiber.new do
|
41
|
+
checkpoint += 'A'
|
88
42
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
43
|
+
# This never returns:
|
44
|
+
result = subject.fork do
|
45
|
+
# We do this to exit immediately.. otherwise Ruby will print a backtrace and that's a bit confusing.
|
46
|
+
trap(:INT) { exit!(0) }
|
47
|
+
sleep(0.2)
|
48
|
+
end
|
49
|
+
|
50
|
+
checkpoint += 'B'
|
51
|
+
end.resume
|
97
52
|
|
98
|
-
|
99
|
-
|
100
|
-
checkpoint = ""
|
101
|
-
|
102
|
-
Fiber.new do
|
103
|
-
# Wait for 2 seconds, let other processes run:
|
104
|
-
group.fork { sleep 2 }
|
105
|
-
checkpoint += 'A'
|
106
|
-
#puts "Finished waiting #1..."
|
107
|
-
|
108
|
-
# If no other processes are running, we are done:
|
109
|
-
Fiber.yield unless group.running?
|
110
|
-
checkpoint += 'B'
|
111
|
-
#puts "Sending SIGINT..."
|
53
|
+
expect(subject).to receive(:kill).with(:INT).once.and_call_original
|
54
|
+
expect(subject).to receive(:kill).with(:TERM).once.and_call_original
|
112
55
|
|
113
|
-
|
114
|
-
|
56
|
+
expect do
|
57
|
+
subject.wait
|
58
|
+
end.to raise_error(Interrupt)
|
115
59
|
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
60
|
+
expect(checkpoint).to be == 'XAY'
|
61
|
+
end
|
62
|
+
|
63
|
+
it "should raise an exception" do
|
64
|
+
checkpoint = ""
|
65
|
+
|
66
|
+
Fiber.new do
|
67
|
+
checkpoint += 'X'
|
120
68
|
|
121
|
-
|
122
|
-
|
123
|
-
checkpoint += 'D'
|
124
|
-
#puts "Sending SIGTERM..."
|
69
|
+
result = subject.fork { sleep 0.1 }
|
70
|
+
expect(result).to be == 0
|
125
71
|
|
126
|
-
|
127
|
-
group.kill(:TERM)
|
72
|
+
checkpoint += 'Y'
|
128
73
|
|
129
|
-
|
130
|
-
|
131
|
-
|
74
|
+
# Raises a RuntimeError
|
75
|
+
fail "Error"
|
76
|
+
end.resume
|
132
77
|
|
133
|
-
|
134
|
-
|
78
|
+
Fiber.new do
|
79
|
+
checkpoint += 'A'
|
135
80
|
|
136
|
-
|
81
|
+
# This never returns:
|
82
|
+
result = subject.fork { sleep 0.2 }
|
137
83
|
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
end_time = Time.now
|
84
|
+
checkpoint += 'B'
|
85
|
+
end.resume
|
86
|
+
|
87
|
+
expect do
|
88
|
+
expect(subject).to receive(:kill).with(:TERM).once
|
145
89
|
|
146
|
-
|
147
|
-
|
148
|
-
|
90
|
+
subject.wait
|
91
|
+
end.to raise_error(RuntimeError)
|
92
|
+
|
93
|
+
expect(checkpoint).to be == 'XAY'
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should pass back out exceptions" do
|
97
|
+
checkpoint = ""
|
98
|
+
|
99
|
+
Fiber.new do
|
100
|
+
# Wait for 2 seconds, let other processes run:
|
101
|
+
subject.fork { sleep 2 }
|
102
|
+
checkpoint += 'A'
|
103
|
+
#puts "Finished waiting #1..."
|
104
|
+
|
105
|
+
# If no other processes are running, we are done:
|
106
|
+
Fiber.yield unless subject.running?
|
107
|
+
checkpoint += 'B'
|
108
|
+
#puts "Sending SIGINT..."
|
109
|
+
|
110
|
+
# Send SIGINT to currently running processes:
|
111
|
+
subject.kill(:INT)
|
112
|
+
|
113
|
+
# Wait for 2 seconds, let other processes run:
|
114
|
+
subject.fork { sleep 2 }
|
115
|
+
checkpoint += 'C'
|
116
|
+
#puts "Finished waiting #2..."
|
117
|
+
|
118
|
+
# If no other processes are running, we are done:
|
119
|
+
Fiber.yield unless subject.running?
|
120
|
+
checkpoint += 'D'
|
121
|
+
#puts "Sending SIGTERM..."
|
122
|
+
|
123
|
+
# Send SIGTERM to currently running processes:
|
124
|
+
subject.kill(:TERM)
|
125
|
+
|
126
|
+
# Raise an Timeout exception which is pased back out:
|
127
|
+
raise StandardError.new("Should never get here!")
|
128
|
+
end.resume
|
129
|
+
|
130
|
+
# Run some other long task:
|
131
|
+
subject.run("sleep 10")
|
132
|
+
|
133
|
+
start_time = Time.now
|
134
|
+
|
135
|
+
# Wait for fiber to complete:
|
136
|
+
expect do
|
137
|
+
subject.wait
|
138
|
+
checkpoint += 'E'
|
139
|
+
end.not_to raise_error
|
140
|
+
|
141
|
+
end_time = Time.now
|
142
|
+
|
143
|
+
expect(checkpoint).to be == 'ABCE'
|
144
|
+
expect(end_time - start_time).to be_within(0.2).of 4.0
|
149
145
|
end
|
150
146
|
end
|
@@ -20,37 +20,33 @@
|
|
20
20
|
|
21
21
|
require 'process/group'
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
3.times do
|
33
|
-
output.puts "Hello World"
|
34
|
-
sleep 0.1
|
35
|
-
end
|
36
|
-
|
37
|
-
exit(0)
|
23
|
+
RSpec.describe Process::Group do
|
24
|
+
it "should read line on separate thread" do
|
25
|
+
input, output = IO.pipe
|
26
|
+
|
27
|
+
Fiber.new do
|
28
|
+
result = subject.fork do
|
29
|
+
3.times do
|
30
|
+
output.puts "Hello World"
|
31
|
+
sleep 0.1
|
38
32
|
end
|
39
|
-
|
40
|
-
expect(result).to be == 0
|
41
|
-
end.resume
|
42
|
-
|
43
|
-
output.close
|
44
33
|
|
45
|
-
|
46
|
-
io_thread = Thread.new do
|
47
|
-
lines = input.read
|
34
|
+
exit(0)
|
48
35
|
end
|
49
36
|
|
50
|
-
|
37
|
+
output.close
|
51
38
|
|
52
|
-
|
53
|
-
|
39
|
+
expect(result).to be == 0
|
40
|
+
end.resume
|
41
|
+
|
42
|
+
lines = nil
|
43
|
+
io_thread = Thread.new do
|
44
|
+
lines = input.read
|
54
45
|
end
|
46
|
+
|
47
|
+
subject.wait
|
48
|
+
|
49
|
+
io_thread.join
|
50
|
+
expect(lines).to be == ("Hello World\n" * 3)
|
55
51
|
end
|
56
52
|
end
|
@@ -20,27 +20,23 @@
|
|
20
20
|
|
21
21
|
require 'process/group'
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
expect(group.blocking?).to be_truthy
|
42
|
-
|
43
|
-
group.wait
|
23
|
+
RSpec.describe Process::Group.new(limit: 5) do
|
24
|
+
it "should only run a limited number of processes" do
|
25
|
+
expect(subject.available?).to be_truthy
|
26
|
+
expect(subject.blocking?).to be_falsey
|
27
|
+
|
28
|
+
5.times do
|
29
|
+
Fiber.new do
|
30
|
+
result = subject.fork do
|
31
|
+
exit(0)
|
32
|
+
end
|
33
|
+
|
34
|
+
expect(result.exitstatus).to be == 0
|
35
|
+
end.resume
|
36
|
+
end
|
37
|
+
|
38
|
+
subject.wait do
|
39
|
+
expect(subject.blocking?).to be_truthy
|
44
40
|
end
|
45
41
|
end
|
46
42
|
end
|
@@ -18,40 +18,38 @@
|
|
18
18
|
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
19
|
# THE SOFTWARE.
|
20
20
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
21
|
+
RSpec.describe Process do
|
22
|
+
it "default fork exit status should be 0" do
|
23
|
+
pid = fork do
|
24
|
+
end
|
25
|
+
|
26
|
+
Process.waitpid(pid)
|
27
|
+
|
28
|
+
expect($?.exitstatus).to be == 0
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should fork and return exit status correctly" do
|
32
|
+
pid = fork do
|
33
|
+
exit(1)
|
30
34
|
end
|
31
35
|
|
32
|
-
|
36
|
+
Process.waitpid(pid)
|
37
|
+
|
38
|
+
expect($?.exitstatus).to be == 1
|
39
|
+
end
|
40
|
+
|
41
|
+
# This is currently broken on Rubinius.
|
42
|
+
it "should be okay to use fork within a fiber" do
|
43
|
+
pid = nil
|
44
|
+
|
45
|
+
Fiber.new do
|
33
46
|
pid = fork do
|
34
|
-
exit(
|
47
|
+
exit(2)
|
35
48
|
end
|
36
|
-
|
37
|
-
Process.waitpid(pid)
|
38
|
-
|
39
|
-
expect($?.exitstatus).to be == 1
|
40
|
-
end
|
49
|
+
end.resume
|
41
50
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
Fiber.new do
|
47
|
-
pid = fork do
|
48
|
-
exit(2)
|
49
|
-
end
|
50
|
-
end.resume
|
51
|
-
|
52
|
-
Process.waitpid(pid)
|
53
|
-
|
54
|
-
expect($?.exitstatus).to be == 2
|
55
|
-
end
|
51
|
+
Process.waitpid(pid)
|
52
|
+
|
53
|
+
expect($?.exitstatus).to be == 2
|
56
54
|
end
|
57
|
-
end
|
55
|
+
end
|
@@ -20,90 +20,80 @@
|
|
20
20
|
|
21
21
|
require 'process/group'
|
22
22
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
group = Process::Group.new
|
27
|
-
|
28
|
-
start_time = Time.now
|
29
|
-
|
30
|
-
Fiber.new do
|
31
|
-
result = group.fork { sleep 1.0 }
|
32
|
-
|
33
|
-
expect(result).to be == 0
|
34
|
-
end.resume
|
35
|
-
|
36
|
-
Fiber.new do
|
37
|
-
result = group.fork { sleep 2.0 }
|
38
|
-
|
39
|
-
expect(result).to be == 0
|
40
|
-
end.resume
|
41
|
-
|
42
|
-
group.wait
|
43
|
-
|
44
|
-
end_time = Time.now
|
45
|
-
|
46
|
-
# Check that the execution time was roughly 2 seconds:
|
47
|
-
expect(end_time - start_time).to be_within(0.1).of(2.0)
|
48
|
-
end
|
49
|
-
|
50
|
-
it "should kill commands" do
|
51
|
-
group = Process::Group.new
|
23
|
+
RSpec.describe Process::Group do
|
24
|
+
it "should execute fibers concurrently" do
|
25
|
+
start_time = Time.now
|
52
26
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
group.run("sleep 2") do |exit_status|
|
60
|
-
expect(exit_status).to_not be 0
|
61
|
-
end
|
27
|
+
Fiber.new do
|
28
|
+
result = subject.fork { sleep 1.0 }
|
29
|
+
|
30
|
+
expect(result).to be == 0
|
31
|
+
end.resume
|
62
32
|
|
63
|
-
|
33
|
+
Fiber.new do
|
34
|
+
result = subject.fork { sleep 2.0 }
|
35
|
+
|
36
|
+
expect(result).to be == 0
|
37
|
+
end.resume
|
64
38
|
|
65
|
-
|
39
|
+
subject.wait
|
66
40
|
|
67
|
-
|
41
|
+
end_time = Time.now
|
68
42
|
|
69
|
-
|
70
|
-
|
43
|
+
# Check that the execution time was roughly 2 seconds:
|
44
|
+
expect(end_time - start_time).to be_within(0.1).of(2.0)
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should kill commands" do
|
48
|
+
start_time = Time.now
|
49
|
+
|
50
|
+
subject.run("sleep 1") do |exit_status|
|
51
|
+
expect(exit_status).to_not be 0
|
71
52
|
end
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
env = {'FOO' => 'BAR'}
|
77
|
-
|
78
|
-
# Make a pipe to receive output from child process:
|
79
|
-
input, output = IO.pipe
|
80
|
-
|
81
|
-
group.run(env, "echo $FOO", out: output) do |exit_status|
|
82
|
-
output.close
|
83
|
-
end
|
84
|
-
|
85
|
-
group.wait
|
86
|
-
|
87
|
-
expect(input.read).to be == "BAR\n"
|
53
|
+
|
54
|
+
subject.run("sleep 2") do |exit_status|
|
55
|
+
expect(exit_status).to_not be 0
|
88
56
|
end
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
57
|
+
|
58
|
+
subject.wait do
|
59
|
+
subject.kill(:KILL)
|
60
|
+
end
|
61
|
+
|
62
|
+
end_time = Time.now
|
63
|
+
|
64
|
+
# Check that processes killed almost immediately:
|
65
|
+
expect(end_time - start_time).to be < 0.2
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should pass environment to child process" do
|
69
|
+
env = {'FOO' => 'BAR'}
|
70
|
+
|
71
|
+
# Make a pipe to receive output from child process:
|
72
|
+
input, output = IO.pipe
|
73
|
+
|
74
|
+
subject.run(env, "echo $FOO", out: output) do |exit_status|
|
75
|
+
output.close
|
76
|
+
end
|
77
|
+
|
78
|
+
subject.wait
|
79
|
+
|
80
|
+
expect(input.read).to be == "BAR\n"
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should yield exit status" do
|
84
|
+
start_time = Time.now
|
85
|
+
|
86
|
+
subject.run("sleep 1")
|
87
|
+
|
88
|
+
subject.run("sleep 1") do |exit_status|
|
89
|
+
expect(exit_status).to be == 0
|
107
90
|
end
|
91
|
+
|
92
|
+
subject.wait
|
93
|
+
|
94
|
+
end_time = Time.now
|
95
|
+
|
96
|
+
# Check that the execution time was roughly 1 second:
|
97
|
+
expect(end_time - start_time).to be_within(0.1).of(1.0)
|
108
98
|
end
|
109
99
|
end
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# Copyright, 2012, by Samuel G. D. Williams. <http://www.codeotaku.com>
|
2
|
+
#
|
3
|
+
# Permission is hereby granted, free of charge, to any person obtaining a copy
|
4
|
+
# of this software and associated documentation files (the "Software"), to deal
|
5
|
+
# in the Software without restriction, including without limitation the rights
|
6
|
+
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
7
|
+
# copies of the Software, and to permit persons to whom the Software is
|
8
|
+
# furnished to do so, subject to the following conditions:
|
9
|
+
#
|
10
|
+
# The above copyright notice and this permission notice shall be included in
|
11
|
+
# all copies or substantial portions of the Software.
|
12
|
+
#
|
13
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
14
|
+
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
15
|
+
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
16
|
+
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
17
|
+
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
18
|
+
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
19
|
+
# THE SOFTWARE.
|
20
|
+
|
21
|
+
require 'process/group'
|
22
|
+
|
23
|
+
RSpec.describe Process::Group do
|
24
|
+
it "should invoke child task normally" do
|
25
|
+
start_time = Time.now
|
26
|
+
|
27
|
+
child_exit_status = nil
|
28
|
+
|
29
|
+
subject.wait do
|
30
|
+
subject.run("exit 0") do |exit_status|
|
31
|
+
child_exit_status = exit_status
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
expect(child_exit_status).to be == 0
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should kill child task if process is interrupted" do
|
39
|
+
start_time = Time.now
|
40
|
+
|
41
|
+
child_exit_status = nil
|
42
|
+
|
43
|
+
expect do
|
44
|
+
subject.wait do
|
45
|
+
subject.run("sleep 10") do |exit_status|
|
46
|
+
child_exit_status = exit_status
|
47
|
+
end
|
48
|
+
|
49
|
+
# Simulate the parent (controlling) process receiving an interrupt.
|
50
|
+
raise Interrupt
|
51
|
+
end
|
52
|
+
end.to raise_error(Interrupt)
|
53
|
+
|
54
|
+
expect(child_exit_status).to_not be == 0
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should propagate Interrupt" do
|
58
|
+
expect(Process::Group).to receive(:new).once.and_call_original
|
59
|
+
|
60
|
+
expect do
|
61
|
+
Process::Group.wait do |group|
|
62
|
+
raise Interrupt
|
63
|
+
end
|
64
|
+
end.to raise_error(Interrupt)
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should clear queue after wait" do
|
68
|
+
subject.limit = 1
|
69
|
+
|
70
|
+
subject.run("sleep 10")
|
71
|
+
subject.run("sleep 10")
|
72
|
+
|
73
|
+
expect(subject.running?).to be_falsey
|
74
|
+
expect(subject.queued?).to be_truthy
|
75
|
+
|
76
|
+
expect do
|
77
|
+
subject.wait do
|
78
|
+
raise Interrupt
|
79
|
+
end
|
80
|
+
end.to raise_error(Interrupt)
|
81
|
+
|
82
|
+
expect(subject.running?).to be_falsey
|
83
|
+
expect(subject.queued?).to be_falsey
|
84
|
+
end
|
85
|
+
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: process-group
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-02-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -78,6 +78,7 @@ files:
|
|
78
78
|
- spec/process/group/load_spec.rb
|
79
79
|
- spec/process/group/process_spec.rb
|
80
80
|
- spec/process/group/spawn_spec.rb
|
81
|
+
- spec/process/group/wait_spec.rb
|
81
82
|
homepage: https://github.com/ioquatix/process-group
|
82
83
|
licenses:
|
83
84
|
- MIT
|
@@ -98,7 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
99
|
version: '0'
|
99
100
|
requirements: []
|
100
101
|
rubyforge_project:
|
101
|
-
rubygems_version: 2.
|
102
|
+
rubygems_version: 2.5.1
|
102
103
|
signing_key:
|
103
104
|
specification_version: 4
|
104
105
|
summary: Run processes concurrently in separate fibers with predictable behaviour.
|
@@ -109,3 +110,4 @@ test_files:
|
|
109
110
|
- spec/process/group/load_spec.rb
|
110
111
|
- spec/process/group/process_spec.rb
|
111
112
|
- spec/process/group/spawn_spec.rb
|
113
|
+
- spec/process/group/wait_spec.rb
|