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