terminator 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
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'
@@ -1,5 +1,6 @@
1
1
  #! /usr/bin/env ruby
2
-
2
+ #:stopdoc:
3
+ require 'rubygems'
3
4
  require 'pathname'
4
5
 
5
6
  $VERBOSE=nil
data/install.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env ruby
2
+ #:stopdoc:
2
3
  require 'rbconfig'
3
4
  require 'find'
4
5
  require 'ftools'
@@ -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.3'
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).to_f } unless Hash === 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 greater than 0" unless seconds > 0
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
- process.puts [pid, seconds, signal].join(' ')
33
- process.flush
151
+ send_terminator(pid, seconds)
34
152
  end
35
153
 
36
- fattr :process do
37
- process = IO.popen "#{ ruby } #{ program.inspect }", 'w+'
38
- at_exit do
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
- fattr :program do
49
- code = <<-code
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
@@ -0,0 +1,12 @@
1
+ require 'terminator'
2
+
3
+ puts "Looping 1000 times on the terminator..."
4
+ success = false
5
+ 1.upto(1000) do |i|
6
+ success = false
7
+ Terminator.terminate(1) do
8
+ success = true
9
+ end
10
+ print "\b\b\b#{i}"
11
+ end
12
+ puts "\nI was successful" if success
@@ -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 handle fractions of seconts" do
24
- failed = false
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
- failed = false
36
- Terminator.terminate(0.01) do
37
- failed = true
27
+ success = false
28
+ Terminator.terminate(10) do
29
+ success = true
38
30
  end
39
- failed.should.be.false
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
- failed = false
44
- Terminator.terminate(1) do
45
- sleep 0.9
46
- failed = true
35
+ success = false
36
+ Terminator.terminate(10) do
37
+ sleep 0.1
38
+ success = true
47
39
  end
48
- failed.should.be.false
40
+ success.should == true
49
41
  end
50
42
 
51
- it "should handle multiple overlapping contracts gracefully" do
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(0.3) do
48
+ Terminator.terminate(10) do
57
49
  first_job = true
58
50
  end
59
51
 
60
- Terminator.terminate(0.3) do
52
+ Terminator.terminate(10) do
61
53
  second_job = true
62
54
  end
63
55
 
64
- Terminator.terminate(0.3) do
56
+ Terminator.terminate(10) do
65
57
  third_job = true
66
58
  end
67
59
 
68
- first_job.should.be.true
69
- second_job.should.be.true
70
- third_job.should.be.true
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(0.3) do
79
- sleep 0.4
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(0.3) do
93
+ Terminator.terminate(10) do
87
94
  second_job = true
88
95
  end
89
96
 
90
- first_job.should.be.false
91
- second_job.should.be.true
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(0.3) do
105
+ Terminator.terminate(10) do
99
106
  first_job = true
100
107
  end
101
108
 
102
109
  begin
103
- Terminator.terminate(0.3) do
104
- sleep 0.4
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.be.true
112
- second_job.should.be.false
118
+ first_job.should == true
119
+ second_job.should == false
113
120
 
114
121
  end
115
122
 
116
- it "should accept an optional trap handler" do
117
- trap = lambda{ 'You failed me again!' }
118
-
119
- doing {
120
- Terminator.terminate(0.001, :trap => trap) do
121
- sleep 0.2
122
- job = true
123
- end }.should.raise(Terminator::Error)
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