terminator 0.4.3 → 0.4.4
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.
- data/README +44 -5
- data/doc/classes/Terminator.html +307 -0
- data/doc/classes/Terminator.src/M000001.html +35 -0
- data/doc/classes/Terminator/Error.html +111 -0
- data/doc/created.rid +1 -0
- data/doc/files/lib/terminator_rb.html +109 -0
- data/doc/files/samples/a_rb.html +108 -0
- data/doc/files/samples/b_rb.html +108 -0
- data/doc/files/samples/c_rb.html +108 -0
- data/doc/files/samples/d_rb.html +108 -0
- data/doc/files/samples/e_rb.html +108 -0
- data/doc/files/spec/terminator_spec_rb.html +122 -0
- data/doc/fr_class_index.html +28 -0
- data/doc/fr_file_index.html +33 -0
- data/doc/fr_method_index.html +27 -0
- data/doc/index.html +24 -0
- data/doc/rdoc-style.css +208 -0
- data/gemspec.rb +2 -1
- data/gen_readme.rb +2 -1
- data/install.rb +1 -0
- data/lib/terminator.rb +132 -42
- data/samples/e.rb +12 -0
- data/spec/terminator_spec.rb +83 -44
- metadata +30 -5
- data/terminator-0.4.2.gem +0 -0
data/gemspec.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#! /usr/bin/env gem build
|
|
2
|
+
#:stopdoc:
|
|
2
3
|
|
|
3
4
|
lib, version = File::basename(File::dirname(File::expand_path(__FILE__))).split %r/-/, 2
|
|
4
5
|
|
|
@@ -24,7 +25,7 @@ Gem::Specification::new do |spec|
|
|
|
24
25
|
|
|
25
26
|
spec.require_path = "lib"
|
|
26
27
|
|
|
27
|
-
spec.has_rdoc = File::exist? "doc"
|
|
28
|
+
spec.has_rdoc = File::exist? "doc"
|
|
28
29
|
spec.test_suite_file = "test/#{ lib }.rb" if File::directory? "test"
|
|
29
30
|
#spec.add_dependency 'lib', '>= version'
|
|
30
31
|
spec.add_dependency 'fattr'
|
data/gen_readme.rb
CHANGED
data/install.rb
CHANGED
data/lib/terminator.rb
CHANGED
|
@@ -1,71 +1,163 @@
|
|
|
1
1
|
require 'rbconfig'
|
|
2
|
-
require 'tempfile'
|
|
3
|
-
|
|
4
2
|
require 'fattr'
|
|
5
3
|
|
|
4
|
+
#=Terminator
|
|
5
|
+
#
|
|
6
|
+
#==Synopsis
|
|
7
|
+
#
|
|
8
|
+
#An external timeout mechanism based on processes and signals. Safe for
|
|
9
|
+
#system calls. Safe for minors. but not very safe for misbehaving,
|
|
10
|
+
#downtrodden zombied out processes.
|
|
11
|
+
#
|
|
12
|
+
#==Description
|
|
13
|
+
#
|
|
14
|
+
#Terminator is a solution to the problem of 'how am I meant to kill a
|
|
15
|
+
#system call in Ruby!?'
|
|
16
|
+
#
|
|
17
|
+
#Ruby (at least MRI) uses green threads to "multitask". This means that
|
|
18
|
+
#there is really only ever one ruby process running which then splits up
|
|
19
|
+
#it's processor time between all of it's threads internally.
|
|
20
|
+
#
|
|
21
|
+
#The processor then only has to deal with one ruby process and the ruby
|
|
22
|
+
#process deals with all it's threads. There are pros and cons to this
|
|
23
|
+
#method, but that is not the point of this library.
|
|
24
|
+
#
|
|
25
|
+
#The point is, that if you make a system call to an external resource from
|
|
26
|
+
#ruby, then the kernel will go and make that call for ruby and NOT COME BACK
|
|
27
|
+
#to ruby until that system call completes or fails. This can take a very
|
|
28
|
+
#long time and is why your feeble attempts at using ruby's internal "Timeout"
|
|
29
|
+
#command has failed miserably at timing out your external web service, database
|
|
30
|
+
#or network connections.
|
|
31
|
+
#
|
|
32
|
+
#You see, Ruby just doesn't get a chance to do anything as the kernel goes
|
|
33
|
+
#"I'm not going to talk to you again until your system calls complete". Sort
|
|
34
|
+
#of a no win situation for Ruby.
|
|
35
|
+
#
|
|
36
|
+
#That's where Terminator comes in. Like Arnie, he will come back. No matter
|
|
37
|
+
#what, and complete his mission, unless he gets aborted before his timeout,
|
|
38
|
+
#you can trust Terminator to thoroughly and without remorse, nuke your
|
|
39
|
+
#misbehaving and timing out ruby processes efficiently, and quickly.
|
|
40
|
+
#
|
|
41
|
+
#==How it Works
|
|
42
|
+
#
|
|
43
|
+
#Basically we create a new terminator ruby process, separate to the existing
|
|
44
|
+
#running ruby process that has a simple command of sleep for x seconds, and then
|
|
45
|
+
#do a process TERM on the PID of the original ruby process that created it.
|
|
46
|
+
#
|
|
47
|
+
#If your process finishes before the timeout, it will kill the Terminator first.
|
|
48
|
+
#
|
|
49
|
+
#So really it is a race of who is going to win?
|
|
50
|
+
#
|
|
51
|
+
#Word of warning though. Terminator is not subtle. Don't expect it to split
|
|
52
|
+
#hairs. Trying to give a process that takes about 1 second to complete, a
|
|
53
|
+
#2 second terminator... well... odds are 50/50 on who is going to make it.
|
|
54
|
+
#
|
|
55
|
+
#If you have a 1 second process, give it 3 seconds to complete. Arnie doesn't
|
|
56
|
+
#much care for casualties of war.
|
|
57
|
+
#
|
|
58
|
+
#Another word of warning, if using Terminator inside a loop, it is possible
|
|
59
|
+
#to exceed your open file limit. I have safely tested looping 1000 times
|
|
60
|
+
#
|
|
61
|
+
#==URIS
|
|
62
|
+
#
|
|
63
|
+
#* http://codeforpeople.com/lib/ruby
|
|
64
|
+
#* http://rubyforge.org/projects/codeforpeople
|
|
65
|
+
#
|
|
66
|
+
#==Usage
|
|
67
|
+
#
|
|
68
|
+
#The terminator library is simple to use.
|
|
69
|
+
#
|
|
70
|
+
# require 'terminator'
|
|
71
|
+
# Terminator.terminate(1) do
|
|
72
|
+
# sleep 4
|
|
73
|
+
# puts("I will never print")
|
|
74
|
+
# end
|
|
75
|
+
# #=> Terminator::Error: Timeout out after 1s
|
|
76
|
+
#
|
|
77
|
+
#The above code snippet will raise a Terminator::Error as the terminator's timeout is
|
|
78
|
+
#2 seconds and the block will take at least 4 to complete.
|
|
79
|
+
#
|
|
80
|
+
#You can put error handling in with a simple begin / rescue block:
|
|
81
|
+
#
|
|
82
|
+
# require 'terminator'
|
|
83
|
+
# begin
|
|
84
|
+
# Terminator.terminate(1) do
|
|
85
|
+
# sleep 4
|
|
86
|
+
# puts("I will never print")
|
|
87
|
+
# end
|
|
88
|
+
# rescue
|
|
89
|
+
# puts("I got terminated, but rescued myself.")
|
|
90
|
+
# end
|
|
91
|
+
# #=> I got terminated, but rescued myself.
|
|
92
|
+
#
|
|
93
|
+
#The standard action on termination is to raise a Terminator::Error, however, this is
|
|
94
|
+
#just an anonymous object that is called, so you can pass your own trap handling by
|
|
95
|
+
#giving the terminator a lambda as an argument.
|
|
96
|
+
#
|
|
97
|
+
# require 'terminator'
|
|
98
|
+
# custom_trap = lambda { eval("raise(RuntimeError, 'Oops... I failed...')") }
|
|
99
|
+
# Terminator.terminate(:seconds => 1, :trap => custom_trap) do
|
|
100
|
+
# sleep 10
|
|
101
|
+
# end
|
|
102
|
+
# #=> RuntimeError: (eval):1:in `irb_binding': Oops... I failed...
|
|
6
103
|
module Terminator
|
|
7
|
-
Version = '0.4.
|
|
8
|
-
|
|
104
|
+
Version = '0.4.4'
|
|
105
|
+
|
|
106
|
+
# Terminator.terminate has two ways you can call it. You can either just specify:
|
|
107
|
+
#
|
|
108
|
+
# Terminator.terminate(seconds) { code_to_execute }
|
|
109
|
+
#
|
|
110
|
+
# where seconds is an integer number greater than or equal to 1. If you pass a float
|
|
111
|
+
# in on seconds, Terminator will call to_i on it and convert it to an integer. This
|
|
112
|
+
# is because Terminator is not a precise tool, due to it calling a new ruby instance,
|
|
113
|
+
# and spawning a new process, relying on split second accuracy is a folly.
|
|
114
|
+
#
|
|
115
|
+
# If you want to pass in the block, please use:
|
|
116
|
+
#
|
|
117
|
+
# Terminator.terminate(:seconds => seconds, :trap => block) { code_to_execute }
|
|
118
|
+
#
|
|
119
|
+
# Where block is an anonymous method that gets called when the timeout occurs.
|
|
9
120
|
def terminate options = {}, &block
|
|
10
|
-
options = { :seconds => Float(options).
|
|
121
|
+
options = { :seconds => Float(options).to_i } unless Hash === options
|
|
11
122
|
|
|
12
123
|
seconds = getopt :seconds, options
|
|
13
124
|
|
|
14
|
-
raise ::Terminator::Error, "Time to kill must be
|
|
125
|
+
raise ::Terminator::Error, "Time to kill must be at least 1 second" unless seconds >= 1
|
|
15
126
|
|
|
16
127
|
trap = getopt :trap, options, lambda{ eval("raise(::Terminator::Error, 'Timeout out after #{ seconds }s')", block) }
|
|
17
128
|
|
|
18
129
|
handler = Signal.trap(signal, &trap)
|
|
19
130
|
|
|
20
|
-
plot_to_kill pid, :in => seconds, :with => signal
|
|
131
|
+
terminator_pid = plot_to_kill pid, :in => seconds, :with => signal
|
|
21
132
|
|
|
22
133
|
begin
|
|
23
134
|
block.call
|
|
135
|
+
nuke_terminator(terminator_pid)
|
|
24
136
|
ensure
|
|
25
137
|
Signal.trap(signal, handler)
|
|
26
138
|
end
|
|
27
139
|
end
|
|
28
140
|
|
|
141
|
+
private
|
|
142
|
+
|
|
143
|
+
def nuke_terminator(pid)
|
|
144
|
+
Process.kill("KILL", pid) rescue nil
|
|
145
|
+
Process.wait(pid)
|
|
146
|
+
end
|
|
147
|
+
|
|
29
148
|
def plot_to_kill pid, options = {}
|
|
30
149
|
seconds = getopt :in, options
|
|
31
150
|
signal = getopt :with, options
|
|
32
|
-
|
|
33
|
-
process.flush
|
|
151
|
+
send_terminator(pid, seconds)
|
|
34
152
|
end
|
|
35
153
|
|
|
36
|
-
|
|
37
|
-
process = IO.popen
|
|
38
|
-
|
|
39
|
-
begin
|
|
40
|
-
Process.kill -9, process.pid
|
|
41
|
-
rescue Object
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
process.sync = true
|
|
45
|
-
process
|
|
154
|
+
def send_terminator(pid, seconds)
|
|
155
|
+
process = IO.popen(%[#{ ruby } -e'sleep #{seconds}.to_i; Process.kill("#{signal}", #{pid}) rescue nil;'], 'w+')
|
|
156
|
+
process.pid
|
|
46
157
|
end
|
|
47
158
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
while(( line = STDIN.gets ))
|
|
51
|
-
pid, seconds, signal, *ignored = line.strip.split
|
|
52
|
-
|
|
53
|
-
pid = Float(pid).to_i
|
|
54
|
-
seconds = Float(seconds)
|
|
55
|
-
signal = Float(signal).to_i rescue String(signal)
|
|
56
|
-
|
|
57
|
-
sleep seconds
|
|
58
|
-
|
|
59
|
-
begin
|
|
60
|
-
Process.kill signal, pid
|
|
61
|
-
rescue Object
|
|
62
|
-
end
|
|
63
|
-
end
|
|
64
|
-
code
|
|
65
|
-
tmp = Tempfile.new "#{ ppid }-#{ pid }-#{ rand }"
|
|
66
|
-
tmp.write code
|
|
67
|
-
tmp.close
|
|
68
|
-
tmp.path
|
|
159
|
+
def temp_file_name
|
|
160
|
+
"terminator-#{ ppid }-#{ pid }-#{ rand }"
|
|
69
161
|
end
|
|
70
162
|
|
|
71
163
|
fattr :ruby do
|
|
@@ -96,8 +188,6 @@ module Terminator
|
|
|
96
188
|
default
|
|
97
189
|
end
|
|
98
190
|
|
|
99
|
-
|
|
100
|
-
|
|
101
191
|
class Error < ::StandardError; end
|
|
102
192
|
def error() ::Terminator::Error end
|
|
103
193
|
def version() ::Terminator::Version end
|
data/samples/e.rb
ADDED
data/spec/terminator_spec.rb
CHANGED
|
@@ -8,10 +8,6 @@ alias :doing :lambda
|
|
|
8
8
|
describe Terminator do
|
|
9
9
|
|
|
10
10
|
describe "being given a contract to terminate" do
|
|
11
|
-
it "should not complain about it" do
|
|
12
|
-
doing { Terminator.terminate(1) { "Hello" } }.should_not raise_error
|
|
13
|
-
end
|
|
14
|
-
|
|
15
11
|
it "should not accept an expired contract" do
|
|
16
12
|
doing { Terminator.terminate(0) { "Hello" } }.should.raise(Terminator::Error)
|
|
17
13
|
end
|
|
@@ -20,54 +16,65 @@ describe Terminator do
|
|
|
20
16
|
doing { Terminator.terminate(-0.1) { "Hello" } }.should.raise(Terminator::Error)
|
|
21
17
|
end
|
|
22
18
|
|
|
23
|
-
it "should
|
|
24
|
-
|
|
25
|
-
Terminator.terminate(0.3) do
|
|
26
|
-
failed = true
|
|
27
|
-
end
|
|
28
|
-
failed.should.be.false
|
|
19
|
+
it "should refuse fractions of seconds less than 1" do
|
|
20
|
+
doing { Terminator.terminate(0.1) { "Hello" } }.should.raise(Terminator::Error)
|
|
29
21
|
end
|
|
30
22
|
|
|
31
23
|
end
|
|
32
24
|
|
|
33
25
|
describe "handling contracts" do
|
|
34
26
|
it "should not kill it's mark if the mark completes" do
|
|
35
|
-
|
|
36
|
-
Terminator.terminate(
|
|
37
|
-
|
|
27
|
+
success = false
|
|
28
|
+
Terminator.terminate(10) do
|
|
29
|
+
success = true
|
|
38
30
|
end
|
|
39
|
-
|
|
31
|
+
success.should == true
|
|
40
32
|
end
|
|
41
33
|
|
|
42
34
|
it "should not terminate it's mark until the time is up" do
|
|
43
|
-
|
|
44
|
-
Terminator.terminate(
|
|
45
|
-
sleep 0.
|
|
46
|
-
|
|
35
|
+
success = false
|
|
36
|
+
Terminator.terminate(10) do
|
|
37
|
+
sleep 0.1
|
|
38
|
+
success = true
|
|
47
39
|
end
|
|
48
|
-
|
|
40
|
+
success.should == true
|
|
49
41
|
end
|
|
50
42
|
|
|
51
|
-
it "should handle multiple
|
|
43
|
+
it "should handle multiple sequential contracts gracefully" do
|
|
52
44
|
first_job = false
|
|
53
45
|
second_job = false
|
|
54
46
|
third_job = false
|
|
55
47
|
|
|
56
|
-
Terminator.terminate(
|
|
48
|
+
Terminator.terminate(10) do
|
|
57
49
|
first_job = true
|
|
58
50
|
end
|
|
59
51
|
|
|
60
|
-
Terminator.terminate(
|
|
52
|
+
Terminator.terminate(10) do
|
|
61
53
|
second_job = true
|
|
62
54
|
end
|
|
63
55
|
|
|
64
|
-
Terminator.terminate(
|
|
56
|
+
Terminator.terminate(10) do
|
|
65
57
|
third_job = true
|
|
66
58
|
end
|
|
67
59
|
|
|
68
|
-
first_job.should
|
|
69
|
-
second_job.should
|
|
70
|
-
third_job.should
|
|
60
|
+
first_job.should == true
|
|
61
|
+
second_job.should == true
|
|
62
|
+
third_job.should == true
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
it "should terminate a process that takes too long" do
|
|
66
|
+
first_job = false
|
|
67
|
+
|
|
68
|
+
begin
|
|
69
|
+
Terminator.terminate(1) do
|
|
70
|
+
sleep 10
|
|
71
|
+
first_job = true
|
|
72
|
+
end
|
|
73
|
+
rescue Terminator::Error
|
|
74
|
+
nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
first_job.should == false
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
it "should be a surgical weapon only selectively destroying it's marks" do
|
|
@@ -75,53 +82,85 @@ describe Terminator do
|
|
|
75
82
|
second_job = false
|
|
76
83
|
|
|
77
84
|
begin
|
|
78
|
-
Terminator.terminate(
|
|
79
|
-
sleep
|
|
85
|
+
Terminator.terminate(1) do
|
|
86
|
+
sleep 10
|
|
80
87
|
first_job = true
|
|
81
88
|
end
|
|
82
89
|
rescue
|
|
83
90
|
nil
|
|
84
91
|
end
|
|
85
92
|
|
|
86
|
-
Terminator.terminate(
|
|
93
|
+
Terminator.terminate(10) do
|
|
87
94
|
second_job = true
|
|
88
95
|
end
|
|
89
96
|
|
|
90
|
-
first_job.should
|
|
91
|
-
second_job.should
|
|
97
|
+
first_job.should == false
|
|
98
|
+
second_job.should == true
|
|
92
99
|
end
|
|
93
100
|
|
|
94
101
|
it "should a surgical weapon only selectively destroying it's marks - backwards" do
|
|
95
102
|
first_job = false
|
|
96
103
|
second_job = false
|
|
97
104
|
|
|
98
|
-
Terminator.terminate(
|
|
105
|
+
Terminator.terminate(10) do
|
|
99
106
|
first_job = true
|
|
100
107
|
end
|
|
101
108
|
|
|
102
109
|
begin
|
|
103
|
-
Terminator.terminate(
|
|
104
|
-
sleep
|
|
110
|
+
Terminator.terminate(1) do
|
|
111
|
+
sleep 10
|
|
105
112
|
second_job = true
|
|
106
113
|
end
|
|
107
114
|
rescue
|
|
108
115
|
nil
|
|
109
116
|
end
|
|
110
117
|
|
|
111
|
-
first_job.should
|
|
112
|
-
second_job.should
|
|
118
|
+
first_job.should == true
|
|
119
|
+
second_job.should == false
|
|
113
120
|
|
|
114
121
|
end
|
|
115
122
|
|
|
116
|
-
it "should
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
Terminator.terminate(
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
123
|
+
it "should handle many many contracts" do
|
|
124
|
+
success = false
|
|
125
|
+
1000.times do
|
|
126
|
+
success = false
|
|
127
|
+
Terminator.terminate(1) do
|
|
128
|
+
success = true
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
success.should == true
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
it "should handle many many contracts with a longer attention span" do
|
|
135
|
+
success = false
|
|
136
|
+
5.times do
|
|
137
|
+
success = false
|
|
138
|
+
Terminator.terminate(5) do
|
|
139
|
+
sleep 1
|
|
140
|
+
success = true
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
success.should == true
|
|
144
|
+
end
|
|
124
145
|
|
|
146
|
+
it "should handle many many contracts with the last one failing" do
|
|
147
|
+
sleep_time = 0
|
|
148
|
+
begin
|
|
149
|
+
5.times do
|
|
150
|
+
Terminator.terminate(2) do
|
|
151
|
+
sleep sleep_time
|
|
152
|
+
sleep_time += 1
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
rescue Terminator::Error
|
|
156
|
+
nil
|
|
157
|
+
end
|
|
158
|
+
sleep_time.should < 4
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
it "should be able to pass in a block for arbitrary execution" do
|
|
162
|
+
new_block = lambda { eval("raise(RuntimeError, 'Oops... I failed...')") }
|
|
163
|
+
doing { Terminator.terminate(:seconds => 1, :trap => new_block) { sleep 10 } }.should.raise(RuntimeError)
|
|
125
164
|
end
|
|
126
165
|
|
|
127
166
|
end
|