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.
@@ -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