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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 28a68aa53cd296412dd77cdca5a9c8b000269439
4
- data.tar.gz: 49777327e43bbddda6e17a2b13776227693d115d
3
+ metadata.gz: 98b4baed52fa9d14e546993096a2e68ca5e2550e
4
+ data.tar.gz: 630111d7484ecdc7e850c48afc3c437767f3695b
5
5
  SHA512:
6
- metadata.gz: 508765b5658d4fbb032c7f43f36a5a9c26e4fbbfc379295398d3ac08e9da12bab9440cb9002bd052d9eaaba5abbbd9ee970f8a290ceaab144ad682e53e95ebfb
7
- data.tar.gz: 0f7d655bd81259b92c757342ff71aee743209e33dbb3c1a71e4b4facd0c4dac5de706a01735c4afd964feb6b52429e8d6b07e706db992408a7da81f599b5a4e9
6
+ metadata.gz: 57f6dcf3d08a2b85315633c630cca1ace63bfcf678db80882125c1c72773e4377e85503677a58f6d0dfd7917404d3e9085da8de7f09d4c250838f33192fcee2a
7
+ data.tar.gz: 29f4993f4ded486db893cad0855a6c4af59d4f89ded8f638d99459d4188917c4b9156135f97b98c968e147cb8d31bfb5c7ec1026e37e505a825a78db115fd508
@@ -9,6 +9,8 @@ rvm:
9
9
  - rbx-2
10
10
  env: COVERAGE=true
11
11
  matrix:
12
+ fast_finish: true
12
13
  allow_failures:
14
+ - rvm: "ruby-head"
13
15
  - rvm: "rbx-2"
14
16
 
data/README.md CHANGED
@@ -12,86 +12,105 @@
12
12
 
13
13
  Add this line to your application's Gemfile:
14
14
 
15
- gem 'process-group'
15
+ gem 'process-group'
16
16
 
17
17
  And then execute:
18
18
 
19
- $ bundle
19
+ $ bundle
20
20
 
21
21
  Or install it yourself as:
22
22
 
23
- $ gem install process-group
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
- group = Process::Group.new
31
-
32
- # Run the command (non-blocking):
33
- group.run("sleep 1") do |exit_status|
34
- # Running in a separate fiber, will execute this code once the process completes:
35
- puts "Command finished with status: #{exit_status}"
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 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.
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
- group = Process::Group.new
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
- # Explicity manage concurrency in this fiber:
53
- Fiber.new do
54
- # These processes will be run sequentially:
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
- ### Specify Options
63
+ ### Explicit Wait
68
64
 
69
- You can specify options to `Group#run` and `Group#spawn` just like `Process::spawn`:
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
- env = {'FOO' => 'BAR'}
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
- # Arguments are essentially the same as Process::spawn.
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. A number of options exist.
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
 
@@ -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
- while running?
149
- process, status = wait_one
150
-
151
- schedule!
161
+ waiting do
162
+ yield(self) if block_given?
152
163
 
153
- process.resume(status)
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,6 +20,6 @@
20
20
 
21
21
  module Process
22
22
  class Group
23
- VERSION = "1.0.1"
23
+ VERSION = "1.1.0"
24
24
  end
25
25
  end
@@ -20,46 +20,41 @@
20
20
 
21
21
  require 'process/group'
22
22
 
23
- module Process::Group::ForkSpec
24
- describe Process::Group do
25
- it "should fork and write to pipe" do
26
- group = Process::Group.new
27
-
28
- input, output = IO.pipe
29
-
30
- Fiber.new do
31
- result = group.fork do
32
- output.puts "Hello World"
33
-
34
- exit(1)
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
- expect(result.exitstatus).not_to be == 0
59
- end.resume
37
+ expect(result.exitstatus).to be == 1
38
+ end.resume
60
39
 
61
- # Shouldn't raise any errors:
62
- group.wait
63
- end
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
- module Process::Group::InterruptSpec
24
- describe Process::Group do
25
- it "should raise interrupt exception" do
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
- Fiber.new do
30
- checkpoint += 'X'
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
- it "should raise an exception" do
62
- group = Process::Group.new
63
- checkpoint = ""
30
+ result = subject.fork { sleep 0.1 }
64
31
 
