balepc-zombie_passenger_killer 0.2.1.patched

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source :rubygems
2
+ gemspec
3
+
4
+ group :development do
5
+ gem 'rake'
6
+ gem 'rspec', '~>2'
7
+ end
data/Gemfile.lock ADDED
@@ -0,0 +1,26 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ zombie_passenger_killer (0.2.1)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.1.3)
10
+ rake (0.9.2)
11
+ rspec (2.6.0)
12
+ rspec-core (~> 2.6.0)
13
+ rspec-expectations (~> 2.6.0)
14
+ rspec-mocks (~> 2.6.0)
15
+ rspec-core (2.6.4)
16
+ rspec-expectations (2.6.0)
17
+ diff-lcs (~> 1.1.2)
18
+ rspec-mocks (2.6.0)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ rake
25
+ rspec (~> 2)
26
+ zombie_passenger_killer!
data/Rakefile ADDED
@@ -0,0 +1,21 @@
1
+ require 'bundler/gem_tasks'
2
+
3
+ task :default do
4
+ sh "rspec spec/"
5
+ end
6
+
7
+ rule /^version:bump:.*/ do |t|
8
+ sh "git status | grep 'nothing to commit'" # ensure we are not dirty
9
+ index = ['major', 'minor','patch'].index(t.name.split(':').last)
10
+ file = 'lib/zombie_passenger_killer/version.rb'
11
+
12
+ version_file = File.read(file)
13
+ old_version, *version_parts = version_file.match(/(\d+)\.(\d+)\.(\d+)/).to_a
14
+ version_parts[index] = version_parts[index].to_i + 1
15
+ version_parts[2] = 0 if index < 2 # remove patch for minor
16
+ version_parts[1] = 0 if index < 1 # remove minor for major
17
+ new_version = version_parts * '.'
18
+ File.open(file,'w'){|f| f.write(version_file.sub(old_version, new_version)) }
19
+
20
+ sh "bundle && git add #{file} Gemfile.lock && git commit -m 'bump version to #{new_version}'"
21
+ end
data/Readme.md ADDED
@@ -0,0 +1,66 @@
1
+ ![Zombies on a train](http://dl.dropbox.com/u/2670385/Web/zombie.jpeg)
2
+
3
+ Guaranteed zombie passengers death.
4
+
5
+ - passenger process no longer listed in passenger-status ? => Death
6
+ - high CPU load over long period (Optional) ? => Death
7
+
8
+ strace of killed zombies is printed, so debugging is easier.
9
+
10
+ (god/bluepill are not suited to monitor passenger apps because of ever-changing pids)
11
+
12
+ Add passenger-status to `/etc/sudoers` or run with sudo.
13
+
14
+ Install
15
+ =======
16
+ sudo gem install zombie_passenger_killer
17
+
18
+ Usage
19
+ =====
20
+
21
+ zombie_passenger_killer [options]
22
+
23
+ Options:
24
+ -m, --max [SIZE] Max high CPU entries in history before killing (default: off)
25
+ --history [SIZE] History size (default: 5)
26
+ -c, --cpu [PERCENT] Mark as high CPU when above PERCENT (default: 70)
27
+ -g, --grace [SECONDS] Wait SECONDS before hard-killing (-9) a process (default: 5)
28
+ -i, --interval [SECONDS] Check every SECONDS (default: 10)
29
+ -p, --pattern [PATTERN] Find processes with this pattern (default: ' Rack: ')
30
+ -r, --rvm-sudo Use `rvmsudo` to see passenger-status
31
+ -h, --help Show this
32
+ -v, --version Show Version
33
+ -t, --time Show time in output
34
+
35
+
36
+ ### Bluepill script
37
+
38
+ app.process("zombie_passenger_killer") do |process|
39
+ process.start_command = "zombie_passenger_killer --max 5 --history 10 --cpu 30 --interval 10"
40
+ process.stdout = process.stderr = "/var/log/autorotate/zombie_passenger_killer.log"
41
+ process.pid_file = "/var/run/zombie_passenger_killer.pid"
42
+ process.daemonize = true
43
+ end
44
+
45
+ ### Monit script
46
+
47
+ check process zombie_killer
48
+ with pidfile "/var/run/zombie_passenger_killer.pid"
49
+ start program = "/bin/bash -c 'export PATH=$PATH:/usr/local/bin HOME=/home;zombie_passenger_killer --max 5 --history 10 --cpu 30 --interval 10 &>/var/log/zombie_passenger_killer.log & &>/dev/null;echo $! > /var/run/zombie_passenger_killer.pid'"
50
+ stop program = "/bin/bash -c 'PIDF=/var/run/zombie_passenger_killer.pid;/bin/kill `cat $PIDF` && rm -f $PIDF'"
51
+ group zombie_killer
52
+
53
+ ### God script
54
+
55
+ # TODO
56
+
57
+ Author
58
+ ======
59
+
60
+ ###Contributors
61
+ - [Roman Heinrich](https://github.com/mindreframer)
62
+ - [Kevin Mullin](https://github.com/kmullin)
63
+
64
+ [Michael Grosser](http://grosser.it)<br/>
65
+ michael@grosser.it<br/>
66
+ Hereby placed under public domain, do what you want, just do not hold me accountable...
@@ -0,0 +1,39 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+
4
+ $LOAD_PATH << File.join(File.dirname(__FILE__), '..', 'lib')
5
+ require 'zombie_passenger_killer'
6
+
7
+ options = {}
8
+ version = ZombiePassengerKiller::VERSION
9
+ OptionParser.new do |opts|
10
+ opts.banner = <<BANNER
11
+ Guaranteed zombie passengers death
12
+
13
+ Usage:
14
+ zombie_passenger_killer [options]
15
+
16
+ Options:
17
+ BANNER
18
+ opts.on("-m", "--max [SIZE]", Integer, "Max high CPU entries in history before killing (default: off)") {|i| options[:max]=i }
19
+ opts.on("--history [SIZE]", Integer, "History size (default: 5)") {|i| options[:history]=i }
20
+ opts.on("-c", "--cpu [PERCENT]", Integer, "Mark as high CPU when above PERCENT (default: 70)") {|i| options[:cpu]=i }
21
+ opts.on("-g", "--grace [SECONDS]", Integer, "Wait SECONDS before hard-killing (-9) a process (default: 5)") {|i| options[:grace]=i }
22
+ opts.on("-i", "--interval [SECONDS]", Integer, "Check every SECONDS (default: 10)") {|i| options[:interval]=i }
23
+ opts.on("-p", "--pattern [PATTERN]", String, "Find processes with this pattern (default: ' Rack: ')") {|i| options[:pattern]=i }
24
+ opts.on("-n", "--no-daemon","Do not start daemon. Check once and exit") {|i| options[:no_daemon]=i }
25
+ opts.on("-r", "--rvm-sudo","Use `rvmsudo` to see passenger-status") {|i| options[:rvm_sudo]=i }
26
+ opts.on("-h", "--help","Show this") { puts opts; exit }
27
+ opts.on("-v", "--version","Show Version"){ puts version; exit }
28
+ opts.on("-t", "--timing","Show timing on output") { options[:show_times]=true }
29
+ end.parse!
30
+
31
+ $stdout.sync = true
32
+ puts "PID: #{$$} Version: #{version} Started at #{Time.now}"
33
+
34
+ $0 = File.basename(__FILE__) + '-' + version
35
+ unless options[:no_daemon].nil?
36
+ ZombiePassengerKiller::Reaper.new(options).hunt_zombies
37
+ else
38
+ ZombiePassengerKiller::Reaper.new(options).lurk
39
+ end
@@ -0,0 +1,91 @@
1
+ module ZombiePassengerKiller
2
+ class Reaper
3
+
4
+ attr_accessor :out # overwriteable for tests
5
+
6
+ def initialize(options)
7
+ @history = {}
8
+ @history_entries = options[:history] || 5
9
+ @max_high_cpu = options[:max]
10
+ @high_cpu = options[:cpu] || 70
11
+ @grace_time = options[:grace] || 5
12
+ @pattern = options[:pattern] || ' Rack: '
13
+ @show_times = options[:show_times] || false
14
+ @interval = options[:interval] || 10
15
+ @strace_time = 5
16
+ @out = STDOUT
17
+ @rvm_sudo = options[:rvm_sudo] || false
18
+ end
19
+
20
+ def lurk
21
+ loop do
22
+ hunt_zombies
23
+ sleep @interval
24
+ end
25
+ rescue Interrupt
26
+ log "Exiting..."
27
+ raise $!
28
+ end
29
+
30
+ def store_current_cpu(processes)
31
+ keys_to_remove = @history.keys - processes.map{|x| x[:pid] }
32
+ keys_to_remove.each{|k| !@history.delete k }
33
+
34
+ processes.each do |process|
35
+ @history[process[:pid]] ||= []
36
+ @history[process[:pid]] << process[:cpu]
37
+ @history[process[:pid]] = @history[process[:pid]].last(@history_entries)
38
+ end
39
+ end
40
+
41
+ def get_strace(pid, time)
42
+ Process.getpgid(pid) rescue return 'No such process'
43
+ `( strace -p #{pid} 2>&1 ) & sleep #{time} ; kill $! 2>&1`
44
+ end
45
+
46
+ def hunt_zombies
47
+ active_pids_in_passenger_status = passenger_pids
48
+ active_processes_in_processlist = process_status
49
+ zombies = active_processes_in_processlist.map{|x| x[:pid] } - active_pids_in_passenger_status rescue Array.new
50
+
51
+ # kill processes with high CPU if user wants it
52
+ high_load = if @max_high_cpu
53
+ store_current_cpu active_processes_in_processlist
54
+ active_pids_in_passenger_status.select do |pid|
55
+ @history[pid].count{|x| x > @high_cpu } >= @max_high_cpu
56
+ end
57
+ else
58
+ []
59
+ end
60
+
61
+ (high_load + zombies).each do |pid|
62
+ kill_zombie pid
63
+ end
64
+ end
65
+
66
+ # return array of pids reported from passenger-status command, nil if passenger-status doesn't run
67
+ def passenger_pids
68
+ pids = %x(#{'rvmsudo ' if @rvm_sudo}passenger-status|grep PID).split("\n").map { |x| x.strip.match(/PID: \d*/).to_s.split[1].to_i }
69
+ pids if $?.exitstatus.zero?
70
+ end
71
+
72
+ def process_status
73
+ %x(ps -eo pid,pcpu,args|grep -v grep|grep '#{@pattern}').split("\n").map do |line|
74
+ values = line.strip.split[0..1]
75
+ {:pid => values.first.to_i, :cpu => values.last.to_f}
76
+ end
77
+ end
78
+
79
+ def kill_zombie(pid)
80
+ log "Killing passenger process #{pid}"
81
+ log get_strace(pid, @strace_time)
82
+ Process.kill('TERM', pid) rescue nil
83
+ sleep @grace_time # wait for it to die
84
+ Process.kill('KILL', pid) rescue nil
85
+ end
86
+
87
+ def log(msg)
88
+ @out.puts "#{@show_times ? "** [#{Time.now.strftime('%Y-%m-%d %H:%M:%S')}] #$$: " : ''}#{msg}"
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,3 @@
1
+ module ZombiePassengerKiller
2
+ Version = VERSION = '0.2.1.patched'
3
+ end
@@ -0,0 +1,4 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.dirname(__FILE__) + '/../lib'))
2
+
3
+ require 'zombie_passenger_killer/version'
4
+ require 'zombie_passenger_killer/reaper'
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift 'lib'
2
+ require 'zombie_passenger_killer'
@@ -0,0 +1,178 @@
1
+ require File.expand_path('spec/spec_helper')
2
+
3
+ describe ZombiePassengerKiller do
4
+ let(:killer){
5
+ ZombiePassengerKiller::Reaper.new(@options || {}).tap do |k|
6
+ k.stub!(:passenger_pids).and_return([111])
7
+ k.out = StringIO.new
8
+ end
9
+ }
10
+
11
+ def output
12
+ killer.out.rewind
13
+ killer.out.read
14
+ end
15
+
16
+ it "has a VERSION" do
17
+ ZombiePassengerKiller::VERSION.should =~ /^\d+\.\d+\.\d+$/
18
+ end
19
+
20
+ describe "#hunt_zombies" do
21
+ it "does not kill anything by default" do
22
+ killer.should_not_receive(:kill_zombie)
23
+ killer.hunt_zombies
24
+ end
25
+
26
+ it "finds the right zombies" do
27
+ killer.stub!(:passenger_pids).and_return([123])
28
+ killer.stub!(:process_status).and_return([{:pid => 124, :cpu => 0}])
29
+ killer.should_receive(:kill_zombie).with(124)
30
+ killer.hunt_zombies
31
+ end
32
+
33
+ it "kills zombies with high cpu over max" do
34
+ @options = {:max => 1}
35
+ killer.stub!(:process_status).and_return([{:pid => 111, :cpu => 100}])
36
+ killer.should_receive(:kill_zombie).with(111)
37
+ killer.hunt_zombies
38
+ end
39
+
40
+ it "does not kills zombies with high cpu under max" do
41
+ @options = {:max => 2}
42
+ killer.stub!(:process_status).and_return([{:pid => 111, :cpu => 100}])
43
+ killer.should_not_receive(:kill_zombie).with(111)
44
+ killer.hunt_zombies
45
+ end
46
+
47
+ it "ignores high cpu levels in old history" do
48
+ @options = {:max => 2, :history => 2}
49
+ killer.should_not_receive(:kill_zombie).with(111)
50
+ killer.stub!(:process_status).and_return([{:pid => 111, :cpu => 100}])
51
+ killer.hunt_zombies
52
+ killer.stub!(:process_status).and_return([{:pid => 111, :cpu => 0}])
53
+ killer.hunt_zombies
54
+ killer.stub!(:process_status).and_return([{:pid => 111, :cpu => 100}])
55
+ killer.hunt_zombies
56
+ end
57
+
58
+ it "kills on high cpu levels in recent history" do
59
+ @options = {:max => 2, :history => 2}
60
+ killer.stub!(:process_status).and_return([{:pid => 111, :cpu => 100}])
61
+ killer.hunt_zombies
62
+ killer.should_receive(:kill_zombie).with(111)
63
+ killer.hunt_zombies
64
+ end
65
+ end
66
+
67
+ describe "#kill_zombies" do
68
+ before do
69
+ killer.instance_eval{
70
+ @grace_time = 0.1
71
+ @strace_time = 0.1
72
+ }
73
+ end
74
+
75
+ def pid_of(marker)
76
+ processes = `ps -ef | grep '#{marker}' | grep -v grep`
77
+ processes.strip.split("\n").last.split(/\s+/)[1].to_i
78
+ end
79
+
80
+ def start_bogus_process(options={})
81
+ marker = "TEST---#{rand(999999999999)}"
82
+ Thread.new do
83
+ `ruby -e 'at_exit{ puts "proper exit"; #{'sleep 10' if options[:hang]}}; sleep 10; puts "#{marker}"' 2>&1`
84
+ end
85
+ sleep 1 # give process time to spin up
86
+ pid_of(marker)
87
+ end
88
+
89
+ def process_alive?(pid)
90
+ Process.getpgid(pid)
91
+ rescue Errno::ESRCH
92
+ false
93
+ end
94
+
95
+ it "kills normal processes" do
96
+ pid = start_bogus_process
97
+ lambda{
98
+ killer.kill_zombie(pid)
99
+ sleep 0.1
100
+ }.should change{ process_alive?(pid) }
101
+ end
102
+
103
+ it "kills hanging processes" do
104
+ pid = start_bogus_process :hang => true
105
+ lambda{
106
+ killer.kill_zombie(pid)
107
+ sleep 0.1
108
+ }.should change{ process_alive?(pid) }
109
+ end
110
+
111
+ it "prints an strace of the process" do
112
+ pid = start_bogus_process
113
+ killer.kill_zombie(pid)
114
+ output.should include('attach:')
115
+ end
116
+
117
+ it "does not take a strace of a dead process" do
118
+ killer.kill_zombie(111)
119
+ output.should_not include('attach:')
120
+ end
121
+
122
+ it "does not fail with an unknown pid" do
123
+ killer.kill_zombie(111)
124
+ output.should include('No such process')
125
+ end
126
+ end
127
+
128
+ describe "#log" do
129
+ it "logs simple when :show_times is not given" do
130
+ killer.log "X"
131
+ output.should == "X\n"
132
+ end
133
+
134
+ it "logs simple when :show_times is not given" do
135
+ @options = {:show_times => true}
136
+ killer.log "X"
137
+ output.should include(Time.now.year.to_s)
138
+ end
139
+ end
140
+
141
+ describe "#lurk" do
142
+ it "sleeps after checking" do
143
+ killer.should_receive(:hunt_zombies)
144
+ killer.should_receive(:sleep).with(10).and_raise "LOOP-BREAKER"
145
+ lambda{
146
+ killer.lurk
147
+ }.should raise_error "LOOP-BREAKER"
148
+ end
149
+
150
+ it "calls sleep with the given interval" do
151
+ @options = {:interval => 5}
152
+ killer.stub(:hunt_zombies)
153
+ killer.should_receive(:sleep).with(5).and_raise "LOOP-BREAKER"
154
+ lambda{
155
+ killer.lurk
156
+ }.should raise_error "LOOP-BREAKER"
157
+ end
158
+
159
+ it "prints Exiting on Interrupt" do
160
+ killer.stub(:hunt_zombies)
161
+ killer.should_receive(:sleep).and_raise Interrupt.new
162
+ lambda{
163
+ killer.lurk
164
+ }.should raise_error Interrupt
165
+ output.should include("Exiting")
166
+ end
167
+ end
168
+
169
+ describe "cli" do
170
+ it "prints its version" do
171
+ `./bin/zombie_passenger_killer -v`.should =~ /^\d+\.\d+\.\d+$/m
172
+ end
173
+
174
+ it "prints help" do
175
+ `./bin/zombie_passenger_killer -h`.should include('Usage')
176
+ end
177
+ end
178
+ end
@@ -0,0 +1,12 @@
1
+ $LOAD_PATH.unshift File.expand_path('../lib', __FILE__)
2
+ require 'zombie_passenger_killer/version'
3
+
4
+ Gem::Specification.new "balepc-zombie_passenger_killer", ZombiePassengerKiller::VERSION do |s|
5
+ s.summary = "Guaranteed zombie passengers death"
6
+ s.authors = ["Michael Grosser"]
7
+ s.email = "michael@grosser.it"
8
+ s.homepage = "http://github.com/grosser/zombie_passenger_killer"
9
+ s.files = `git ls-files`.split("\n")
10
+ s.executables = ["zombie_passenger_killer"]
11
+ s.license = "MIT"
12
+ end
metadata ADDED
@@ -0,0 +1,78 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: balepc-zombie_passenger_killer
3
+ version: !ruby/object:Gem::Version
4
+ hash: -1513943312
5
+ prerelease: 6
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 1
10
+ - patched
11
+ version: 0.2.1.patched
12
+ platform: ruby
13
+ authors:
14
+ - Michael Grosser
15
+ autorequire:
16
+ bindir: bin
17
+ cert_chain: []
18
+
19
+ date: 2012-03-14 00:00:00 Z
20
+ dependencies: []
21
+
22
+ description:
23
+ email: michael@grosser.it
24
+ executables:
25
+ - zombie_passenger_killer
26
+ extensions: []
27
+
28
+ extra_rdoc_files: []
29
+
30
+ files:
31
+ - Gemfile
32
+ - Gemfile.lock
33
+ - Rakefile
34
+ - Readme.md
35
+ - bin/zombie_passenger_killer
36
+ - lib/zombie_passenger_killer.rb
37
+ - lib/zombie_passenger_killer/reaper.rb
38
+ - lib/zombie_passenger_killer/version.rb
39
+ - spec/spec_helper.rb
40
+ - spec/zombie_passenger_killer_spec.rb
41
+ - zombie_passenger_killer.gemspec
42
+ homepage: http://github.com/grosser/zombie_passenger_killer
43
+ licenses:
44
+ - MIT
45
+ post_install_message:
46
+ rdoc_options: []
47
+
48
+ require_paths:
49
+ - lib
50
+ required_ruby_version: !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ hash: 3
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ none: false
61
+ requirements:
62
+ - - ">"
63
+ - !ruby/object:Gem::Version
64
+ hash: 25
65
+ segments:
66
+ - 1
67
+ - 3
68
+ - 1
69
+ version: 1.3.1
70
+ requirements: []
71
+
72
+ rubyforge_project:
73
+ rubygems_version: 1.8.17
74
+ signing_key:
75
+ specification_version: 3
76
+ summary: Guaranteed zombie passengers death
77
+ test_files: []
78
+