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