65
- Fiber.new do
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
- Fiber.new do
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
- expect do
87
- expect(group).to receive(:kill).with(:TERM).once
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
- group.wait
90
- end.to raise_error(RuntimeError)
91
-
92
- expect(checkpoint).to be == 'XAY'
93
- end
94
-
95
- class Timeout < StandardError
96
- end
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
- it "should pass back out exceptions" do
99
- group = Process::Group.new
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
- # Send SIGINT to currently running processes:
114
- group.kill(:INT)
56
+ expect do
57
+ subject.wait
58
+ end.to raise_error(Interrupt)
115
59
 
116
- # Wait for 2 seconds, let other processes run:
117
- group.fork { sleep 2 }
118
- checkpoint += 'C'
119
- #puts "Finished waiting #2..."
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
- # If no other processes are running, we are done:
122
- Fiber.yield unless group.running?
123
- checkpoint += 'D'
124
- #puts "Sending SIGTERM..."
69
+ result = subject.fork { sleep 0.1 }
70
+ expect(result).to be == 0
125
71
 
126
- # Send SIGTERM to currently running processes:
127
- group.kill(:TERM)
72
+ checkpoint += 'Y'
128
73
 
129
- # Raise an Timeout exception which is pased back out:
130
- raise Timeout
131
- end.resume
74
+ # Raises a RuntimeError
75
+ fail "Error"
76
+ end.resume
132
77
 
133
- # Run some other long task:
134
- group.run("sleep 10")
78
+ Fiber.new do
79
+ checkpoint += 'A'
135
80
 
136
- start_time = Time.now
81
+ # This never returns:
82
+ result = subject.fork { sleep 0.2 }
137
83
 
138
- # Wait for fiber to complete:
139
- expect do
140
- group.wait
141
- checkpoint += 'E'
142
- end.not_to raise_error
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
- expect(checkpoint).to be == 'ABCE'
147
- expect(end_time - start_time).to be_within(0.2).of 4.0
148
- end
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
- module Process::Group::IOSpec
24
- describe Process::Group do
25
- it "should read line on separate thread" do
26
- group = Process::Group.new
27
-
28
- input, output = IO.pipe
29
-
30
- Fiber.new do
31
- result = group.fork do
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
- lines = nil
46
- io_thread = Thread.new do
47
- lines = input.read
34
+ exit(0)
48
35
  end
49
36
 
50
- group.wait
37
+ output.close
51
38
 
52
- io_thread.join
53
- expect(lines).to be == ("Hello World\n" * 3)
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
- module Process::Group::LoadSpec
24
- describe Process::Group do
25
- it "should only run a limited number of processes" do
26
- group = Process::Group.new(limit: 5)
27
-
28
- expect(group.available?).to be_truthy
29
- expect(group.blocking?).to be_falsey
30
-
31
- 5.times do
32
- Fiber.new do
33
- result = group.fork do
34
- exit(0)
35
- end
36
-
37
- expect(result.exitstatus).to be == 0
38
- end.resume
39
- end
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
- module Process::Group::ProcessSpec
22
- describe Process do
23
- it "default fork exit status should be 0" do
24
- pid = fork do
25
- end
26
-
27
- Process.waitpid(pid)
28
-
29
- expect($?.exitstatus).to be == 0
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
- it "should fork and return exit status correctly" do
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(1)
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
- # This is currently broken on Rubinius.
43
- it "should be okay to use fork within a fiber" do
44
- pid = nil
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
- module Process::Group::SpawnSpec
24
- describe Process::Group do
25
- it "should execute fibers concurrently" do
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
- start_time = Time.now
54
-
55
- group.run("sleep 1") do |exit_status|
56
- expect(exit_status).to_not be 0
57
- end
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
- group.kill(:KILL)
33
+ Fiber.new do
34
+ result = subject.fork { sleep 2.0 }
35
+
36
+ expect(result).to be == 0
37
+ end.resume
64
38
 
65
- group.wait
39
+ subject.wait
66
40
 
67
- end_time = Time.now
41
+ end_time = Time.now
68
42
 
69
- # Check that processes killed almost immediately:
70
- expect(end_time - start_time).to be < 0.2
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
- it "should pass environment to child process" do
74
- group = Process::Group.new
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
- it "should yield exit status" do
91
- group = Process::Group.new
92
-
93
- start_time = Time.now
94
-
95
- group.run("sleep 1")
96
-
97
- group.run("sleep 1") do |exit_status|
98
- expect(exit_status).to be == 0
99
- end
100
-
101
- group.wait
102
-
103
- end_time = Time.now
104
-
105
- # Check that the execution time was roughly 1 second:
106
- expect(end_time - start_time).to be_within(0.1).of(1.0)
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.1
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-01-09 00:00:00.000000000 Z
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.4.6
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