god 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/lib/god/watch.rb CHANGED
@@ -1,11 +1,13 @@
1
1
  module God
2
2
 
3
3
  class Watch < Base
4
+ VALID_STATES = [:init, :up, :start, :restart]
5
+
4
6
  # config
5
- attr_accessor :name, :start, :stop, :restart, :interval, :grace
7
+ attr_accessor :name, :state, :start, :stop, :restart, :interval, :grace
6
8
 
7
9
  # api
8
- attr_accessor :behaviors, :conditions
10
+ attr_accessor :behaviors, :metrics
9
11
 
10
12
  # internal
11
13
  attr_accessor :mutex
@@ -13,18 +15,18 @@ module God
13
15
  #
14
16
  def initialize(meddle)
15
17
  @meddle = meddle
16
-
18
+
17
19
  # no grace period by default
18
20
  self.grace = 0
19
21
 
20
- # keep track of which action each condition belongs to
21
- @action = nil
22
-
22
+ # the list of behaviors
23
23
  self.behaviors = []
24
24
 
25
25
  # the list of conditions for each action
26
- self.conditions = {:start => [],
27
- :restart => []}
26
+ self.metrics = {:init => [],
27
+ :start => [],
28
+ :restart => [],
29
+ :up => []}
28
30
 
29
31
  # mutex
30
32
  self.mutex = Mutex.new
@@ -48,67 +50,89 @@ module God
48
50
  self.behaviors << b
49
51
  end
50
52
 
53
+ ###########################################################################
54
+ #
55
+ # Advanced mode
56
+ #
57
+ ###########################################################################
58
+
59
+ # Define a transition handler which consists of a set of conditions
60
+ def transition(start_states, end_states)
61
+ # convert to into canonical hash form
62
+ canonical_end_states = canonical_hash_form(end_states)
63
+
64
+ # for each start state do
65
+ Array(start_states).each do |start_state|
66
+ # validate start state
67
+ unless VALID_STATES.include?(start_state)
68
+ abort "Invalid state :#{start_state}. Must be one of the symbols #{VALID_STATES.map{|x| ":#{x}"}.join(', ')}"
69
+ end
70
+
71
+ # create a new metric to hold the watch, end states, and conditions
72
+ m = Metric.new(self, canonical_end_states)
73
+
74
+ # let the config file define some conditions on the metric
75
+ yield(m)
76
+
77
+ # record the metric
78
+ self.metrics[start_state] << m
79
+ end
80
+ end
81
+
82
+ ###########################################################################
83
+ #
84
+ # Simple mode
85
+ #
86
+ ###########################################################################
87
+
51
88
  def start_if
52
- @action = :start
53
- yield(self)
54
- @action = nil
89
+ self.transition(:up, :start) do |on|
90
+ yield(on)
91
+ end
55
92
  end
56
93
 
57
94
  def restart_if
58
- @action = :restart
59
- yield(self)
60
- @action = nil
95
+ self.transition(:up, :restart) do |on|
96
+ yield(on)
97
+ end
61
98
  end
62
99
 
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"
100
+ ###########################################################################
101
+ #
102
+ # Lifecycle
103
+ #
104
+ ###########################################################################
105
+
106
+ # Schedule all poll conditions and register all condition events
107
+ def monitor
108
+ # start monitoring at the first available of the init or up states
109
+ if !self.metrics[:init].empty?
110
+ self.move(:init)
111
+ else
112
+ self.move(:up)
69
113
  end
70
-
71
- # create the condition
72
- begin
73
- c = Condition.generate(kind)
74
- rescue NoSuchConditionError => e
75
- abort e.message
114
+ end
115
+
116
+ # Move from one state to another
117
+ def move(to_state)
118
+ puts "move '#{self.state}' to '#{to_state}'"
119
+
120
+ # cleanup from current state
121
+ if from_state = self.state
122
+ self.metrics[from_state].each { |m| m.disable }
76
123
  end
77
124
 
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
125
+ # perform action (if available)
126
+ self.action(to_state)
83
127
 
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
128
+ # move to new state
129
+ self.metrics[to_state].each { |m| m.enable }
98
130
 
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
131
+ # set state
132
+ self.state = to_state
109
133
  end
110
134
 
111
- def action(a, c)
135
+ def action(a, c = nil)
112
136
  case a
113
137
  when :start
114
138
  puts self.start
@@ -132,7 +156,9 @@ module God
132
156
 
133
157
  def call_action(condition, action, command)
134
158
  # before
135
- (self.behaviors + [condition]).each { |b| b.send("before_#{action}") }
159
+ before_items = self.behaviors
160
+ before_items += [condition] if condition
161
+ before_items.each { |b| b.send("before_#{action}") }
136
162
 
137
163
  # action
138
164
  if command.kind_of?(String)
@@ -144,7 +170,13 @@ module God
144
170
  end
145
171
 
146
172
  # after
147
- (self.behaviors + [condition]).each { |b| b.send("after_#{action}") }
173
+ after_items = self.behaviors
174
+ after_items += [condition] if condition
175
+ after_items.each { |b| b.send("after_#{action}") }
176
+ end
177
+
178
+ def canonical_hash_form(to)
179
+ to.instance_of?(Symbol) ? {true => to} : to
148
180
  end
