snipr 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -0,0 +1,14 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ *.bundle
11
+ *.so
12
+ *.o
13
+ *.a
14
+ mkmf.log
@@ -0,0 +1 @@
1
+ snipr
@@ -0,0 +1 @@
1
+ ruby-1.8.7-p374
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in snipr.gemspec
4
+ gemspec
@@ -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.
@@ -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
@@ -0,0 +1,2 @@
1
+ require "bundler/gem_tasks"
2
+
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module Snipr
2
+ VERSION = "0.0.1"
3
+ end
@@ -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
@@ -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
@@ -0,0 +1,2 @@
1
+ require 'rspec'
2
+ require 'snipr'
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