process-group 0.1.0 → 0.1.2

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: 026f8b1f40e77c491180b9faff360d884a6717fa
4
- data.tar.gz: c4ecc9c7c932b7c662f420f3c82ece3ea0a6d7b1
3
+ metadata.gz: bd660d2e26e8b418c6e8a05b6bcc30b9d85b7eed
4
+ data.tar.gz: fbb0e6d195952d1911d248feac57d6088744fdd2
5
5
  SHA512:
6
- metadata.gz: e364e21669e91dec753aff10d6100584c0d80057a52a4db539fcc3fb57df1b2a58b1d69220f66d9c9e8ce137383b5b7f9b62d780e51b292d295e0f5e38c283c0
7
- data.tar.gz: c723127bf2fc53e8d9752f016e858ea1417f053548d687f4732866bfc29275fc80b0680f5609aee396c99fc0c2c8ad996d6396afa3d6b9a55ca6d7cf96731d5d
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
@@ -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 @running.size > 0
136
- # Wait for processes in this group:
137
- pid, status = Process.wait2(-@pgid)
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 @running.size > 0
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
@@ -1,5 +1,5 @@
1
1
  module Process
2
2
  class Group
3
- VERSION = "0.1.0"
3
+ VERSION = "0.1.2"
4
4
  end
5
5
  end
@@ -22,4 +22,5 @@ Gem::Specification.new do |spec|
22
22
 
23
23
  spec.add_development_dependency "bundler", "~> 1.3"
24
24
  spec.add_development_dependency "rake"
25
+ spec.add_development_dependency "mocha"
25
26
  end
@@ -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
@@ -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.spawn("sleep 1")
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.spawn("sleep 2")
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 the execution time was roughly 2 seconds:
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.0
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-13 00:00:00.000000000 Z
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