149
181
  end
150
182
 
data/test/configs/real.rb CHANGED
@@ -21,11 +21,8 @@ God.meddle do |god|
21
21
 
22
22
  # start if process is not running
23
23
  w.start_if do |start|
24
- # start.condition(:process_exits) do |c|
25
- # c.pid_file = pid_file
26
- # end
27
-
28
- start.condition(:process_not_running) do |c|
24
+ start.condition(:process_running) do |c|
25
+ c.running = false
29
26
  c.pid_file = pid_file
30
27
  end
31
28
  end
@@ -0,0 +1,75 @@
1
+ if $0 == __FILE__
2
+ require File.join(File.dirname(__FILE__), *%w[.. .. lib god])
3
+ end
4
+
5
+ RAILS_ROOT = "/Users/tom/dev/git/helloworld"
6
+
7
+ God.meddle do |god|
8
+ god.watch do |w|
9
+ w.name = "local-3000"
10
+ w.interval = 5 # seconds
11
+ w.start = "mongrel_rails start -P ./log/mongrel.pid -c #{RAILS_ROOT} -p 3001 -d"
12
+ w.stop = "mongrel_rails stop -P ./log/mongrel.pid -c #{RAILS_ROOT}"
13
+
14
+ pid_file = File.join(RAILS_ROOT, "log/mongrel.pid")
15
+
16
+ # clean pid files before start if necessary
17
+ w.behavior(:clean_pid_file) do |b|
18
+ b.pid_file = pid_file
19
+ end
20
+
21
+ # determine the state on startup
22
+ w.transition(:init, { true => :up, false => :start }) do |on|
23
+ on.condition(:process_running) do |c|
24
+ c.running = true
25
+ c.pid_file = pid_file
26
+ end
27
+ end
28
+
29
+ # determine when process has finished starting
30
+ w.transition([:start, :restart], :up) do |on|
31
+ on.condition(:process_running) do |c|
32
+ c.running = true
33
+ c.pid_file = pid_file
34
+ end
35
+ end
36
+
37
+ # start if process is not running
38
+ w.transition(:up, :start) do |on|
39
+ on.condition(:process_exits) do |c|
40
+ c.pid_file = pid_file
41
+ end
42
+ end
43
+
44
+ # restart if memory or cpu is too high
45
+ w.transition(:up, :restart) do |on|
46
+ on.condition(:memory_usage) do |c|
47
+ c.interval = 20
48
+ c.pid_file = pid_file
49
+ c.above = (50 * 1024) # 50mb
50
+ c.times = [3, 5]
51
+ end
52
+
53
+ on.condition(:cpu_usage) do |c|
54
+ c.interval = 10
55
+ c.pid_file = pid_file
56
+ c.above = 10 # percent
57
+ c.times = [3, 5]
58
+ end
59
+ end
60
+ end
61
+
62
+ # clear old session files
63
+ # god.watch do |w|
64
+ # w.name = "local-session-cleanup"
65
+ # w.start = lambda do
66
+ # Dir["#{RAILS_ROOT}/tmp/sessions/ruby_sess.*"].select do |f|
67
+ # File.mtime(f) < Time.now - (7 * 24 * 60 * 60)
68
+ # end.each { |f| File.delete(f) }
69
+ # end
70
+ #
71
+ # w.start_if do |start|
72
+ # start.condition(:always)
73
+ # end
74
+ # end
75
+ end
data/test/helper.rb CHANGED
@@ -1,3 +1,4 @@
1
+ $:.unshift File.join(File.dirname(__FILE__), *%w[.. ext kqueue_handler])
1
2
  require File.join(File.dirname(__FILE__), *%w[.. lib god])
2
3
 
3
4
  require 'test/unit'
@@ -2,7 +2,7 @@ require File.dirname(__FILE__) + '/helper'
2
2
 
3
3
  class TestCondition < Test::Unit::TestCase
4
4
  def test_generate_should_return_an_object_corresponding_to_the_given_type
5
- assert_equal Conditions::ProcessNotRunning, Condition.generate(:process_not_running).class
5
+ assert_equal Conditions::ProcessRunning, Condition.generate(:process_running).class
6
6
  end
7
7
 
8
8
  def test_generate_should_raise_on_invalid_type
data/test/test_god.rb CHANGED
@@ -2,14 +2,15 @@ require File.dirname(__FILE__) + '/helper'
2
2
 
3
3
  class TestGod < Test::Unit::TestCase
4
4
  def test_should_create_new_meddle
5
- Meddle.expects(:new).with(:port => 1).returns(mock(:monitor => true, :timer => stub(:join => nil)))
5
+ Meddle.expects(:new).with(:port => 1).returns(mock(:monitor => true))
6
+ Timer.expects(:get).returns(stub(:join => nil)).times(2)
6
7
 
7
8
  God.meddle(:port => 1) {}
8
9
  end
9
10
 
10
11
  def test_should_start_monitoring
