god 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (62) hide show
  1. data/History.txt +26 -2
  2. data/Manifest.txt +19 -5
  3. data/Rakefile +7 -2
  4. data/bin/god +131 -11
  5. data/examples/events.god +52 -0
  6. data/examples/gravatar.god +29 -35
  7. data/ext/god/extconf.rb +49 -2
  8. data/ext/god/kqueue_handler.c +2 -2
  9. data/ext/god/netlink_handler.c +17 -4
  10. data/lib/god.rb +127 -25
  11. data/lib/god/behavior.rb +8 -3
  12. data/lib/god/behaviors/clean_pid_file.rb +2 -8
  13. data/lib/god/condition.rb +6 -5
  14. data/lib/god/conditions/cpu_usage.rb +4 -4
  15. data/lib/god/conditions/lambda.rb +19 -0
  16. data/lib/god/conditions/memory_usage.rb +4 -4
  17. data/lib/god/conditions/process_exits.rb +5 -7
  18. data/lib/god/conditions/process_running.rb +4 -4
  19. data/lib/god/errors.rb +3 -0
  20. data/lib/god/event_handler.rb +28 -3
  21. data/lib/god/event_handlers/dummy_handler.rb +13 -0
  22. data/lib/god/event_handlers/kqueue_handler.rb +2 -0
  23. data/lib/god/event_handlers/netlink_handler.rb +2 -0
  24. data/lib/god/hub.rb +40 -23
  25. data/lib/god/metric.rb +4 -4
  26. data/lib/god/process.rb +105 -0
  27. data/lib/god/registry.rb +28 -0
  28. data/lib/god/server.rb +3 -4
  29. data/lib/god/sugar.rb +47 -0
  30. data/lib/god/system/process.rb +1 -2
  31. data/lib/god/timer.rb +13 -6
  32. data/lib/god/watch.rb +61 -29
  33. data/test/configs/child_events/child_events.god +25 -0
  34. data/test/configs/child_events/simple_server.rb +3 -0
  35. data/test/configs/child_polls/child_polls.god +15 -0
  36. data/test/configs/child_polls/simple_server.rb +3 -0
  37. data/test/configs/daemon_events/daemon_events.god +30 -0
  38. data/test/configs/daemon_events/simple_server.rb +6 -0
  39. data/test/configs/real.rb +47 -49
  40. data/test/configs/test.rb +52 -62
  41. data/test/helper.rb +44 -14
  42. data/test/test_behavior.rb +10 -2
  43. data/test/test_condition.rb +19 -3
  44. data/test/test_conditions_process_running.rb +42 -0
  45. data/test/test_event_handler.rb +73 -0
  46. data/test/test_god.rb +206 -9
  47. data/test/test_handlers_kqueue_handler.rb +12 -0
  48. data/test/test_hub.rb +157 -0
  49. data/test/test_metric.rb +30 -2
  50. data/test/test_process.rb +84 -0
  51. data/test/test_registry.rb +14 -0
  52. data/test/test_server.rb +3 -2
  53. data/test/test_sugar.rb +42 -0
  54. data/test/test_system_process.rb +1 -1
  55. data/test/test_timer.rb +8 -1
  56. data/test/test_watch.rb +137 -2
  57. metadata +28 -17
  58. data/examples/local.god +0 -60
  59. data/ext/god/Makefile +0 -149
  60. data/lib/god/base.rb +0 -13
  61. data/lib/god/meddle.rb +0 -38
  62. data/test/test_meddle.rb +0 -46
@@ -9,4 +9,7 @@ module God
9
9
  class NoSuchBehaviorError < StandardError
10
10
  end
11
11
 
12
+ class InvalidCommandError < StandardError
13
+ end
14
+
12
15
  end
@@ -2,9 +2,34 @@ module God
2
2
  class EventHandler
3
3
  @@actions = {}
4
4
  @@handler = nil
5
+ @@loaded = false
5
6
 
6
- def self.handler=(value)
7
- @@handler = value
7
+ def self.loaded?
8
+ @@loaded
9
+ end
10
+
11
+ def self.event_system
12
+ @@handler::EVENT_SYSTEM
13
+ end
14
+
15
+ def self.load
16
+ begin
17
+ case RUBY_PLATFORM
18
+ when /darwin/i, /bsd/i
19
+ require 'god/event_handlers/kqueue_handler'
20
+ @@handler = KQueueHandler
21
+ when /linux/i
22
+ require 'god/event_handlers/netlink_handler'
23
+ @@handler = NetlinkHandler
24
+ else
25
+ raise NotImplementedError, "Platform not supported for EventHandler"
26
+ end
27
+ @@loaded = true
28
+ rescue Exception
29
+ require 'god/event_handlers/dummy_handler'
30
+ @@handler = DummyHandler
31
+ @@loaded = false
32
+ end
8
33
  end
