balepc-zombie_passenger_killer 0.2.1.patched

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