exceptional_fork 1.1.0 → 1.2.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: 1c0f186b58e704eeda7fe96b5fe617927fe9e64e
4
- data.tar.gz: b44013381bb2431a0d984ac5e9e937385cb65985
3
+ metadata.gz: 6ab22c22f6a3441848a352c6dc0ae33b899e9d9c
4
+ data.tar.gz: 83c5ce7dd6da50a819974df9ced0dbd57a8127e4
5
5
  SHA512:
6
- metadata.gz: 1169303635307088336f99794328231245f870013ecf7eb138fe9dd1fb32f197c85e8df1ad1107c1831120a222cfbd8d38cbbd156c3ed4d48c4fdff34c7046e3
7
- data.tar.gz: 15431a14f4ba784cafdb27940709eb03e4280c86c5d06ce01c83e61dae823f7d97a2390114a7b80fe11dd1b31f71a524c477a198e22d12cbe35e248d61f13d48
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.14"
4
+ gem "rspec", "~> 3.2"
10
5
  gem "rdoc", "~> 3.12"
11
6
  gem "bundler", "~> 1.0"
12
7
  gem "jeweler", "~> 2.0.1"
@@ -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.1.0 ruby lib
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.1.0"
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 = "2015-12-15"
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.2.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.14"])
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.14"])
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.14"])
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"])
@@ -1,7 +1,9 @@
1
1
  module ExceptionalFork
2
- VERSION = '1.1.0'
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
- def fork_and_wait
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
- Process.wait(pid)
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 $?.exitstatus != 0 # If the process exited uncleanly capture the error
37
- # If the child gets kill -9d then no exception gets written, and no information
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.1.0
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: 2015-12-15 00:00:00.000000000 Z
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.14'
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.14'
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.2.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