process-group 0.1.0 → 0.1.2
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 +4 -4
- data/README.md +48 -0
- data/lib/process/group.rb +45 -10
- data/lib/process/group/version.rb +1 -1
- data/process-group.gemspec +1 -0
- data/test/test_interrupt.rb +151 -0
- data/test/test_spawn.rb +3 -3
- metadata +18 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: bd660d2e26e8b418c6e8a05b6bcc30b9d85b7eed
|
4
|
+
data.tar.gz: fbb0e6d195952d1911d248feac57d6088744fdd2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bea0fb36a908dc45663e69f7606e934260c91f6665f056f82c1f56a31b7cd76aef4b4a645e2610c06053f548abd66e2abd2e7c1b2cbf76837787ea21cd373907
|
7
|
+
data.tar.gz: 021ca23fe4e15239b7da0715a541e56cf0c5bb2f0ea86657f96bdaca24bc9238e83d6235dd2d2013713c54c4074b881697ea8e6cca70b64db73d79cb268afc92
|
data/README.md
CHANGED
@@ -37,6 +37,8 @@ The simplest concurrent usage is as follows:
|
|
37
37
|
# Wait for all processes in group to finish:
|
38
38
|
group.wait
|
39
39
|
|
40
|
+
The `group.wait` call is an explicit synchronisation 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.
|
41
|
+
|
40
42
|
### Explicit Fibers
|
41
43
|
|
42
44
|
Items within a single fiber will execute sequentially. Processes (e.g. via `Group#spawn`) will run concurrently in multiple fibers.
|
@@ -97,6 +99,52 @@ It is possible to send a signal (kill) to the entire process group:
|
|
97
99
|
|
98
100
|
If there are no running processes, this is a no-op (rather than an error).
|
99
101
|
|
102
|
+
#### Handling Interrupts
|
103
|
+
|
104
|
+
`Process::Graph` transparently handles `Interrupt` when raised within a `Fiber`. If `Interrupt` is raised, all children processes will be sent `kill(:INT)` and we will wait for all children to complete, but without resuming the controlling fibers. If `Interrupt` is raised during this process, children will be sent `kill(:TERM)`. After calling `Interrupt`, the fibers will not be resumed.
|
105
|
+
|
106
|
+
### Process Timeout
|
107
|
+
|
108
|
+
You can run a process group with a time limit by using a separate child process:
|
109
|
+
|
110
|
+
group = Process::Group.new
|
111
|
+
|
112
|
+
class Timeout < StandardError
|
113
|
+
end
|
114
|
+
|
115
|
+
Fiber.new do
|
116
|
+
# Wait for 2 seconds, let other processes run:
|
117
|
+
group.fork { sleep 2 }
|
118
|
+
|
119
|
+
# If no other processes are running, we are done:
|
120
|
+
Fiber.yield unless group.running?
|
121
|
+
|
122
|
+
# Send SIGINT to currently running processes:
|
123
|
+
group.kill(:INT)
|
124
|
+
|
125
|
+
# Wait for 2 seconds, let other processes run:
|
126
|
+
group.fork { sleep 2 }
|
127
|
+
|
128
|
+
# If no other processes are running, we are done:
|
129
|
+
Fiber.yield unless group.running?
|
130
|
+
|
131
|
+
# Send SIGTERM to currently running processes:
|
132
|
+
group.kill(:TERM)
|
133
|
+
|
134
|
+
# Raise an Timeout exception which is based back out:
|
135
|
+
raise Timeout
|
136
|
+
end.resume
|
137
|
+
|
138
|
+
# Run some other long task:
|
139
|
+
group.run("sleep 10")
|
140
|
+
|
141
|
+
# Wait for fiber to complete:
|
142
|
+
begin
|
143
|
+
group.wait
|
144
|
+
rescue Timeout
|
145
|
+
puts "Process group was terminated forcefully."
|
146
|
+
end
|
147
|
+
|
100
148
|
## Contributing
|
101
149
|
|
102
150
|
1. Fork it
|
data/lib/process/group.rb
CHANGED
@@ -96,6 +96,10 @@ module Process
|
|
96
96
|
-@pgid
|
97
97
|
end
|
98
98
|
|
99
|
+
def running?
|
100
|
+
@running.size > 0
|
101
|
+
end
|
102
|
+
|
99
103
|
# Run a process, arguments have same meaning as Process#spawn.
|
100
104
|
def run(*arguments)
|
101
105
|
Fiber.new do
|
@@ -132,26 +136,35 @@ module Process
|
|
132
136
|
|
133
137
|
# Wait for all processes to finish, naturally would schedule any fibers which are currently blocked.
|
134
138
|
def wait
|
135
|
-
while
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
process = @running.delete(pid)
|
140
|
-
|
141
|
-
raise RuntimeError.new("Process id=#{pid} is not part of group!") unless process
|
142
|
-
|
139
|
+
while running?
|
140
|
+
process, status = wait_one
|
141
|
+
|
143
142
|
schedule!
|
144
|
-
|
143
|
+
|
145
144
|
process.resume(status)
|
146
145
|
end
|
147
146
|
|
148
147
|
# No processes, process group is no longer valid:
|
149
148
|
@pgid = nil
|
149
|
+
rescue Interrupt
|
150
|
+
# If the user interrupts the wait, interrupt the process group and wait for them to finish:
|
151
|
+
self.kill(:INT)
|
152
|
+
|
153
|
+
# If user presses Ctrl-C again (or something else goes wrong), we will come out and kill(:TERM) in the ensure below:
|
154
|
+
wait_all
|
155
|
+
|
156
|
+
raise
|
157
|
+
ensure
|
158
|
+
# You'd only get here with running processes if some unexpected error was thrown:
|
159
|
+
self.kill(:TERM)
|
160
|
+
|
161
|
+
# Clean up zombie processes - if user presses Ctrl-C or for some reason something else blows up, exception would propagate back to caller:
|
162
|
+
wait_all
|
150
163
|
end
|
151
164
|
|
152
165
|
# Send a signal to all processes.
|
153
166
|
def kill(signal)
|
154
|
-
if
|
167
|
+
if running?
|
155
168
|
Process.kill(signal, id)
|
156
169
|
end
|
157
170
|
end
|
@@ -183,5 +196,27 @@ module Process
|
|
183
196
|
@running[pid] = process
|
184
197
|
end
|
185
198
|
end
|
199
|
+
|
200
|
+
# Wait for all children to exit but without resuming any controlling fibers.
|
201
|
+
def wait_all
|
202
|
+
wait_one while running?
|
203
|
+
end
|
204
|
+
|
205
|
+
# Wait for one process, should only be called when a child process has finished, otherwise would block.
|
206
|
+
def wait_one(flags = 0)
|
207
|
+
raise RuntimeError.new("Process group has no running children!") unless running?
|
208
|
+
|
209
|
+
# Wait for processes in this group:
|
210
|
+
pid, status = Process.wait2(-@pgid, flags)
|
211
|
+
|
212
|
+
return if flags & Process::WNOHANG and pid == nil
|
213
|
+
|
214
|
+
process = @running.delete(pid)
|
215
|
+
|
216
|
+
# This should never happen unless something very odd has happened:
|
217
|
+
raise RuntimeError.new("Process id=#{pid} is not part of group!") unless process
|
218
|
+
|
219
|
+
return process, status
|
220
|
+
end
|
186
221
|
end
|
187
222
|
end
|
data/process-group.gemspec
CHANGED
@@ -0,0 +1,151 @@
|
|
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 'test/unit'
|
22
|
+
require 'mocha/test_unit'
|
23
|
+
|
24
|
+
require 'process/group'
|
25
|
+
|
26
|
+
class TestInterrupt < Test::Unit::TestCase
|
27
|
+
def test_raise_interrupt
|
28
|
+
group = Process::Group.new
|
29
|
+
checkpoint = ""
|
30
|
+
|
31
|
+
Fiber.new do
|
32
|
+
checkpoint += 'X'
|
33
|
+
|
34
|
+
result = group.fork { sleep 0.1 }
|
35
|
+
assert_equal 0, result
|
36
|
+
|
37
|
+
checkpoint += 'Y'
|
38
|
+
|
39
|
+
# Simulate the user pressing Ctrl-C after 0.5 seconds:
|
40
|
+
raise Interrupt
|
41
|
+
end.resume
|
42
|
+
|
43
|
+
Fiber.new do
|
44
|
+
checkpoint += 'A'
|
45
|
+
|
46
|
+
# This never returns:
|
47
|
+
result = group.fork { sleep 0.2 }
|
48
|
+
|
49
|
+
checkpoint += 'B'
|
50
|
+
end.resume
|
51
|
+
|
52
|
+
group.expects(:kill).with(:INT).once
|
53
|
+
group.expects(:kill).with(:TERM).once
|
54
|
+
|
55
|
+
assert_raises Interrupt do
|
56
|
+
group.wait
|
57
|
+
end
|
58
|
+
|
59
|
+
assert_equal 'XAY', checkpoint
|
60
|
+
end
|
61
|
+
|
62
|
+
def test_raise_exception
|
63
|
+
group = Process::Group.new
|
64
|
+
checkpoint = ""
|
65
|
+
|
66
|
+
Fiber.new do
|
67
|
+
checkpoint += 'X'
|
68
|
+
|
69
|
+
result = group.fork { sleep 0.1 }
|
70
|
+
assert_equal 0, result
|
71
|
+
|
72
|
+
checkpoint += 'Y'
|
73
|
+
|
74
|
+
# Raises a RuntimeError
|
75
|
+
fail "Error"
|
76
|
+
end.resume
|
77
|
+
|
78
|
+
Fiber.new do
|
79
|
+
checkpoint += 'A'
|
80
|
+
|
81
|
+
# This never returns:
|
82
|
+
result = group.fork { sleep 0.2 }
|
83
|
+
|
84
|
+
checkpoint += 'B'
|
85
|
+
end.resume
|
86
|
+
|
87
|
+
assert_raises RuntimeError do
|
88
|
+
group.expects(:kill).with(:TERM).once
|
89
|
+
|
90
|
+
group.wait
|
91
|
+
end
|
92
|
+
|
93
|
+
assert_equal 'XAY', checkpoint
|
94
|
+
end
|
95
|
+
|
96
|
+
class Timeout < StandardError
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_timeout
|
100
|
+
group = Process::Group.new
|
101
|
+
checkpoint = ""
|
102
|
+
|
103
|
+
Fiber.new do
|
104
|
+
# Wait for 2 seconds, let other processes run:
|
105
|
+
group.fork { sleep 2 }
|
106
|
+
checkpoint += 'A'
|
107
|
+
#puts "Finished waiting #1..."
|
108
|
+
|
109
|
+
# If no other processes are running, we are done:
|
110
|
+
Fiber.yield unless group.running?
|
111
|
+
checkpoint += 'B'
|
112
|
+
#puts "Sending SIGINT..."
|
113
|
+
|
114
|
+
# Send SIGINT to currently running processes:
|
115
|
+
group.kill(:INT)
|
116
|
+
|
117
|
+
# Wait for 2 seconds, let other processes run:
|
118
|
+
group.fork { sleep 2 }
|
119
|
+
checkpoint += 'C'
|
120
|
+
#puts "Finished waiting #2..."
|
121
|
+
|
122
|
+
# If no other processes are running, we are done:
|
123
|
+
Fiber.yield unless group.running?
|
124
|
+
checkpoint += 'D'
|
125
|
+
#puts "Sending SIGTERM..."
|
126
|
+
|
127
|
+
# Send SIGTERM to currently running processes:
|
128
|
+
group.kill(:TERM)
|
129
|
+
|
130
|
+
# Raise an Timeout exception which is based back out:
|
131
|
+
raise Timeout
|
132
|
+
end.resume
|
133
|
+
|
134
|
+
# Run some other long task:
|
135
|
+
group.run("sleep 10")
|
136
|
+
|
137
|
+
start_time = Time.now
|
138
|
+
|
139
|
+
# Wait for fiber to complete:
|
140
|
+
assert_nothing_raised Timeout do
|
141
|
+
group.wait
|
142
|
+
checkpoint += 'E'
|
143
|
+
end
|
144
|
+
|
145
|
+
end_time = Time.now
|
146
|
+
|
147
|
+
assert_equal 'ABCE', checkpoint
|
148
|
+
|
149
|
+
assert (3.9..4.1).include?(end_time - start_time)
|
150
|
+
end
|
151
|
+
end
|
data/test/test_spawn.rb
CHANGED
@@ -29,12 +29,12 @@ class TestSpawn < Test::Unit::TestCase
|
|
29
29
|
start_time = Time.now
|
30
30
|
|
31
31
|
Fiber.new do
|
32
|
-
result = group.
|
32
|
+
result = group.fork { sleep 1.0 }
|
33
33
|
assert_equal 0, result
|
34
34
|
end.resume
|
35
35
|
|
36
36
|
Fiber.new do
|
37
|
-
result = group.
|
37
|
+
result = group.fork { sleep 2.0 }
|
38
38
|
|
39
39
|
assert_equal 0, result
|
40
40
|
end.resume
|
@@ -66,7 +66,7 @@ class TestSpawn < Test::Unit::TestCase
|
|
66
66
|
|
67
67
|
end_time = Time.now
|
68
68
|
|
69
|
-
# Check that
|
69
|
+
# Check that processes killed almost immediately:
|
70
70
|
assert (end_time - start_time) < 0.1
|
71
71
|
end
|
72
72
|
|
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: 0.1.
|
4
|
+
version: 0.1.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Samuel Williams
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-03-
|
11
|
+
date: 2014-03-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -38,6 +38,20 @@ dependencies:
|
|
38
38
|
- - '>='
|
39
39
|
- !ruby/object:Gem::Version
|
40
40
|
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: mocha
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - '>='
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - '>='
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
41
55
|
description: "\tManages a unix process group for running multiple processes, keeps
|
42
56
|
track of multiple processes and leverages fibers to provide predictable behaviour
|
43
57
|
in complicated process-based scripts.\n"
|
@@ -57,6 +71,7 @@ files:
|
|
57
71
|
- lib/process/group/version.rb
|
58
72
|
- process-group.gemspec
|
59
73
|
- test/test_fork.rb
|
74
|
+
- test/test_interrupt.rb
|
60
75
|
- test/test_spawn.rb
|
61
76
|
homepage: ''
|
62
77
|
licenses:
|
@@ -84,4 +99,5 @@ specification_version: 4
|
|
84
99
|
summary: Run processes concurrently in separate fibers with predictable behaviour.
|
85
100
|
test_files:
|
86
101
|
- test/test_fork.rb
|
102
|
+
- test/test_interrupt.rb
|
87
103
|
- test/test_spawn.rb
|