god 0.1.0
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.
- 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
|