9
34
 
10
35
  def self.register(pid, event, &block)
@@ -26,7 +51,7 @@ module God
26
51
  end
27
52
 
28
53
  def self.call(pid, event)
29
- @@actions[pid][event].call
54
+ @@actions[pid][event].call if watching_pid?(pid) && @@actions[pid][event]
30
55
  end
31
56
 
32
57
  def self.watching_pid?(pid)
@@ -0,0 +1,13 @@
1
+ module God
2
+ class DummyHandler
3
+ EVENT_SYSTEM = "none"
4
+
5
+ def self.register_process
6
+ raise NotImplementedError
7
+ end
8
+
9
+ def self.handle_events
10
+ raise NotImplementedError
11
+ end
12
+ end
13
+ end
@@ -2,6 +2,8 @@ require 'kqueue_handler_ext'
2
2
 
3
3
  module God
4
4
  class KQueueHandler
5
+ EVENT_SYSTEM = "kqueue"
6
+
5
7
  def self.register_process(pid, events)
6
8
  monitor_process(pid, events_mask(events))
7
9
  end
@@ -2,6 +2,8 @@ require 'netlink_handler_ext'
2
2
 
3
3
  module God
4
4
  class NetlinkHandler
5
+ EVENT_SYSTEM = "netlink"
6
+
5
7
  def self.register_process(pid, events)
6
8
  # netlink doesn't need to do this
7
9
  # it just reads from the eventhandler actions to see if the pid
@@ -1,14 +1,18 @@
1
1
  module God
2
2
 
3
3
  class Hub
4
- # directory to hold conditions and their corresponding metric
5
- # key: condition
6
- # val: metric
7
- @@directory = {}
4
+ class << self
5
+ # directory to hold conditions and their corresponding metric
6
+ # key: condition
7
+ # val: metric
8
+ attr_accessor :directory
9
+ end
10
+
11
+ self.directory = {}
8
12
 
9
13
  def self.attach(condition, metric)
10
14
  # add the condition to the directory
11
- @@directory[condition] = metric
15
+ self.directory[condition] = metric
12
16
 
13
17
  # schedule poll condition
14
18
  # register event condition
@@ -21,7 +25,7 @@ module God
21
25
 
22
26
  def self.detach(condition)
23
27
  # remove the condition from the directory
24
- @@directory.delete(condition)
28
+ self.directory.delete(condition)
25
29
 
26
30
  # unschedule any pending polls
27
31
  Timer.get.unschedule(condition)
@@ -42,37 +46,50 @@ module God
42
46
 
43
47
  def self.handle_poll(condition)
44
48
  Thread.new do
45
- metric = @@directory[condition]
46
- watch = metric.watch
47
-
48
- watch.mutex.synchronize do
49
- result = condition.test
49
+ begin
50
+ metric = self.directory[condition]
51
+
52
+ # it's possible that the timer will trigger an event before it can be cleared
53
+ # by an exiting metric, in which case it should be ignored
54
+ return if metric.nil?
50
55
 
51
- puts watch.name + ' ' + condition.class.name + " [#{result}]"
56
+ watch = metric.watch
57
+
58
+ watch.mutex.synchronize do
59
+ result = condition.test
52
60
 
53
- condition.after
61
+ msg = watch.name + ' ' + condition.class.name + " [#{result}] " + metric.destination.inspect
62
+ Syslog.debug(msg)
63
+ puts msg
54
64
 
55
- p metric.destination
65
+ condition.after
56
66
 
57
- if dest = metric.destination[result]
58
- watch.move(dest)
59
- else
60
- # reschedule
61
- Timer.get.schedule(condition)
67
+ dest = metric.destination[result]
68
+ if dest
69
+ watch.move(dest)
70
+ else
71
+ # reschedule
72
+ Timer.get.schedule(condition)
73
+ end
62
74
  end
75
+ rescue => e
76
+ message = format("Unhandled exception (%s): %s\n%s",
77
+ e.class, e.message, e.backtrace.join("\n"))
78
+ Syslog.crit message
79
+ abort message
63
80
  end
64
81
  end
65
82
  end
66
83
 
67
84
  def self.handle_event(condition)
68
85
  Thread.new do
69
- metric = @@directory[condition]
86
+ metric = self.directory[condition]
70
87
  watch = metric.watch
71
88
 
72
89
  watch.mutex.synchronize do
