god 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/Manifest.txt +37 -0
- data/README.txt +42 -0
- data/Rakefile +28 -0
- data/bin/god +26 -0
- data/examples/gravatar.god +41 -0
- data/examples/local.god +60 -0
- data/lib/god.rb +35 -0
- data/lib/god/base.rb +13 -0
- data/lib/god/behavior.rb +67 -0
- data/lib/god/behaviors/clean_pid_file.rb +23 -0
- data/lib/god/condition.rb +48 -0
- data/lib/god/conditions/always.rb +11 -0
- data/lib/god/conditions/cpu_usage.rb +44 -0
- data/lib/god/conditions/memory_usage.rb +44 -0
- data/lib/god/conditions/process_not_running.rb +22 -0
- data/lib/god/conditions/timeline.rb +17 -0
- data/lib/god/errors.rb +12 -0
- data/lib/god/meddle.rb +54 -0
- data/lib/god/reporter.rb +25 -0
- data/lib/god/server.rb +28 -0
- data/lib/god/system/process.rb +56 -0
- data/lib/god/timer.rb +78 -0
- data/lib/god/watch.rb +151 -0
- data/test/configs/real.rb +64 -0
- data/test/helper.rb +93 -0
- data/test/suite.rb +6 -0
- data/test/test_behavior.rb +13 -0
- data/test/test_condition.rb +26 -0
- data/test/test_god.rb +15 -0
- data/test/test_meddle.rb +46 -0
- data/test/test_reporter.rb +18 -0
- data/test/test_server.rb +24 -0
- data/test/test_system_process.rb +42 -0
- data/test/test_timeline.rb +24 -0
- data/test/test_timer.rb +43 -0
- data/test/test_watch.rb +123 -0
- metadata +111 -0
@@ -0,0 +1,44 @@
|
|
1
|
+
module God
|
2
|
+
module Conditions
|
3
|
+
|
4
|
+
class CpuUsage < PollCondition
|
5
|
+
attr_accessor :pid_file, :above, :times
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
self.above = nil
|
10
|
+
self.times = [1, 1]
|
11
|
+
end
|
12
|
+
|
13
|
+
def prepare
|
14
|
+
if self.times.kind_of?(Integer)
|
15
|
+
self.times = [self.times, self.times]
|
16
|
+
end
|
17
|
+
|
18
|
+
@timeline = Timeline.new(self.times[1])
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid?
|
22
|
+
valid = true
|
23
|
+
valid &= complain("You must specify the 'pid_file' attribute for :memory_usage") if self.pid_file.nil?
|
24
|
+
valid &= complain("You must specify the 'above' attribute for :memory_usage") if self.above.nil?
|
25
|
+
valid
|
26
|
+
end
|
27
|
+
|
28
|
+
def test
|
29
|
+
return false unless File.exist?(self.pid_file)
|
30
|
+
|
31
|
+
pid = File.open(self.pid_file).read.strip
|
32
|
+
process = System::Process.new(pid)
|
33
|
+
@timeline.push(process.percent_cpu)
|
34
|
+
if @timeline.select { |x| x > self.above }.size < self.times.first
|
35
|
+
return true
|
36
|
+
else
|
37
|
+
@timeline.clear
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module God
|
2
|
+
module Conditions
|
3
|
+
|
4
|
+
class MemoryUsage < PollCondition
|
5
|
+
attr_accessor :pid_file, :above, :times
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
super
|
9
|
+
self.above = nil
|
10
|
+
self.times = [1, 1]
|
11
|
+
end
|
12
|
+
|
13
|
+
def prepare
|
14
|
+
if self.times.kind_of?(Integer)
|
15
|
+
self.times = [self.times, self.times]
|
16
|
+
end
|
17
|
+
|
18
|
+
@timeline = Timeline.new(self.times[1])
|
19
|
+
end
|
20
|
+
|
21
|
+
def valid?
|
22
|
+
valid = true
|
23
|
+
valid &= complain("You must specify the 'pid_file' attribute for :memory_usage") if self.pid_file.nil?
|
24
|
+
valid &= complain("You must specify the 'above' attribute for :memory_usage") if self.above.nil?
|
25
|
+
valid
|
26
|
+
end
|
27
|
+
|
28
|
+
def test
|
29
|
+
return false unless File.exist?(self.pid_file)
|
30
|
+
|
31
|
+
pid = File.open(self.pid_file).read.strip
|
32
|
+
process = System::Process.new(pid)
|
33
|
+
@timeline.push(process.memory)
|
34
|
+
if @timeline.select { |x| x > self.above }.size < self.times.first
|
35
|
+
return true
|
36
|
+
else
|
37
|
+
@timeline.clear
|
38
|
+
return false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module God
|
2
|
+
module Conditions
|
3
|
+
|
4
|
+
class ProcessNotRunning < PollCondition
|
5
|
+
attr_accessor :pid_file
|
6
|
+
|
7
|
+
def valid?
|
8
|
+
valid = true
|
9
|
+
valid &= complain("You must specify the 'pid_file' attribute for :process_not_running") if self.pid_file.nil?
|
10
|
+
valid
|
11
|
+
end
|
12
|
+
|
13
|
+
def test
|
14
|
+
return false unless File.exist?(self.pid_file)
|
15
|
+
|
16
|
+
pid = File.open(self.pid_file).read.strip
|
17
|
+
System::Process.new(pid).exists?
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
data/lib/god/errors.rb
ADDED
data/lib/god/meddle.rb
ADDED
@@ -0,0 +1,54 @@
|
|
1
|
+
module God
|
2
|
+
|
3
|
+
class Meddle < Base
|
4
|
+
# drb
|
5
|
+
attr_accessor :server
|
6
|
+
|
7
|
+
# api
|
8
|
+
attr_accessor :watches, :timer
|
9
|
+
|
10
|
+
# Create a new instance that is ready for use by a configuration file
|
11
|
+
def initialize(options = {})
|
12
|
+
self.watches = []
|
13
|
+
self.server = Server.new(self, options[:host], options[:port])
|
14
|
+
self.timer = Timer.new
|
15
|
+
end
|
16
|
+
|
17
|
+
# Instantiate a new, empty Watch object and pass it to the mandatory
|
18
|
+
# block. The attributes of the watch will be set by the configuration
|
19
|
+
# file.
|
20
|
+
def watch
|
21
|
+
w = Watch.new(self)
|
22
|
+
yield(w)
|
23
|
+
|
24
|
+
# ensure the new watch has a unique name
|
25
|
+
unless @watches.select { |x| x.name == w.name }.empty?
|
26
|
+
abort "Duplicate Watch with name '#{w.name}'"
|
27
|
+
end
|
28
|
+
|
29
|
+
# add to list of watches
|
30
|
+
@watches << w
|
31
|
+
end
|
32
|
+
|
33
|
+
# Schedule all poll conditions and register all condition events
|
34
|
+
def monitor
|
35
|
+
@watches.each { |w| w.monitor }
|
36
|
+
end
|
37
|
+
|
38
|
+
# def monitor
|
39
|
+
# threads = []
|
40
|
+
#
|
41
|
+
# @watches.each do |w|
|
42
|
+
# threads << Thread.new do
|
43
|
+
# while true do
|
44
|
+
# w.run
|
45
|
+
# sleep self.interval
|
46
|
+
# end
|
47
|
+
# end
|
48
|
+
# end
|
49
|
+
#
|
50
|
+
# threads.each { |t| t.join }
|
51
|
+
# end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/lib/god/reporter.rb
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'drb'
|
2
|
+
|
3
|
+
module God
|
4
|
+
|
5
|
+
class Reporter
|
6
|
+
def initialize(host = nil, port = nil)
|
7
|
+
@host = host
|
8
|
+
@port = port || 7777
|
9
|
+
@service = nil
|
10
|
+
end
|
11
|
+
|
12
|
+
def method_missing(*args, &block)
|
13
|
+
service.send(*args, &block)
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def service
|
19
|
+
return @service if @service
|
20
|
+
DRb.start_service
|
21
|
+
@service = DRbObject.new(nil, "druby://#{@host}:#{@port}")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
data/lib/god/server.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
require 'drb'
|
2
|
+
|
3
|
+
# The God::Server oversees the DRb server which dishes out info on this God daemon.
|
4
|
+
|
5
|
+
module God
|
6
|
+
|
7
|
+
class Server
|
8
|
+
attr_reader :host, :port
|
9
|
+
|
10
|
+
def initialize(meddle = nil, host = nil, port = nil)
|
11
|
+
@meddle = meddle
|
12
|
+
@host = host
|
13
|
+
@port = port || 7777
|
14
|
+
start
|
15
|
+
end
|
16
|
+
|
17
|
+
def method_missing(*args, &block)
|
18
|
+
@meddle.send(*args, &block)
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def start
|
24
|
+
@drb ||= DRb.start_service("druby://#{@host}:#{@port}", self)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
module God
|
2
|
+
module System
|
3
|
+
|
4
|
+
class Process
|
5
|
+
def initialize(pid)
|
6
|
+
@pid = pid.to_i
|
7
|
+
end
|
8
|
+
|
9
|
+
# Return true if this process is running, false otherwise
|
10
|
+
def exists?
|
11
|
+
cmd_name = RUBY_PLATFORM =~ /solaris/i ? "args" : "command"
|
12
|
+
!ps_string(cmd_name).empty?
|
13
|
+
end
|
14
|
+
|
15
|
+
# Memory usage in kilobytes (resident set size)
|
16
|
+
def memory
|
17
|
+
ps_int('rss')
|
18
|
+
end
|
19
|
+
|
20
|
+
# Percentage memory usage
|
21
|
+
def percent_memory
|
22
|
+
ps_float('%mem')
|
23
|
+
end
|
24
|
+
|
25
|
+
# Percentage CPU usage
|
26
|
+
def percent_cpu
|
27
|
+
ps_float('%cpu')
|
28
|
+
end
|
29
|
+
|
30
|
+
# Seconds of CPU time (accumulated cpu time, user + system)
|
31
|
+
def cpu_time
|
32
|
+
time_string_to_seconds(ps_string('time'))
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def ps_int(keyword)
|
38
|
+
`ps -o #{keyword}= -p #{@pid}`.to_i
|
39
|
+
end
|
40
|
+
|
41
|
+
def ps_float(keyword)
|
42
|
+
`ps -o #{keyword}= -p #{@pid}`.to_f
|
43
|
+
end
|
44
|
+
|
45
|
+
def ps_string(keyword)
|
46
|
+
`ps -o #{keyword}= -p #{@pid}`.strip
|
47
|
+
end
|
48
|
+
|
49
|
+
def time_string_to_seconds(text)
|
50
|
+
_, minutes, seconds, useconds = *text.match(/(\d+):(\d{2}).(\d{2})/)
|
51
|
+
(minutes.to_i * 60) + seconds.to_i
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
end
|
56
|
+
end
|
data/lib/god/timer.rb
ADDED
@@ -0,0 +1,78 @@
|
|
1
|
+
module God
|
2
|
+
|
3
|
+
class TimerEvent
|
4
|
+
attr_accessor :watch, :condition, :command, :at
|
5
|
+
|
6
|
+
def initialize(watch, condition, command)
|
7
|
+
self.watch = watch
|
8
|
+
self.condition = condition
|
9
|
+
self.command = command
|
10
|
+
self.at = Time.now.to_i + condition.interval
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
class Timer < Base
|
15
|
+
INTERVAL = 0.25
|
16
|
+
|
17
|
+
attr_reader :events
|
18
|
+
|
19
|
+
# Start the scheduler loop to handle events
|
20
|
+
def initialize
|
21
|
+
@events = []
|
22
|
+
|
23
|
+
@timer = Thread.new do
|
24
|
+
loop do
|
25
|
+
# get the current time
|
26
|
+
t = Time.now.to_i
|
27
|
+
|
28
|
+
# iterate over each event and trigger any that are due
|
29
|
+
@events.each do |event|
|
30
|
+
if t >= event.at
|
31
|
+
self.trigger(event)
|
32
|
+
@events.delete(event)
|
33
|
+
else
|
34
|
+
break
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# sleep until next check
|
39
|
+
sleep INTERVAL
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Create and register a new TimerEvent with the given parameters
|
45
|
+
def register(watch, condition, command)
|
46
|
+
@events << TimerEvent.new(watch, condition, command)
|
47
|
+
@events.sort! { |x, y| x.at <=> y.at }
|
48
|
+
end
|
49
|
+
|
50
|
+
def trigger(event)
|
51
|
+
timer = self
|
52
|
+
|
53
|
+
Thread.new do
|
54
|
+
w = event.watch
|
55
|
+
c = event.condition
|
56
|
+
|
57
|
+
w.mutex.synchronize do
|
58
|
+
if c.test
|
59
|
+
puts w.name + ' ' + c.class.name + ' [ok]'
|
60
|
+
else
|
61
|
+
puts w.name + ' ' + c.class.name + ' [fail]'
|
62
|
+
c.after
|
63
|
+
w.action(event.command, c)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# reschedule
|
68
|
+
timer.register(w, c, event.command)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
# Join the timer thread
|
73
|
+
def join
|
74
|
+
@timer.join
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
data/lib/god/watch.rb
ADDED
@@ -0,0 +1,151 @@
|
|
1
|
+
module God
|
2
|
+
|
3
|
+
class Watch < Base
|
4
|
+
# config
|
5
|
+
attr_accessor :name, :start, :stop, :restart, :interval, :grace
|
6
|
+
|
7
|
+
# api
|
8
|
+
attr_accessor :behaviors, :conditions
|
9
|
+
|
10
|
+
# internal
|
11
|
+
attr_accessor :mutex
|
12
|
+
|
13
|
+
#
|
14
|
+
def initialize(meddle)
|
15
|
+
@meddle = meddle
|
16
|
+
|
17
|
+
# no grace period by default
|
18
|
+
self.grace = 0
|
19
|
+
|
20
|
+
# keep track of which action each condition belongs to
|
21
|
+
@action = nil
|
22
|
+
|
23
|
+
self.behaviors = []
|
24
|
+
|
25
|
+
# the list of conditions for each action
|
26
|
+
self.conditions = {:start => [],
|
27
|
+
:restart => []}
|
28
|
+
|
29
|
+
# mutex
|
30
|
+
self.mutex = Mutex.new
|
31
|
+
end
|
32
|
+
|
33
|
+
def behavior(kind)
|
34
|
+
# create the behavior
|
35
|
+
begin
|
36
|
+
b = Behavior.generate(kind)
|
37
|
+
rescue NoSuchBehaviorError => e
|
38
|
+
abort e.message
|
39
|
+
end
|
40
|
+
|
41
|
+
# send to block so config can set attributes
|
42
|
+
yield(b) if block_given?
|
43
|
+
|
44
|
+
# abort if the Behavior is invalid, the Behavior will have printed
|
45
|
+
# out its own error messages by now
|
46
|
+
abort unless b.valid?
|
47
|
+
|
48
|
+
self.behaviors << b
|
49
|
+
end
|
50
|
+
|
51
|
+
def start_if
|
52
|
+
@action = :start
|
53
|
+
yield(self)
|
54
|
+
@action = nil
|
55
|
+
end
|
56
|
+
|
57
|
+
def restart_if
|
58
|
+
@action = :restart
|
59
|
+
yield(self)
|
60
|
+
@action = nil
|
61
|
+
end
|
62
|
+
|
63
|
+
# Instantiate a Condition of type +kind+ and pass it into the optional
|
64
|
+
# block. Attributes of the condition must be set in the config file
|
65
|
+
def condition(kind)
|
66
|
+
# must be in a _if block
|
67
|
+
unless @action
|
68
|
+
abort "Watch#condition can only be called from inside a start_if block"
|
69
|
+
end
|
70
|
+
|
71
|
+
# create the condition
|
72
|
+
begin
|
73
|
+
c = Condition.generate(kind)
|
74
|
+
rescue NoSuchConditionError => e
|
75
|
+
abort e.message
|
76
|
+
end
|
77
|
+
|
78
|
+
# send to block so config can set attributes
|
79
|
+
yield(c) if block_given?
|
80
|
+
|
81
|
+
# call prepare on the condition
|
82
|
+
c.prepare
|
83
|
+
|
84
|
+
# abort if the Condition is invalid, the Condition will have printed
|
85
|
+
# out its own error messages by now
|
86
|
+
unless c.valid?
|
87
|
+
abort
|
88
|
+
end
|
89
|
+
|
90
|
+
# inherit interval from meddle if no poll condition specific interval was set
|
91
|
+
if c.kind_of?(PollCondition) && !c.interval
|
92
|
+
if self.interval
|
93
|
+
c.interval = self.interval
|
94
|
+
else
|
95
|
+
abort "No interval set for Condition '#{c.class.name}' in Watch '#{self.name}', and no default Watch interval from which to inherit"
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
self.conditions[@action] << c
|
100
|
+
end
|
101
|
+
|
102
|
+
# Schedule all poll conditions and register all condition events
|
103
|
+
def monitor
|
104
|
+
[:start, :restart].each do |cmd|
|
105
|
+
self.conditions[cmd].each do |c|
|
106
|
+
@meddle.timer.register(self, c, cmd) if c.kind_of?(PollCondition)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def action(a, c)
|
112
|
+
case a
|
113
|
+
when :start
|
114
|
+
puts self.start
|
115
|
+
call_action(c, :start, self.start)
|
116
|
+
sleep(self.grace)
|
117
|
+
when :restart
|
118
|
+
if self.restart
|
119
|
+
puts self.restart
|
120
|
+
call_action(c, :restart, self.restart)
|
121
|
+
else
|
122
|
+
action(:stop, c)
|
123
|
+
action(:start, c)
|
124
|
+
end
|
125
|
+
sleep(self.grace)
|
126
|
+
when :stop
|
127
|
+
puts self.stop
|
128
|
+
call_action(c, :stop, self.stop)
|
129
|
+
sleep(self.grace)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
def call_action(condition, action, command)
|
134
|
+
# before
|
135
|
+
(self.behaviors + [condition]).each { |b| b.send("before_#{action}") }
|
136
|
+
|
137
|
+
# action
|
138
|
+
if command.kind_of?(String)
|
139
|
+
# string command
|
140
|
+
system(command)
|
141
|
+
else
|
142
|
+
# lambda command
|
143
|
+
command.call
|
144
|
+
end
|
145
|
+
|
146
|
+
# after
|
147
|
+
(self.behaviors + [condition]).each { |b| b.send("after_#{action}") }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
end
|