god 0.2.0 → 0.3.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.
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)