exceptional_fork 1.1.0 → 1.2.0
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/Gemfile +1 -6
- data/exceptional_fork.gemspec +7 -7
- data/lib/exceptional_fork.rb +53 -7
- data/spec/exceptional_fork_spec.rb +14 -0
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6ab22c22f6a3441848a352c6dc0ae33b899e9d9c
|
4
|
+
data.tar.gz: 83c5ce7dd6da50a819974df9ced0dbd57a8127e4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8e26f513d002378e533be2cba1e0221fc0d174782351f0b9e7e23921f09f6d183962d9d7aff4a08376f358e6179094d87de14c7cf89ffdec23d2146c967d8892
|
7
|
+
data.tar.gz: d8ae3caa7edc189dd4103729c466bfd704bedb4052355294c2b5acf1c8e3ef3b36599e7f0a94f4c25400834ffceb48b6d8fe7c1d9ef667e691a1801198a497be
|
data/Gemfile
CHANGED
@@ -1,12 +1,7 @@
|
|
1
1
|
source "http://rubygems.org"
|
2
|
-
# Add dependencies required to use your gem here.
|
3
|
-
# Example:
|
4
|
-
# gem "activesupport", ">= 2.3.5"
|
5
2
|
|
6
|
-
# Add dependencies to develop your gem here.
|
7
|
-
# Include everything needed to run rake, tests, features, etc.
|
8
3
|
group :development do
|
9
|
-
gem "rspec", "~> 2
|
4
|
+
gem "rspec", "~> 3.2"
|
10
5
|
gem "rdoc", "~> 3.12"
|
11
6
|
gem "bundler", "~> 1.0"
|
12
7
|
gem "jeweler", "~> 2.0.1"
|
data/exceptional_fork.gemspec
CHANGED
@@ -2,16 +2,16 @@
|
|
2
2
|
# DO NOT EDIT THIS FILE DIRECTLY
|
3
3
|
# Instead, edit Jeweler::Tasks in Rakefile, and run 'rake gemspec'
|
4
4
|
# -*- encoding: utf-8 -*-
|
5
|
-
# stub: exceptional_fork 1.
|
5
|
+
# stub: exceptional_fork 1.2.0 ruby lib
|
6
6
|
|
7
7
|
Gem::Specification.new do |s|
|
8
8
|
s.name = "exceptional_fork"
|
9
|
-
s.version = "1.
|
9
|
+
s.version = "1.2.0"
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.require_paths = ["lib"]
|
13
13
|
s.authors = ["Julik Tarkhanov"]
|
14
|
-
s.date = "
|
14
|
+
s.date = "2016-09-21"
|
15
15
|
s.description = " Uses pipes to re-raise exceptions. Something better than an exit code has to exist. "
|
16
16
|
s.email = "me@julik.nl"
|
17
17
|
s.extra_rdoc_files = [
|
@@ -32,27 +32,27 @@ Gem::Specification.new do |s|
|
|
32
32
|
]
|
33
33
|
s.homepage = "http://github.com/julik/exceptional_fork"
|
34
34
|
s.licenses = ["MIT"]
|
35
|
-
s.rubygems_version = "2.
|
35
|
+
s.rubygems_version = "2.5.1"
|
36
36
|
s.summary = "Raise exceptions from the forked child process in the parent"
|
37
37
|
|
38
38
|
if s.respond_to? :specification_version then
|
39
39
|
s.specification_version = 4
|
40
40
|
|
41
41
|
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
42
|
-
s.add_development_dependency(%q<rspec>, ["~> 2
|
42
|
+
s.add_development_dependency(%q<rspec>, ["~> 3.2"])
|
43
43
|
s.add_development_dependency(%q<rdoc>, ["~> 3.12"])
|
44
44
|
s.add_development_dependency(%q<bundler>, ["~> 1.0"])
|
45
45
|
s.add_development_dependency(%q<jeweler>, ["~> 2.0.1"])
|
46
46
|
s.add_development_dependency(%q<simplecov>, [">= 0"])
|
47
47
|
else
|
48
|
-
s.add_dependency(%q<rspec>, ["~> 2
|
48
|
+
s.add_dependency(%q<rspec>, ["~> 3.2"])
|
49
49
|
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
50
50
|
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
51
51
|
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
52
52
|
s.add_dependency(%q<simplecov>, [">= 0"])
|
53
53
|
end
|
54
54
|
else
|
55
|
-
s.add_dependency(%q<rspec>, ["~> 2
|
55
|
+
s.add_dependency(%q<rspec>, ["~> 3.2"])
|
56
56
|
s.add_dependency(%q<rdoc>, ["~> 3.12"])
|
57
57
|
s.add_dependency(%q<bundler>, ["~> 1.0"])
|
58
58
|
s.add_dependency(%q<jeweler>, ["~> 2.0.1"])
|
data/lib/exceptional_fork.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
module ExceptionalFork
|
2
|
-
VERSION = '1.
|
2
|
+
VERSION = '1.2.0'
|
3
3
|
QUIT = "The child process %d has quit or was killed abruptly. No error information could be retrieved".freeze
|
4
4
|
ProcessHung = Class.new(StandardError)
|
5
|
+
DEFAULT_TIMEOUT = 10
|
6
|
+
DEFAULT_ERROR_STATUS = 99
|
5
7
|
|
6
8
|
# Fork with a block and wait until the forked child exits.
|
7
9
|
# Any exceptions raised within the block will be re-raised from this
|
@@ -18,7 +20,14 @@ module ExceptionalFork
|
|
18
20
|
#
|
19
21
|
# It is not guaranteed that all the exception metadata will be reinstated due to
|
20
22
|
# marshaling/unmarshaling mechanics, but it helps debugging nevertheless.
|
21
|
-
|
23
|
+
#
|
24
|
+
# By default, the child process will be expected to complete within DEFAULT_TIMEOUT seconds
|
25
|
+
# (scientifically chosen by an Arbitraryometer-Of-Random-Assumptions). If you need to adjust
|
26
|
+
# the timeout, pass a different number as the timeout argument. The timeout is going to be
|
27
|
+
# excercised using wait2() with the WNOHANG option, so no threads are going to be used and
|
28
|
+
# the wait is not going to be blocking. ExceptionFork is going to do a `Thread.pass` to let
|
29
|
+
# other threads do work while it waits on the process to complete.
|
30
|
+
def fork_and_wait(kill_after_timeout = DEFAULT_TIMEOUT)
|
22
31
|
# Redirect the exceptions in the child to the pipe. When we get a non-zero
|
23
32
|
# exit code we can read from that pipe to obtain the exception.
|
24
33
|
reader, writer = IO.pipe
|
@@ -26,16 +35,16 @@ module ExceptionalFork
|
|
26
35
|
# Run the block in a forked child
|
27
36
|
pid = fork_with_error_output(writer) { yield }
|
28
37
|
|
29
|
-
# Wait for the forked process to exit
|
30
|
-
|
38
|
+
# Wait for the forked process to exit, in a non-blocking fashion
|
39
|
+
exit_code = wait_and_capture(pid, kill_after_timeout)
|
31
40
|
|
32
41
|
writer.close rescue IOError # Close the writer so that we can read from the reader
|
33
42
|
child_error = reader.read # Read the error output
|
34
43
|
reader.close rescue IOError # Do not leak pipes since the process might be long-lived
|
35
44
|
|
36
|
-
if
|
37
|
-
# If the child gets
|
38
|
-
# gets recovered.
|
45
|
+
if exit_code.nonzero? # If the process exited uncleanly capture the error
|
46
|
+
# If the child gets KILLed then no exception gets written,
|
47
|
+
# and no information gets recovered.
|
39
48
|
raise ProcessHung.new(QUIT % pid) if (child_error.nil? || child_error.empty?)
|
40
49
|
|
41
50
|
unmarshaled_error, backtrace_in_child = Marshal.load(child_error)
|
@@ -66,6 +75,7 @@ module ExceptionalFork
|
|
66
75
|
errors_pipe.puts(Marshal.dump(error_payload))
|
67
76
|
errors_pipe.flush
|
68
77
|
ensure
|
78
|
+
errors_pipe.close rescue nil
|
69
79
|
Process.exit! success # Exit maintaining the status code
|
70
80
|
end
|
71
81
|
end
|
@@ -74,5 +84,41 @@ module ExceptionalFork
|
|
74
84
|
pid
|
75
85
|
end
|
76
86
|
|
87
|
+
# Wait for a process to quit in a non-blocking fashion, using a
|
88
|
+
# preset timeout, and collect (or synthesize) it's exit code value afterwards
|
89
|
+
def wait_and_capture(pid, timeout)
|
90
|
+
started_waiting_at = Time.now
|
91
|
+
status = nil
|
92
|
+
signals = [:TERM, :KILL]
|
93
|
+
loop do
|
94
|
+
# Use wait2 to recover the status without the global variable (we might
|
95
|
+
# be threaded), use WNOHANG so that we do not have to block while waiting
|
96
|
+
# for the process to complete. If we block (without WNOHANG), MRI will still
|
97
|
+
# be able to do other work _but_ we might be waiting indefinitely. If we use
|
98
|
+
# a non-blocking option we can supply a timeout and force-quit the process
|
99
|
+
# without using the Timeout module (and conversely having an overhead of 1
|
100
|
+
# watcher thread per child spawned)
|
101
|
+
if wait_res = Process.wait2(pid, Process::WNOHANG)
|
102
|
+
_, status = wait_res
|
103
|
+
return status.exitstatus || DEFAULT_ERROR_STATUS
|
104
|
+
else
|
105
|
+
# If the process is still busy and didn't quit,
|
106
|
+
# we have to undertake Measures. Send progressively
|
107
|
+
# harsher signals to the child
|
108
|
+
Process.kill(signals.shift, pid) if (Time.now - started_waiting_at) > timeout
|
109
|
+
if signals.empty? # If we exhausted our force-quit powers, do a blocking wait. KILL _will_ work.
|
110
|
+
_, status = Process.wait2(pid)
|
111
|
+
return status.exitstatus || DEFAULT_ERROR_STATUS # For killed processes this will be nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
Thread.pass
|
115
|
+
end
|
116
|
+
rescue Errno::ECHILD, Errno::ESRCH, Errno::EPERM => e# The child already quit
|
117
|
+
# Assume the process finished correctly. If there was an error, we will discover
|
118
|
+
# that from a zero-size output file. There may of course be a thing where the
|
119
|
+
# file gets written incompletely and the child crashes but hey - computers!
|
120
|
+
return 0
|
121
|
+
end
|
122
|
+
|
77
123
|
extend self
|
78
124
|
end
|
@@ -17,6 +17,20 @@ describe "ExceptionalFork" do
|
|
17
17
|
end
|
18
18
|
end
|
19
19
|
|
20
|
+
it 'raises a simple exception upfront' do
|
21
|
+
expect(Process).to receive(:fork).and_call_original
|
22
|
+
expect {
|
23
|
+
ExceptionalFork.fork_and_wait { raise "Explosion! "}
|
24
|
+
}.to raise_error(/Explosion/)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "kills a process that takes too long to terminate" do
|
28
|
+
expect(Process).to receive(:fork).and_call_original
|
29
|
+
expect {
|
30
|
+
ExceptionalFork.fork_and_wait(1) { sleep 5; raise "Should never ever get here" }
|
31
|
+
}.to raise_error(ExceptionalFork::ProcessHung)
|
32
|
+
end
|
33
|
+
|
20
34
|
it "raises a ProcessHung if no exception information can be recovered" do
|
21
35
|
expect(Process).to receive(:fork).and_call_original
|
22
36
|
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: exceptional_fork
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Julik Tarkhanov
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2016-09-21 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -16,14 +16,14 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '2
|
19
|
+
version: '3.2'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '2
|
26
|
+
version: '3.2'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rdoc
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
@@ -119,7 +119,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
119
119
|
version: '0'
|
120
120
|
requirements: []
|
121
121
|
rubyforge_project:
|
122
|
-
rubygems_version: 2.
|
122
|
+
rubygems_version: 2.5.1
|
123
123
|
signing_key:
|
124
124
|
specification_version: 4
|
125
125
|
summary: Raise exceptions from the forked child process in the parent
|