73
- puts watch.name + ' ' + condition.class.name + " [true]"
74
-
75
- p metric.destination
90
+ msg = watch.name + ' ' + condition.class.name + " [true] " + metric.destination.inspect
91
+ Syslog.debug(msg)
92
+ puts msg
76
93
 
77
94
  dest = metric.destination[true]
78
95
  watch.move(dest)
@@ -1,6 +1,6 @@
1
1
  module God
2
2
 
3
- class Metric < Base
3
+ class Metric
4
4
  attr_accessor :watch, :destination, :conditions
5
5
 
6
6
  def initialize(watch, destination)
@@ -14,7 +14,7 @@ module God
14
14
  def condition(kind)
15
15
  # create the condition
16
16
  begin
17
- c = Condition.generate(kind)
17
+ c = Condition.generate(kind, self.watch)
18
18
  rescue NoSuchConditionError => e
19
19
  abort e.message
20
20
  end
@@ -28,10 +28,10 @@ module God
28
28
  # abort if the Condition is invalid, the Condition will have printed
29
29
  # out its own error messages by now
30
30
  unless c.valid?
31
- abort
31
+ abort "Exiting on invalid condition"
32
32
  end
33
33
 
34
- # inherit interval from meddle if no poll condition specific interval was set
34
+ # inherit interval from watch if no poll condition specific interval was set
35
35
  if c.kind_of?(PollCondition) && !c.interval
36
36
  if self.watch.interval
37
37
  c.interval = self.watch.interval
@@ -0,0 +1,105 @@
1
+ require 'fileutils'
2
+
3
+ module God
4
+ class Process
5
+ WRITES_PID = [:start, :restart]
6
+
7
+ attr_accessor :name, :uid, :gid, :start, :stop, :restart
8
+
9
+ def initialize(options={})
10
+ options.each do |k,v|
11
+ send("#{k}=", v)
12
+ end
13
+
14
+ @tracking_pid = false
15
+ end
16
+
17
+ # DON'T USE THIS INTERNALLY. Use the instance variable. -- Kev
18
+ # No really, trust me. Use the instance variable.
19
+ def pid_file=(value)
20
+ @tracking_pid = false
21
+ @pid_file = value
22
+ end
23
+
24
+ def pid_file
25
+ if @pid_file.nil?
26
+ @tracking_pid = true
27
+ @pid_file = default_pid_file
28
+ end
29
+ @pid_file
30
+ end
31
+
32
+ def start!
33
+ call_action(:start)
34
+ end
35
+
36
+ def stop!
37
+ call_action(:stop)
38
+ end
39
+
40
+ def restart!
41
+ call_action(:restart)
42
+ end
43
+
44
+ def call_action(action)
45
+ command = send(action)
46
+ if command.kind_of?(String)
47
+ # Make pid directory
48
+ unless test(?d, God.pid_file_directory)
49
+ begin
50
+ FileUtils.mkdir_p(God.pid_file_directory)
51
+ rescue Errno::EACCES => e
52
+ abort"Failed to create pid file directory: #{e.message}"
53
+ end
54
+ end
55
+
56
+ unless test(?w, God.pid_file_directory)
57
+ abort "The pid file directory (#{God.pid_file_directory}) is not writable by #{Etc.getlogin}"
58
+ end
59
+
60
+ # string command
61
+ # fork/exec to setuid/gid
62
+ r, w = IO.pipe
63
+ opid = fork do
64
+ STDOUT.reopen(w)
65
+ r.close
66
+ pid = fork do
67
+ ::Process.setsid
68
+ ::Process::Sys.setgid(Etc.getgrnam(self.gid).gid) if self.gid
69
+ ::Process::Sys.setuid(Etc.getpwnam(self.uid).uid) if self.uid
70
+ Dir.chdir "/"
71
+ $0 = command
72
+ STDIN.reopen "/dev/null"
73
+ STDOUT.reopen "/dev/null", "a"
74
+ STDERR.reopen STDOUT
75
+ exec command
76
+ end
77
+ puts pid.to_s
78
+ end
79
+
80
+ ::Process.waitpid(opid, 0)
81
+ w.close
82
+ pid = r.gets.chomp
83
+
84
+ if @tracking_pid or (@pid_file.nil? and WRITES_PID.include?(action))
85
+ File.open(default_pid_file, 'w') do |f|
86
+ f.write pid
87
+ end
88
+
89
+ @tracking_pid = true
90
+ @pid_file = default_pid_file
91
+ end
92
+
93
+ elsif command.kind_of?(Proc)
94
+ # lambda command
95
+ command.call
96
+ else
97
+ raise NotImplementedError
98
+ end
99
+ end
100
+
101
+ def default_pid_file
102
+ File.join(God.pid_file_directory, "#{self.name}.pid")
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,28 @@
1
+ module God
2
+ def self.registry
3
+ @registry ||= Registry.new
4
+ end
5
+
6
+ class Registry
7
+ def initialize
8
+ @storage = {}
9
+ end
10
+
11
+ def add(item)
12
+ # raise TypeError unless item.is_a? God::Process
13
+ @storage[item.name] = item
14
+ end
15
+
16
+ def size
17
+ @storage.size
18
+ end
19
+
20
+ def [](name)
21
+ @storage[name]
22
+ end
23
+
24
+ def reset
25
+ @storage.clear
26
+ end
27
+ end
28
+ end
@@ -7,15 +7,14 @@ module God
7
7
  class Server
