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