snipr 0.0.1
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.
- 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
|