8
8
  attr_reader :host, :port
9
9
 
10
- def initialize(meddle = nil, host = nil, port = nil)
11
- @meddle = meddle
10
+ def initialize(host = nil, port = nil)
12
11
  @host = host
13
- @port = port || 7777
12
+ @port = port || 17165
14
13
  start
15
14
  end
16
15
 
17
16
  def method_missing(*args, &block)
18
- @meddle.send(*args, &block)
17
+ God.send(*args, &block)
19
18
  end
20
19
 
21
20
  private
@@ -0,0 +1,47 @@
1
+ class Numeric
2
+ def seconds
3
+ self
4
+ end
5
+
6
+ alias :second :seconds
7
+
8
+ def minutes
9
+ self * 60
10
+ end
11
+
12
+ alias :minute :minutes
13
+
14
+ def hours
15
+ self * 3600
16
+ end
17
+
18
+ alias :hour :hours
19
+
20
+ def days
21
+ self * 86400
22
+ end
23
+
24
+ alias :day :days
25
+
26
+ def kilobytes
27
+ self
28
+ end
29
+
30
+ alias :kilobyte :kilobytes
31
+
32
+ def megabytes
33
+ self * 1024
34
+ end
35
+
36
+ alias :megabyte :megabytes
37
+
38
+ def gigabytes
39
+ self * (1024 ** 2)
40
+ end
41
+
42
+ alias :gigabyte :gigabytes
43
+
44
+ def percent
45
+ self
46
+ end
47
+ end
@@ -8,8 +8,7 @@ module God
8
8
 
9
9
  # Return true if this process is running, false otherwise
10
10
  def exists?
11
- cmd_name = RUBY_PLATFORM =~ /solaris/i ? "args" : "command"
12
- !ps_string(cmd_name).empty?
11
+ system("kill -0 #{@pid} &> /dev/null")
13
12
  end
14
13
 
15
14
  # Memory usage in kilobytes (resident set size)
@@ -9,10 +9,10 @@ module God
9
9
  end
10
10
  end
11
11
 
12
- class Timer < Base
12
+ class Timer
13
13
  INTERVAL = 0.25
14
14
 
15
- attr_reader :events
15
+ attr_reader :events, :timer
16
16
 
17
17
  @@timer = nil
18
18
 
@@ -27,6 +27,7 @@ module God
27
27
  # Start the scheduler loop to handle events
28
28
  def initialize
29
29
  @events = []
30
+ @mutex = Mutex.new
30
31
 
31
32
  @timer = Thread.new do
32
33
  loop do
@@ -37,7 +38,9 @@ module God
37
38
  @events.each do |event|
38
39
  if t >= event.at
39
40
  self.trigger(event)
40
- @events.delete(event)
41
+ @mutex.synchronize do
42
+ @events.delete(event)
43
+ end
41
44
  else
42
45
  break
43
46
  end
@@ -51,13 +54,17 @@ module God
51
54
 
52
55
  # Create and register a new TimerEvent with the given parameters
53
56
  def schedule(condition, interval = condition.interval)
54
- @events << TimerEvent.new(condition, interval)
55
- @events.sort! { |x, y| x.at <=> y.at }
57
+ @mutex.synchronize do
58
+ @events << TimerEvent.new(condition, interval)
59
+ @events.sort! { |x, y| x.at <=> y.at }
60
+ end
56
61
  end
57
62
 
58
63
  # Remove any TimerEvents for the given condition
59
64
  def unschedule(condition)
60
- @events.reject! { |x| x.condition == condition }
65
+ @mutex.synchronize do
66
+ @events.reject! { |x| x.condition == condition }
67
+ end
61
68
  end
62
69
 
63
70
  def trigger(event)