11
12
  Meddle.any_instance.expects(:monitor)
12
- Meddle.any_instance.expects(:timer).returns(stub(:join => nil))
13
+ Timer.expects(:get).returns(stub(:join => nil)).times(2)
13
14
  God.meddle {}
14
15
  end
15
16
  end
@@ -0,0 +1,60 @@
1
+ require File.dirname(__FILE__) + '/helper'
2
+
3
+ class TestMetric < Test::Unit::TestCase
4
+ def setup
5
+ @metric = Metric.new(stub(:interval => 10), nil)
6
+ end
7
+
8
+ # watch
9
+
10
+ def test_watch
11
+ w = stub()
12
+ m = Metric.new(w, nil)
13
+ assert_equal w, m.watch
14
+ end
15
+
16
+ # destination
17
+
18
+ def test_destination
19
+ d = stub()
20
+ m = Metric.new(nil, d)
21
+ assert_equal d, m.destination
22
+ end
23
+
24
+ # condition
25
+
26
+ def test_condition_should_be_block_optional
27
+ @metric.condition(:fake_poll_condition)
28
+ assert_equal 1, @metric.conditions.size
29
+ end
30
+
31
+ def test_poll_condition_should_inherit_interval_from_watch_if_not_specified
32
+ @metric.condition(:fake_poll_condition)
33
+ assert_equal 10, @metric.conditions.first.interval
34
+ end
35
+
36
+ def test_poll_condition_should_abort_if_no_interval_and_no_watch_interval
37
+ metric = Metric.new(stub(:name => 'foo', :interval => nil), nil)
38
+
39
+ assert_raise AbortCalledError do
40
+ metric.condition(:fake_poll_condition)
41
+ end
42
+ end
43
+
44
+ def test_condition_should_allow_generation_of_subclasses_of_poll_or_event
45
+ metric = Metric.new(stub(:name => 'foo', :interval => 10), nil)
46
+
47
+ assert_nothing_raised do
48
+ metric.condition(:fake_poll_condition)
49
+ metric.condition(:fake_event_condition)
50
+ end
51
+ end
52
+
53
+ def test_condition_should_abort_if_not_subclass_of_poll_or_event
54
+ metric = Metric.new(stub(:name => 'foo', :interval => 10), nil)
55
+
56
+ assert_raise AbortCalledError do
57
+ metric.condition(:fake_condition) { |c| }
58
+ end
59
+ end
60
+ end
data/test/test_timer.rb CHANGED
@@ -2,42 +2,52 @@ require File.dirname(__FILE__) + '/helper'
2
2
 
3
3
  class TestTimer < Test::Unit::TestCase
4
4
  def setup
5
- @t = Timer.new
5
+ Timer.reset
6
+ @t = Timer.get
6
7
  end
7
8
 
8
9
  def test_new_timer_should_have_no_events
9
10
  assert_equal 0, @t.events.size
10
11
  end
11
12
 
12
- def test_register_should_queue_event
13
+ def test_schedule_should_queue_event
13
14
  Time.stubs(:now).returns(0)
14
15
 
15
16
  w = Watch.new(nil)
16
- @t.register(w, stub(:interval => 20), nil)
17
+ @t.schedule(stub(:interval => 20))
17
18
 
18
19
  assert_equal 1, @t.events.size
19
- assert_equal w, @t.events.first.watch
20
20
  end
21
21
 
22
22
  def test_timer_should_remove_expired_events
23
- @t.register(nil, stub(:interval => 0), nil)
23
+ @t.schedule(stub(:interval => 0))
24
24
  sleep(0.3)
25
25
  assert_equal 0, @t.events.size
26
26
  end
27
27
 
28
28
  def test_timer_should_remove_only_expired_events
29
- @t.register(nil, stub(:interval => 0), nil)
30
- @t.register(nil, stub(:interval => 1000), nil)
29
+ @t.schedule(stub(:interval => 0))
30
+ @t.schedule(stub(:interval => 1000))
31
31
  sleep(0.3)
32
32
  assert_equal 1, @t.events.size
33
33
  end
34
34
 
35
35
  def test_timer_should_sort_timer_events
36
- @t.register(nil, stub(:interval => 1000), nil)
37
- @t.register(nil, stub(:interval => 800), nil)
38
- @t.register(nil, stub(:interval => 900), nil)
39
- @t.register(nil, stub(:interval => 100), nil)
36
+ @t.schedule(stub(:interval => 1000))
37
+ @t.schedule(stub(:interval => 800))
38
+ @t.schedule(stub(:interval => 900))
39
+ @t.schedule(stub(:interval => 100))
40
40
  sleep(0.3)
41
41
  assert_equal [100, 800, 900, 1000], @t.events.map { |x| x.condition.interval }
42
42
  end
43
+
44
+ def test_unschedule_should_remove_conditions
45
+ a = stub()
46
+ b = stub()
47
+ @t.schedule(a, 100)
48
+ @t.schedule(b, 200)
49
+ assert_equal 2, @t.events.size
50
+ @t.unschedule(a)
51
+ assert_equal 1, @t.events.size
52
+ end
43
53
  end