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.
@@ -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
@@ -0,0 +1,17 @@
1
+ module God
2
+ module Conditions
3
+
4
+ class Timeline < Array
5
+ def initialize(max_size)
6
+ super()
7
+ @max_size = max_size
8
+ end
9
+
10
+ def push(val)
11
+ unshift(val)
12
+ pop if size > @max_size
13
+ end
14
+ end
15
+
16
+ end
17
+ end
@@ -0,0 +1,12 @@
1
+ module God
2
+
3
+ class AbstractMethodNotOverriddenError < StandardError
4
+ end
5
+
6
+ class NoSuchConditionError < StandardError
7
+ end
8
+
9
+ class NoSuchBehaviorError < StandardError
10
+ end
11
+
12
+ end
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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