snipr 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +14 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +44 -0
- data/Rakefile +2 -0
- data/bin/reap_resque_workers +99 -0
- data/lib/snipr.rb +24 -0
- data/lib/snipr/output.rb +27 -0
- data/lib/snipr/process_locator.rb +134 -0
- data/lib/snipr/process_signaller.rb +113 -0
- data/lib/snipr/version.rb +3 -0
- data/snipr.gemspec +29 -0
- data/spec/process_locator_spec.rb +123 -0
- data/spec/process_signaller_spec.rb +100 -0
- data/spec/ps_output.txt +53 -0
- data/spec/spec_helper.rb +2 -0
- metadata +114 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA512:
|
3
|
+
metadata.gz: 0431e21ba622f828ce67bbef64c3ea2bd900e9dac1e6069dbeb88feb0389c6837f9dd1a5654918dd3a2059b69ce52c836927522ea5f1a6c4aa5bec1f74d51bd3
|
4
|
+
data.tar.gz: f2f45b0e24e2576610dd7883ac64b9828dd2ef6586320399d77832b54a597183a297e11e18a252a93130179c86cb11da4d7c610b9d4b90d5acb6a86afb5421ea
|
5
|
+
SHA1:
|
6
|
+
metadata.gz: e4ba65cbad1a2cd5acb11c37237b5f843328a176
|
7
|
+
data.tar.gz: 9ab5063ba228c17e50ab64634c58efea2fc6aee4
|
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
snipr
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-1.8.7-p374
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2014 Lance Woodson
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
# Snipr
|
2
|
+
|
3
|
+
Tool to manage runaway processes. More to be written later.
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'snipr'
|
11
|
+
```
|
12
|
+
|
13
|
+
And then execute:
|
14
|
+
|
15
|
+
$ bundle
|
16
|
+
|
17
|
+
Or install it yourself as:
|
18
|
+
|
19
|
+
$ gem install snipr
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
#### Culling Runaway Resque Workers
|
24
|
+
```
|
25
|
+
Usage: reap_resque_workers [options]
|
26
|
+
|
27
|
+
Can be used to reap runaway resque workers that have exceeded too much memory use, CPU use, or time alive.
|
28
|
+
By default, this sends USR1 to the parent worker process, which causes it to immediately kill the runaway
|
29
|
+
child. The parent will then spawn another child to continue work.
|
30
|
+
|
31
|
+
Options:
|
32
|
+
-m, --memory [BYTES] Workers using more than some bytes size of memory
|
33
|
+
-c, --cpu [PERCENTAGE] workers using more than a percentage of CPU
|
34
|
+
-a, --alive [SECONDS] Workers that have been alive for some length of time in seconds
|
35
|
+
-s, --signal [SIGNAL] Signal to send to the worker's parent. Defaults to USR1.
|
36
|
+
```
|
37
|
+
|
38
|
+
## Contributing
|
39
|
+
|
40
|
+
1. Fork it ( https://github.com/[my-github-username]/snipr/fork )
|
41
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
42
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
43
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
44
|
+
5. Create a new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'ostruct'
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
options = OpenStruct.new({
|
6
|
+
:signal => 'USR1'
|
7
|
+
})
|
8
|
+
|
9
|
+
parser = OptionParser.new do |config|
|
10
|
+
config.banner = "Usage: reap_resque_workers [options]"
|
11
|
+
config.separator ""
|
12
|
+
config.separator "Can be used to reap runaway resque workers " +
|
13
|
+
"that have exceeded too much memory use, " +
|
14
|
+
"CPU use, or time alive."
|
15
|
+
config.separator "By default, this sends USR1 to the parent worker " +
|
16
|
+
"process, which causes it to immediately kill " +
|
17
|
+
"the runaway"
|
18
|
+
config.separator "child. The parent will then spawn another child to " +
|
19
|
+
"continue work."
|
20
|
+
config.separator ""
|
21
|
+
config.separator "Options:"
|
22
|
+
|
23
|
+
desc = "Workers using more than some bytes size of memory"
|
24
|
+
config.on("-m", "--memory [BYTES]", desc) do |bytes|
|
25
|
+
options.bytes = bytes.to_i
|
26
|
+
end
|
27
|
+
|
28
|
+
desc = "workers using more than a percentage of CPU"
|
29
|
+
config.on("-c", "--cpu [PERCENTAGE]", desc) do |cpu|
|
30
|
+
options.cpu = cpu.to_f
|
31
|
+
end
|
32
|
+
|
33
|
+
desc = "Workers that have been alive for some length of time in seconds"
|
34
|
+
config.on("-a", "--alive [SECONDS]", desc) do |sec|
|
35
|
+
options.alive = sec.to_i
|
36
|
+
end
|
37
|
+
|
38
|
+
desc = "Signal to send to the worker's parent. Defaults to USR1."
|
39
|
+
config.on("-s", "--signal [SIGNAL]", desc) do |signal|
|
40
|
+
options.signal = signal
|
41
|
+
end
|
42
|
+
end.parse!
|
43
|
+
|
44
|
+
# TODO remove me
|
45
|
+
$: << 'lib'
|
46
|
+
|
47
|
+
require 'snipr'
|
48
|
+
output = Snipr::Output.new
|
49
|
+
|
50
|
+
unless options.bytes || options.keys || options.alive
|
51
|
+
output.err("error - You must specify at least one of -m, -c or -a")
|
52
|
+
Kernel.exit(-1)
|
53
|
+
end
|
54
|
+
|
55
|
+
signaller = Snipr::ProcessSignaller.new do |signaller|
|
56
|
+
signaller.signal options.signal
|
57
|
+
signaller.target_parent true
|
58
|
+
signaller.include /resque/
|
59
|
+
signaller.include /processing/i
|
60
|
+
signaller.exclude /scheduler/i
|
61
|
+
|
62
|
+
if options.bytes
|
63
|
+
signaller.memory_greater_than(options.bytes)
|
64
|
+
end
|
65
|
+
|
66
|
+
if options.cpu
|
67
|
+
signaller.cpu_greater_than(options.cpu)
|
68
|
+
end
|
69
|
+
|
70
|
+
if options.alive
|
71
|
+
signaller.alive_longer_than(options.alive)
|
72
|
+
end
|
73
|
+
|
74
|
+
signaller.on_no_processes do
|
75
|
+
output.info("no runaways found")
|
76
|
+
end
|
77
|
+
|
78
|
+
signaller.after_signal do |signal, process|
|
79
|
+
msg = "sent #{signal} to worker #{process.ppid} to gracefully shutdown " +
|
80
|
+
"child #{process.pid}"
|
81
|
+
output.info(msg)
|
82
|
+
|
83
|
+
msg = "memory:#{process.memory} cpu:#{process.cpu} time_alive: " +
|
84
|
+
"#{process.seconds_alive} command: #{process.command}"
|
85
|
+
output.info(msg)
|
86
|
+
end
|
87
|
+
|
88
|
+
signaller.on_error do |error, signal, process|
|
89
|
+
raise error
|
90
|
+
if signal && process
|
91
|
+
output.err("error sending #{signal} to #{process.ppid} to gracefully shutdown #{process.pid}: #{error}")
|
92
|
+
else
|
93
|
+
output.err("error: #{error}")
|
94
|
+
Kernel.exit(-1)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
signaller.send_signals
|
data/lib/snipr.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'open3'
|
2
|
+
|
3
|
+
require 'snipr/version'
|
4
|
+
require 'snipr/output'
|
5
|
+
require 'snipr/process_locator'
|
6
|
+
require 'snipr/process_signaller'
|
7
|
+
|
8
|
+
module Snipr
|
9
|
+
##
|
10
|
+
# Error raised when a system command fails
|
11
|
+
class ExecError < StandardError; end
|
12
|
+
|
13
|
+
##
|
14
|
+
# Executes a command, returning the output as
|
15
|
+
# an array of lines. Raises an ExecError if the
|
16
|
+
# command did not execute cleanly.
|
17
|
+
def self.exec_cmd(command)
|
18
|
+
Open3.popen3(command) do |stdin, stdout, stderr|
|
19
|
+
err = stderr.read
|
20
|
+
raise ExecError, err unless err.empty?
|
21
|
+
stdout.read.split("\n")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
data/lib/snipr/output.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
3
|
+
module Snipr
|
4
|
+
##
|
5
|
+
# Class for writing to standard error & standard out in a
|
6
|
+
# uniform way.
|
7
|
+
class Output
|
8
|
+
##
|
9
|
+
# Write a message prepended with an ISO8601 timestamp to
|
10
|
+
# STDOUT
|
11
|
+
def info(msg)
|
12
|
+
STDOUT.write("#{runtime} #{msg}\n")
|
13
|
+
end
|
14
|
+
|
15
|
+
##
|
16
|
+
# Write a message prepndend with an ISO8601 timestamp to
|
17
|
+
# STDERR
|
18
|
+
def err(msg)
|
19
|
+
STDERR.write("#{runtime} #{msg}\n")
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
def runtime
|
24
|
+
Time.now.iso8601
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,134 @@
|
|
1
|
+
module Snipr
|
2
|
+
##
|
3
|
+
# Simple data structure representing a kernel process
|
4
|
+
KernelProcess = Struct.new(:pid,:ppid,:memory,:cpu,:etime,:seconds_alive,:command)
|
5
|
+
|
6
|
+
##
|
7
|
+
# Responsible for locating running processes and returning an array
|
8
|
+
# of KernelProcess objects that represent them. Uses the output of
|
9
|
+
# ps to locate processes, so this only works on *nix. Tested on
|
10
|
+
# RHEL 6.5 and Linux Mint
|
11
|
+
class ProcessLocator
|
12
|
+
DAY_SECONDS = 86400
|
13
|
+
HOUR_SECONDS = 3600
|
14
|
+
MINUTE_SECONDS = 60
|
15
|
+
|
16
|
+
|
17
|
+
attr_accessor :signal
|
18
|
+
attr_reader :includes, :excludes, :filters
|
19
|
+
|
20
|
+
def initialize
|
21
|
+
@includes = []
|
22
|
+
@excludes = []
|
23
|
+
@filters = []
|
24
|
+
end
|
25
|
+
|
26
|
+
##
|
27
|
+
# Locates the processes that match all include patterns and do not
|
28
|
+
# match all exclude patterns
|
29
|
+
def locate
|
30
|
+
processes = includes.reduce(all_processes, &by_inclusion_patterns)
|
31
|
+
processes = excludes.reduce(processes, &by_exclusion_patterns)
|
32
|
+
processes = filters.reduce(processes, &by_filter)
|
33
|
+
end
|
34
|
+
|
35
|
+
##
|
36
|
+
# Define a pattern that the command portion of the ps command must match to
|
37
|
+
# include the process. Multiple patterns can be defined and all must be
|
38
|
+
# matched
|
39
|
+
def include(pattern)
|
40
|
+
includes << pattern
|
41
|
+
end
|
42
|
+
|
43
|
+
##
|
44
|
+
# Define a pattern that the command portion of the ps command must match to
|
45
|
+
# exclude the process. Multiple patterns can be defined and all will be
|
46
|
+
# rejected.
|
47
|
+
def exclude(pattern)
|
48
|
+
excludes << pattern
|
49
|
+
end
|
50
|
+
|
51
|
+
##
|
52
|
+
# Define a size in bytes that processes must be greater than to be included
|
53
|
+
# in the result.
|
54
|
+
def memory_greater_than(bytes)
|
55
|
+
filter { |process| process.memory > bytes }
|
56
|
+
end
|
57
|
+
|
58
|
+
##
|
59
|
+
# Define a cpu use percentage that processes must be greater than to be
|
60
|
+
# included in the result.
|
61
|
+
def cpu_greater_than(percent)
|
62
|
+
filter {|process| process.cpu > percent}
|
63
|
+
end
|
64
|
+
|
65
|
+
##
|
66
|
+
# Define a time in seconds that processes must have been alive for longer
|
67
|
+
# than to be included in results
|
68
|
+
def alive_longer_than(sec)
|
69
|
+
filter {|process| process.seconds_alive > sec}
|
70
|
+
end
|
71
|
+
|
72
|
+
##
|
73
|
+
# Define your own filter using a lambda that receives a KernelProcess object
|
74
|
+
# and returns true if the process should be included in results
|
75
|
+
def filter(&callable)
|
76
|
+
filters << callable
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
def clear!
|
81
|
+
@includes = []
|
82
|
+
@excludes = []
|
83
|
+
end
|
84
|
+
|
85
|
+
def by_inclusion_patterns
|
86
|
+
lambda {|processes, filter| processes.select(&match(filter))}
|
87
|
+
end
|
88
|
+
|
89
|
+
def by_exclusion_patterns
|
90
|
+
lambda {|processes, filter| processes.reject(&match(filter))}
|
91
|
+
end
|
92
|
+
|
93
|
+
def by_filter
|
94
|
+
lambda {|processes, filter| processes.select(&filter)}
|
95
|
+
end
|
96
|
+
|
97
|
+
def match(filter)
|
98
|
+
lambda {|process| process.command.match(filter)}
|
99
|
+
end
|
100
|
+
|
101
|
+
def all_processes
|
102
|
+
Snipr.exec_cmd('ps h -eo pid,ppid,size,%cpu,etime,cmd').map do |line|
|
103
|
+
pid, ppid, mem, cpu, etime, *cmd = line.split
|
104
|
+
cmd = cmd.join(" ")
|
105
|
+
|
106
|
+
KernelProcess.new(
|
107
|
+
pid.to_i,
|
108
|
+
ppid.to_i,
|
109
|
+
mem.to_i,
|
110
|
+
cpu.to_f,
|
111
|
+
etime,
|
112
|
+
parse_seconds(etime),
|
113
|
+
cmd
|
114
|
+
)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
##
|
119
|
+
# Parses etime, which is in the format dd-hh:mm:ss
|
120
|
+
# dd- will be omitted if the run time is < 24 hours
|
121
|
+
# hh: will be omitted if the run time is < 1 hour
|
122
|
+
def parse_seconds(etime)
|
123
|
+
time, days = etime.split("-").reverse
|
124
|
+
sec, min, hr = time.split(":").reverse
|
125
|
+
|
126
|
+
total = 0
|
127
|
+
total += days.to_i * DAY_SECONDS if days
|
128
|
+
total += hr.to_i * HOUR_SECONDS if hr
|
129
|
+
total += min.to_i * MINUTE_SECONDS if min
|
130
|
+
total += sec.to_i if sec
|
131
|
+
total
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Snipr
|
4
|
+
##
|
5
|
+
# Class that can send signals to targetted processes or their
|
6
|
+
# parent processes and invoke callbacks around the actions. Delegates
|
7
|
+
# process location to a ProcessLocator. Is configured using a
|
8
|
+
# block on initialization as follows:
|
9
|
+
#
|
10
|
+
# signaller = ProcessSignaller.new do |signaller|
|
11
|
+
# signaller.include /resque/
|
12
|
+
# signaller.exclude /scheduler/
|
13
|
+
# signaller.signal "USR1"
|
14
|
+
# signaller.target_parent false
|
15
|
+
#
|
16
|
+
# signaller.on_no_processes do
|
17
|
+
# puts "No processes"
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# signaller.before_signal do |signal, process|
|
21
|
+
# puts "Sending #{signal} to #{process.pid}"
|
22
|
+
# end
|
23
|
+
#
|
24
|
+
# signaller.after_signal do |signal, process|
|
25
|
+
# puts "Sent #{signal} to #{process.pid}"
|
26
|
+
# end
|
27
|
+
#
|
28
|
+
# signaller.on_error do |e, signal, process|
|
29
|
+
# puts "Ooops, got #{e} sending #{signal} to #{process.pid}"
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
#
|
33
|
+
# signaller.send_signals
|
34
|
+
#
|
35
|
+
class ProcessSignaller
|
36
|
+
extend Forwardable
|
37
|
+
def_delegators :locator, :include, :exclude, :memory_greater_than,
|
38
|
+
:cpu_greater_than, :alive_longer_than, :filter
|
39
|
+
|
40
|
+
attr_reader :signal
|
41
|
+
attr_writer :locator
|
42
|
+
|
43
|
+
def initialize(&block)
|
44
|
+
@locator = ProcessLocator.new
|
45
|
+
on_no_processes {}
|
46
|
+
before_signal {}
|
47
|
+
after_signal {}
|
48
|
+
on_error {|e, process, signal| raise e}
|
49
|
+
block.call(self)
|
50
|
+
end
|
51
|
+
|
52
|
+
##
|
53
|
+
# Send the specified signal to all located processes
|
54
|
+
def send_signals
|
55
|
+
processes = @locator.locate
|
56
|
+
|
57
|
+
if processes.empty?
|
58
|
+
@on_no_processes.call
|
59
|
+
else
|
60
|
+
processes.each do |process|
|
61
|
+
signal_process(process)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
rescue StandardError => e
|
65
|
+
@on_error.call(e)
|
66
|
+
end
|
67
|
+
|
68
|
+
def signal(signal)
|
69
|
+
@signal = Signal.list[signal.to_s.upcase].tap do |sig|
|
70
|
+
unless sig
|
71
|
+
raise "'#{signal}' not found -- see http://ruby-doc.org/core-1.8.7/Signal.html#method-c-list"
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def locator(locator=nil)
|
77
|
+
@locator ||= (locator || ProcessLocator.new)
|
78
|
+
end
|
79
|
+
|
80
|
+
def on_no_processes(&callback)
|
81
|
+
@on_no_processes = callback
|
82
|
+
end
|
83
|
+
|
84
|
+
def before_signal(&callback)
|
85
|
+
@before_signal = callback
|
86
|
+
end
|
87
|
+
|
88
|
+
def after_signal(&callback)
|
89
|
+
@after_signal = callback
|
90
|
+
end
|
91
|
+
|
92
|
+
def on_error(&callback)
|
93
|
+
@on_error = callback
|
94
|
+
end
|
95
|
+
|
96
|
+
def target_parent(flag=false)
|
97
|
+
@target_parent = flag
|
98
|
+
end
|
99
|
+
|
100
|
+
private
|
101
|
+
def signal_process(process)
|
102
|
+
@before_signal.call(@signal, process)
|
103
|
+
if @target_parent
|
104
|
+
Process.kill(@signal, process.ppid)
|
105
|
+
else
|
106
|
+
Process.kill(@signal, process.pid)
|
107
|
+
end
|
108
|
+
@after_signal.call(@signal, process)
|
109
|
+
rescue StandardError => e
|
110
|
+
@on_error.call(e, @signal, process)
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
data/snipr.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'snipr/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "snipr"
|
8
|
+
spec.version = Snipr::VERSION
|
9
|
+
spec.authors = ["Lance Woodson"]
|
10
|
+
spec.email = ["lance@webmaneuvers.com"]
|
11
|
+
spec.summary = %q{Take aim and fire at runaway processes using ruby}
|
12
|
+
spec.description = <<-END
|
13
|
+
Ruby classes and executables for targetting and sending signals to
|
14
|
+
*nix processes that match/don't match command name patterns, memory
|
15
|
+
use, cpu use and time alive
|
16
|
+
END
|
17
|
+
spec.homepage = ""
|
18
|
+
spec.license = "MIT"
|
19
|
+
|
20
|
+
spec.files = `git ls-files -z`.split("\x0")
|
21
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
22
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
23
|
+
spec.require_paths = ["lib"]
|
24
|
+
|
25
|
+
spec.add_development_dependency "bundler", "~> 1.6"
|
26
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
27
|
+
spec.add_development_dependency "rspec", "~> 3.1.0"
|
28
|
+
spec.add_development_dependency "pry"
|
29
|
+
end
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
module Snipr
|
4
|
+
describe ProcessLocator do
|
5
|
+
describe "#parse_seconds" do
|
6
|
+
let(:result) {subject.send(:parse_seconds, etime)}
|
7
|
+
|
8
|
+
describe "with a time spanning days" do
|
9
|
+
let(:etime) {"1-02:03:04"}
|
10
|
+
it "should calculate the correct number of seconds" do
|
11
|
+
expect(result).to eq(93784)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "with a time spanning hours" do
|
16
|
+
let(:etime) {"2:03:04"}
|
17
|
+
it "should calculate the correct number of seconds" do
|
18
|
+
expect(result).to eq(7384)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "with a time spanning minutes" do
|
23
|
+
let(:etime) {"03:04"}
|
24
|
+
it "should calculate the correct number of seconds" do
|
25
|
+
expect(result).to eq(184)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe "with a time spanning seconds" do
|
30
|
+
let(:etime) {"04"}
|
31
|
+
it "should calculate the correct number of seconds" do
|
32
|
+
expect(result).to eq(4)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
describe "#locate" do
|
38
|
+
let(:ps_output) {File.read("spec/ps_output.txt").split("\n")}
|
39
|
+
before do
|
40
|
+
expect(Snipr).to receive(:exec_cmd).and_return(ps_output).at_least(:once)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should return an array of KernelProcess objects" do
|
44
|
+
process = subject.locate.select{|process| process.pid == 3552}.first
|
45
|
+
expect(process.pid).to eq(3552)
|
46
|
+
expect(process.ppid).to eq(4354)
|
47
|
+
expect(process.memory).to eq(1297860)
|
48
|
+
expect(process.cpu).to eq(8.7)
|
49
|
+
expect(process.etime).to eq("1-07:12:08")
|
50
|
+
expect(process.seconds_alive).to eq(112328)
|
51
|
+
expect(process.command).to eq("resque-1.24.1: Processing foo since 1410077129 [FooJob]")
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "when no processes match filters" do
|
55
|
+
it "should return an empty array" do
|
56
|
+
subject.include /thereisnoinputwiththisstring/
|
57
|
+
expect(subject.locate).to be_empty
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe "when a single include is specified" do
|
62
|
+
it "should return all processes that match the include pattern" do
|
63
|
+
subject.include /Processing/
|
64
|
+
expect(subject.locate.size).to eq(20)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "when multiple includes are specified" do
|
69
|
+
it "should return all processses that match all include patterns" do
|
70
|
+
subject.include /Processing/
|
71
|
+
subject.include /Delayed Items/
|
72
|
+
expect(subject.locate.size).to eq(1)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "when a single exclude is specified" do
|
77
|
+
it "should return all processes not matching the exclude pattern" do
|
78
|
+
subject.exclude /grep/
|
79
|
+
expect(subject.locate.size).to eq(52)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
describe "when multiple excludes are specified" do
|
84
|
+
it "should return all processes that don't match all exclude patterns" do
|
85
|
+
subject.exclude /grep/
|
86
|
+
subject.exclude /scheduler/
|
87
|
+
expect(subject.locate.size).to eq(51)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "when filtering processes by memory greater than" do
|
92
|
+
it "should return only processes using memory greater than the specified amount" do
|
93
|
+
subject.memory_greater_than(1000000000)
|
94
|
+
expect(subject.locate.size).to eq(1)
|
95
|
+
expect(subject.locate.first.pid).to eq(32178)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
describe "when filtering processes by cpu greater than" do
|
100
|
+
it "should return only processes using cpu greater than the specified amount" do
|
101
|
+
subject.cpu_greater_than(90.0)
|
102
|
+
expect(subject.locate.size).to eq(1)
|
103
|
+
expect(subject.locate.first.pid).to eq(6337)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
describe "when filtering processes by alive longer than" do
|
108
|
+
it "should return only processes that have been alive longer than the specified amount" do
|
109
|
+
subject.alive_longer_than(31449600)
|
110
|
+
expect(subject.locate.size).to eq(1)
|
111
|
+
expect(subject.locate.first.pid).to eq(28309)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe "when filtering by a custom filter" do
|
116
|
+
it "should return only processes that match the filter" do
|
117
|
+
subject.filter { |process| process.pid % 2 == 0 }
|
118
|
+
expect(subject.locate.size).to eq(24)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'ostruct'
|
3
|
+
|
4
|
+
module Snipr
|
5
|
+
describe ProcessSignaller do
|
6
|
+
let(:signal) {Signal.list["USR1"]}
|
7
|
+
let(:ps_output) {File.read("spec/ps_output.txt").split("\n")}
|
8
|
+
let(:checkins) {OpenStruct.new}
|
9
|
+
subject do
|
10
|
+
ProcessSignaller.new do |signaller|
|
11
|
+
signaller.include /resque/i
|
12
|
+
signaller.signal "USR1"
|
13
|
+
|
14
|
+
signaller.on_no_processes do
|
15
|
+
checkins.on_no_processes = true
|
16
|
+
end
|
17
|
+
|
18
|
+
signaller.before_signal do |signal, process|
|
19
|
+
checkins.before_signal = "#{signal} > #{process.pid}"
|
20
|
+
end
|
21
|
+
|
22
|
+
signaller.after_signal do |signal, process|
|
23
|
+
checkins.after_signal = "#{signal} > #{process.pid}"
|
24
|
+
end
|
25
|
+
|
26
|
+
signaller.on_error do |exc, signal, process|
|
27
|
+
msg = "#{exc}"
|
28
|
+
if signal && process
|
29
|
+
msg += " #{signal} > #{process.pid}"
|
30
|
+
end
|
31
|
+
checkins.on_error = msg
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe "#signal" do
|
37
|
+
it "should raise an error if the signal is not defined by the system" do
|
38
|
+
expect{subject.signal("AJDKASDJKLASD")}.to raise_error
|
39
|
+
end
|
40
|
+
|
41
|
+
it "should assign the numeric value of the signal as defined by the system" do
|
42
|
+
expect(subject.signal("USR1")).to eq(Signal.list["USR1"])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "#signal" do
|
47
|
+
context "when there is a general failure" do
|
48
|
+
let(:locator) {subject.instance_variable_get(:@locator)}
|
49
|
+
|
50
|
+
it "should invoke the on_error callback" do
|
51
|
+
expect(locator).to receive(:locate).and_raise('Ouch!')
|
52
|
+
subject.send_signals
|
53
|
+
expect(checkins.on_error).to eq("Ouch!")
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
context "when no processes found" do
|
58
|
+
before {subject.exclude /resque/i}
|
59
|
+
it "should invoke the on_no_processes callback" do
|
60
|
+
subject.send_signals
|
61
|
+
expect(checkins.on_no_processes).to be_truthy
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context "when process is found" do
|
66
|
+
before do
|
67
|
+
expect(Snipr).to receive(:exec_cmd).and_return(ps_output).at_least(:once)
|
68
|
+
subject.cpu_greater_than(90)
|
69
|
+
end
|
70
|
+
|
71
|
+
context "targetting the process itself" do
|
72
|
+
it "should send the appropriate signal to the process and call callbacks" do
|
73
|
+
expect(Process).to receive(:kill).with(signal, 6337)
|
74
|
+
subject.send_signals
|
75
|
+
expect(checkins.before_signal).to eq("#{signal} > 6337")
|
76
|
+
expect(checkins.after_signal).to eq("#{signal} > 6337")
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
context "targetting the parent process" do
|
81
|
+
it "should send the appropriate signal to the parent process and call callbacks" do
|
82
|
+
subject.target_parent true
|
83
|
+
expect(Process).to receive(:kill).with(signal, 4347)
|
84
|
+
subject.send_signals
|
85
|
+
expect(checkins.before_signal).to eq("#{signal} > 6337")
|
86
|
+
expect(checkins.after_signal).to eq("#{signal} > 6337")
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
context "when encountering an error signalling a process" do
|
91
|
+
it "should call the on_error callback" do
|
92
|
+
expect(Process).to receive(:kill).with(signal, 6337).and_raise('Ouch!')
|
93
|
+
subject.send_signals
|
94
|
+
expect(checkins.on_error).to eq("Ouch! #{signal} > 6337")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
data/spec/ps_output.txt
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
3552 4354 1297860 8.7 1-07:12:08 resque-1.24.1: Processing foo since 1410077129 [FooJob]
|
2
|
+
4236 1 1169412 0.0 3-12:52:29 resque-1.24.1: Waiting for bar,bar
|
3
|
+
4238 1 1191864 0.0 3-12:52:29 resque-1.24.1: Waiting for foo,bar
|
4
|
+
4241 1 1192064 0.0 3-12:52:29 resque-1.24.1: Waiting for foo,bar
|
5
|
+
4245 1 1184716 0.2 3-12:52:29 resque-1.24.1: Forked 28309 at 1410084369
|
6
|
+
4248 1 1206600 0.3 3-12:52:29 resque-1.24.1: Forked 28778 at 1410189413
|
7
|
+
4250 1 1198872 0.2 3-12:52:29 resque-1.24.1: Forked 32153 at 1410189457
|
8
|
+
4253 1 1202312 0.3 3-12:52:29 resque-1.24.1: Forked 31818 at 1410189453
|
9
|
+
4257 1 1194072 0.4 3-12:52:29 resque-1.24.1: Forked 17477 at 1410188882
|
10
|
+
4259 1 1188480 0.0 3-12:52:29 resque-1.24.1: Waiting for foo,bar
|
11
|
+
4263 1 1188872 0.0 3-12:52:29 resque-1.24.1: Waiting for foo,bar
|
12
|
+
4265 1 1189032 0.0 3-12:52:29 resque-1.24.1: Waiting for foo,bar
|
13
|
+
4269 1 1182676 0.1 3-12:52:29 resque-1.24.1: Forked 29640 at 1410189424
|
14
|
+
4271 1 1195432 0.2 3-12:52:29 resque-1.24.1: Forked 32035 at 1410189455
|
15
|
+
4274 1 1181816 0.1 3-12:52:29 resque-1.24.1: Forked 31915 at 1410189454
|
16
|
+
4277 1 1188152 0.1 3-12:52:28 resque-1.24.1: Forked 32038 at 1410189455
|
17
|
+
4280 1 1179444 0.1 3-12:52:28 resque-1.24.1: Forked 30789 at 1410185297
|
18
|
+
4284 1 1169100 0.0 3-12:52:28 resque-1.24.1: Waiting for baz
|
19
|
+
4293 1 1183856 0.0 3-12:52:28 resque-1.24.1: Waiting for baz
|
20
|
+
4301 1 1170028 0.0 3-12:52:28 resque-1.24.1: Waiting for baz
|
21
|
+
4309 1 1155112 0.0 3-12:52:28 resque-1.24.1: Waiting for baz
|
22
|
+
4316 1 1188900 0.0 3-12:52:28 resque-1.24.1: Waiting for baz
|
23
|
+
4321 1 1196844 0.3 3-12:52:28 resque-1.24.1: Forked 7229 at 1410131675
|
24
|
+
4324 1 1157052 0.3 3-12:52:28 resque-1.24.1: Forked 28474 at 1410189411
|
25
|
+
4326 1 1179852 0.1 3-12:52:28 resque-1.24.1: Forked 27462 at 1410189396
|
26
|
+
4337 1 1202044 0.3 3-12:52:28 resque-1.24.1: Forked 32178 at 1410189457
|
27
|
+
4340 1 1202976 0.3 3-12:52:27 resque-1.24.1: Forked 31662 at 1410189012
|
28
|
+
4347 1 1163824 0.4 3-12:52:27 resque-1.24.1: Forked 6337 at 1410189132
|
29
|
+
4351 1 1163120 0.2 3-12:52:27 resque-1.24.1: Forked 22400 at 1410189331
|
30
|
+
4354 1 1196480 0.2 3-12:52:27 resque-1.24.1: Forked 3552 at 1410077129
|
31
|
+
4357 1 1163572 0.3 3-12:52:27 resque-1.24.1: Forked 8356 at 1410189171
|
32
|
+
4361 1 1187620 0.0 3-12:52:27 resque-1.24.1: Waiting for foo
|
33
|
+
4364 1 1169364 0.0 3-12:52:27 resque-1.24.1: Waiting for bar
|
34
|
+
5585 1 972188 0.0 3-12:50:57 resque-scheduler-2.0.1: Processing Delayed Items
|
35
|
+
6337 4347 1293004 99.9 05:26 resque-1.24.1: Processing foo since 1410189132 [FooJob]
|
36
|
+
7229 4321 1292692 8.2 16:03:03 resque-1.24.1: Processing foo since 1410131675 [FooJob]
|
37
|
+
8356 4357 1220988 19.0 04:47 resque-1.24.1: Processing foo since 1410189171 [FooJob]
|
38
|
+
17477 4257 1194072 0.0 09:35 resque-1.24.1: Processing foo since 1410188882 [FooJob]
|
39
|
+
22400 4351 1219712 3.3 02:06 resque-1.24.1: Processing foo since 1410189331 [FooJob]
|
40
|
+
27462 4326 1194624 2.1 01:02 resque-1.24.1: Processing foo since 1410189396 [FooJob]
|
41
|
+
28309 4245 1278328 7.1 364-05:11:28 resque-1.24.1: Processing foo since 1410084369 [FooJob]
|
42
|
+
28474 4324 1182012 6.7 00:47 resque-1.24.1: Processing foo since 1410189410 [FooJob]
|
43
|
+
28778 4248 1205572 4.9 00:45 resque-1.24.1: Processing foo since 1410189413 [FooJob]
|
44
|
+
29640 4269 1189152 3.2 00:34 resque-1.24.1: Processing foo since 1410189424 [FooJob]
|
45
|
+
30789 4280 1263476 6.7 01:09:21 resque-1.24.1: Processing foo since 1410185297 [FooJob]
|
46
|
+
31662 4340 1286344 2.7 07:26 resque-1.24.1: Processing foo since 1410189012 [FooJob]
|
47
|
+
31818 4253 1202312 4.6 00:05 resque-1.24.1: Processing foo since 1410189452 [FooJob]
|
48
|
+
31915 4274 1181816 13.2 00:04 resque-1.24.1: Processing foo since 1410189454 [FooJob]
|
49
|
+
32035 4271 1195432 13.5 00:02 resque-1.24.1: Processing foo since 1410189455 [FooJob]
|
50
|
+
32038 4277 1188152 13.0 00:02 resque-1.24.1: Processing foo since 1410189455 [FooJob]
|
51
|
+
32153 4250 1198872 21.0 00:01 resque-1.24.1: Processing foo since 1410189457 [FooJob]
|
52
|
+
32178 4337 1001202048 0.0 00:00 resque-1.24.1: Processing bar since 1410189457 [BarJob]
|
53
|
+
32189 28509 276 0.0 00:00 grep resque
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,114 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: snipr
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Lance Woodson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
|
12
|
+
date: 2014-09-08 00:00:00 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: bundler
|
16
|
+
prerelease: false
|
17
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: "1.6"
|
22
|
+
type: :development
|
23
|
+
version_requirements: *id001
|
24
|
+
- !ruby/object:Gem::Dependency
|
25
|
+
name: rake
|
26
|
+
prerelease: false
|
27
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
28
|
+
requirements:
|
29
|
+
- - ~>
|
30
|
+
- !ruby/object:Gem::Version
|
31
|
+
version: "10.0"
|
32
|
+
type: :development
|
33
|
+
version_requirements: *id002
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: rspec
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
38
|
+
requirements:
|
39
|
+
- - ~>
|
40
|
+
- !ruby/object:Gem::Version
|
41
|
+
version: 3.1.0
|
42
|
+
type: :development
|
43
|
+
version_requirements: *id003
|
44
|
+
- !ruby/object:Gem::Dependency
|
45
|
+
name: pry
|
46
|
+
prerelease: false
|
47
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- &id005
|
50
|
+
- ">="
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: "0"
|
53
|
+
type: :development
|
54
|
+
version_requirements: *id004
|
55
|
+
description: |
|
56
|
+
Ruby classes and executables for targetting and sending signals to
|
57
|
+
*nix processes that match/don't match command name patterns, memory
|
58
|
+
use, cpu use and time alive
|
59
|
+
|
60
|
+
email:
|
61
|
+
- lance@webmaneuvers.com
|
62
|
+
executables:
|
63
|
+
- reap_resque_workers
|
64
|
+
extensions: []
|
65
|
+
|
66
|
+
extra_rdoc_files: []
|
67
|
+
|
68
|
+
files:
|
69
|
+
- .gitignore
|
70
|
+
- .ruby-gemset
|
71
|
+
- .ruby-version
|
72
|
+
- Gemfile
|
73
|
+
- LICENSE.txt
|
74
|
+
- README.md
|
75
|
+
- Rakefile
|
76
|
+
- bin/reap_resque_workers
|
77
|
+
- lib/snipr.rb
|
78
|
+
- lib/snipr/output.rb
|
79
|
+
- lib/snipr/process_locator.rb
|
80
|
+
- lib/snipr/process_signaller.rb
|
81
|
+
- lib/snipr/version.rb
|
82
|
+
- snipr.gemspec
|
83
|
+
- spec/process_locator_spec.rb
|
84
|
+
- spec/process_signaller_spec.rb
|
85
|
+
- spec/ps_output.txt
|
86
|
+
- spec/spec_helper.rb
|
87
|
+
homepage: ""
|
88
|
+
licenses:
|
89
|
+
- MIT
|
90
|
+
metadata: {}
|
91
|
+
|
92
|
+
post_install_message:
|
93
|
+
rdoc_options: []
|
94
|
+
|
95
|
+
require_paths:
|
96
|
+
- lib
|
97
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- *id005
|
100
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- *id005
|
103
|
+
requirements: []
|
104
|
+
|
105
|
+
rubyforge_project:
|
106
|
+
rubygems_version: 2.0.14
|
107
|
+
signing_key:
|
108
|
+
specification_version: 4
|
109
|
+
summary: Take aim and fire at runaway processes using ruby
|
110
|
+
test_files:
|
111
|
+
- spec/process_locator_spec.rb
|
112
|
+
- spec/process_signaller_spec.rb
|
113
|
+
- spec/ps_output.txt
|
114
|
+
- spec/spec_helper.rb
|