ace-eye 0.6.1

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 (114) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +38 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +6 -0
  5. data/CHANGES.md +77 -0
  6. data/Gemfile +6 -0
  7. data/LICENSE +22 -0
  8. data/README.md +212 -0
  9. data/Rakefile +35 -0
  10. data/bin/eye +5 -0
  11. data/bin/loader_eye +72 -0
  12. data/bin/runner +16 -0
  13. data/examples/dependency.eye +17 -0
  14. data/examples/notify.eye +19 -0
  15. data/examples/plugin/README.md +15 -0
  16. data/examples/plugin/main.eye +15 -0
  17. data/examples/plugin/plugin.rb +63 -0
  18. data/examples/process_thin.rb +29 -0
  19. data/examples/processes/em.rb +57 -0
  20. data/examples/processes/forking.rb +20 -0
  21. data/examples/processes/sample.rb +144 -0
  22. data/examples/processes/thin.ru +12 -0
  23. data/examples/puma.eye +29 -0
  24. data/examples/rbenv.eye +11 -0
  25. data/examples/sidekiq.eye +23 -0
  26. data/examples/test.eye +87 -0
  27. data/examples/thin-farm.eye +30 -0
  28. data/examples/unicorn.eye +39 -0
  29. data/eye.gemspec +40 -0
  30. data/lib/eye.rb +28 -0
  31. data/lib/eye/application.rb +73 -0
  32. data/lib/eye/checker.rb +258 -0
  33. data/lib/eye/checker/children_count.rb +44 -0
  34. data/lib/eye/checker/children_memory.rb +12 -0
  35. data/lib/eye/checker/cpu.rb +17 -0
  36. data/lib/eye/checker/cputime.rb +13 -0
  37. data/lib/eye/checker/file_ctime.rb +24 -0
  38. data/lib/eye/checker/file_size.rb +34 -0
  39. data/lib/eye/checker/file_touched.rb +15 -0
  40. data/lib/eye/checker/http.rb +96 -0
  41. data/lib/eye/checker/memory.rb +17 -0
  42. data/lib/eye/checker/nop.rb +6 -0
  43. data/lib/eye/checker/runtime.rb +18 -0
  44. data/lib/eye/checker/socket.rb +159 -0
  45. data/lib/eye/child_process.rb +101 -0
  46. data/lib/eye/cli.rb +185 -0
  47. data/lib/eye/cli/commands.rb +78 -0
  48. data/lib/eye/cli/render.rb +130 -0
  49. data/lib/eye/cli/server.rb +93 -0
  50. data/lib/eye/client.rb +32 -0
  51. data/lib/eye/config.rb +91 -0
  52. data/lib/eye/control.rb +2 -0
  53. data/lib/eye/controller.rb +54 -0
  54. data/lib/eye/controller/commands.rb +88 -0
  55. data/lib/eye/controller/helpers.rb +101 -0
  56. data/lib/eye/controller/load.rb +224 -0
  57. data/lib/eye/controller/options.rb +18 -0
  58. data/lib/eye/controller/send_command.rb +177 -0
  59. data/lib/eye/controller/status.rb +72 -0
  60. data/lib/eye/dsl.rb +53 -0
  61. data/lib/eye/dsl/application_opts.rb +39 -0
  62. data/lib/eye/dsl/chain.rb +12 -0
  63. data/lib/eye/dsl/child_process_opts.rb +13 -0
  64. data/lib/eye/dsl/config_opts.rb +55 -0
  65. data/lib/eye/dsl/group_opts.rb +32 -0
  66. data/lib/eye/dsl/helpers.rb +20 -0
  67. data/lib/eye/dsl/main.rb +51 -0
  68. data/lib/eye/dsl/opts.rb +151 -0
  69. data/lib/eye/dsl/process_opts.rb +36 -0
  70. data/lib/eye/dsl/pure_opts.rb +121 -0
  71. data/lib/eye/dsl/validation.rb +88 -0
  72. data/lib/eye/group.rb +140 -0
  73. data/lib/eye/group/chain.rb +81 -0
  74. data/lib/eye/loader.rb +10 -0
  75. data/lib/eye/local.rb +100 -0
  76. data/lib/eye/logger.rb +104 -0
  77. data/lib/eye/notify.rb +118 -0
  78. data/lib/eye/notify/jabber.rb +30 -0
  79. data/lib/eye/notify/mail.rb +48 -0
  80. data/lib/eye/process.rb +85 -0
  81. data/lib/eye/process/children.rb +60 -0
  82. data/lib/eye/process/commands.rb +280 -0
  83. data/lib/eye/process/config.rb +81 -0
  84. data/lib/eye/process/controller.rb +73 -0
  85. data/lib/eye/process/data.rb +78 -0
  86. data/lib/eye/process/monitor.rb +108 -0
  87. data/lib/eye/process/notify.rb +32 -0
  88. data/lib/eye/process/scheduler.rb +82 -0
  89. data/lib/eye/process/states.rb +86 -0
  90. data/lib/eye/process/states_history.rb +66 -0
  91. data/lib/eye/process/system.rb +97 -0
  92. data/lib/eye/process/trigger.rb +34 -0
  93. data/lib/eye/process/validate.rb +33 -0
  94. data/lib/eye/process/watchers.rb +66 -0
  95. data/lib/eye/reason.rb +20 -0
  96. data/lib/eye/server.rb +60 -0
  97. data/lib/eye/sigar.rb +5 -0
  98. data/lib/eye/system.rb +139 -0
  99. data/lib/eye/system_resources.rb +99 -0
  100. data/lib/eye/trigger.rb +136 -0
  101. data/lib/eye/trigger/check_dependency.rb +30 -0
  102. data/lib/eye/trigger/flapping.rb +41 -0
  103. data/lib/eye/trigger/stop_children.rb +17 -0
  104. data/lib/eye/trigger/transition.rb +15 -0
  105. data/lib/eye/trigger/wait_dependency.rb +49 -0
  106. data/lib/eye/utils.rb +45 -0
  107. data/lib/eye/utils/alive_array.rb +57 -0
  108. data/lib/eye/utils/celluloid_chain.rb +71 -0
  109. data/lib/eye/utils/celluloid_klass.rb +5 -0
  110. data/lib/eye/utils/leak_19.rb +10 -0
  111. data/lib/eye/utils/mini_active_support.rb +111 -0
  112. data/lib/eye/utils/pmap.rb +7 -0
  113. data/lib/eye/utils/tail.rb +20 -0
  114. metadata +398 -0
@@ -0,0 +1,34 @@
1
+ module Eye::Process::Trigger
2
+
3
+ def add_triggers
4
+ if self[:triggers]
5
+ self[:triggers].each do |type, cfg|
6
+ add_trigger(cfg)
7
+ end
8
+ end
9
+ end
10
+
11
+ def remove_triggers
12
+ self.triggers = []
13
+ end
14
+
15
+ def check_triggers(transition)
16
+ self.triggers.each { |trigger| trigger.notify(transition, state_reason) }
17
+ end
18
+
19
+ def retry_start_after_flapping
20
+ return unless unmonitored?
21
+ return unless state_reason.to_s.include?('flapping') # TODO: remove hackety
22
+
23
+ schedule :start, Eye::Reason.new(:'retry start after flapping')
24
+ self.flapping_times += 1
25
+ end
26
+
27
+ private
28
+
29
+ def add_trigger(cfg = {})
30
+ trigger = Eye::Trigger.create(current_actor, cfg)
31
+ self.triggers << trigger if trigger
32
+ end
33
+
34
+ end
@@ -0,0 +1,33 @@
1
+ require 'shellwords'
2
+ require 'etc'
3
+
4
+ module Eye::Process::Validate
5
+
6
+ class Error < Exception; end
7
+
8
+ def validate(config, localize = true)
9
+ if (str = config[:start_command])
10
+ # it should parse with Shellwords and not raise
11
+ spl = Shellwords.shellwords(str) * '#'
12
+
13
+ if config[:daemonize] && !config[:use_leaf_child]
14
+ if spl =~ %r[sh#\-c|#&&#|;#]
15
+ raise Error, "#{config[:name]}, daemonize does not support concats like '&&' in start_command"
16
+ end
17
+ end
18
+ end
19
+
20
+ Shellwords.shellwords(config[:stop_command]) if config[:stop_command]
21
+ Shellwords.shellwords(config[:restart_command]) if config[:restart_command]
22
+
23
+ if localize
24
+ Etc.getpwnam(config[:uid]) if config[:uid]
25
+ Etc.getpwnam(config[:gid]) if config[:gid]
26
+
27
+ if config[:working_dir]
28
+ raise Error, "working_dir '#{config[:working_dir]}' is invalid" unless File.directory?(config[:working_dir])
29
+ end
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,66 @@
1
+ module Eye::Process::Watchers
2
+
3
+ def add_watchers(force = false)
4
+ return unless self.up?
5
+
6
+ remove_watchers if force
7
+
8
+ if @watchers.blank?
9
+ # default watcher :check_alive
10
+ add_watcher(:check_alive, self[:check_alive_period]) do
11
+ check_alive
12
+ end
13
+
14
+ # monitor children pids
15
+ if self[:monitor_children]
16
+ add_watcher(:check_children, self[:children_update_period]) do
17
+ add_or_update_children
18
+ end
19
+ end
20
+
21
+ # monitor conditional watchers
22
+ start_checkers
23
+ else
24
+ warn 'add_watchers failed, watchers are already present'
25
+ end
26
+ end
27
+
28
+ def remove_watchers
29
+ @watchers.each{|_, h| h[:timer].cancel }
30
+ @watchers = {}
31
+ end
32
+
33
+ private
34
+
35
+ def add_watcher(type, period = 2, subject = nil, &block)
36
+ return if @watchers[type]
37
+
38
+ debug "adding watcher: #{type}(#{period})"
39
+
40
+ timer = every(period.to_f) do
41
+ debug "check #{type}"
42
+ block.call(subject)
43
+ end
44
+
45
+ @watchers[type] ||= {:timer => timer, :subject => subject}
46
+ end
47
+
48
+ def start_checkers
49
+ self[:checks].each{|name, cfg| start_checker(name, cfg) }
50
+ end
51
+
52
+ def start_checker(name, cfg)
53
+ subject = Eye::Checker.create(pid, cfg, current_actor)
54
+
55
+ # ex: {:type => :memory, :every => 5.seconds, :below => 100.megabytes, :times => [3,5]}
56
+ add_watcher("check_#{name}".to_sym, subject.every, subject, &method(:watcher_tick).to_proc) if subject
57
+ end
58
+
59
+ def watcher_tick(subject)
60
+ unless subject.check
61
+ return unless up?
62
+ subject.fire
63
+ end
64
+ end
65
+
66
+ end
@@ -0,0 +1,20 @@
1
+ class Eye::Reason
2
+
3
+ def initialize(mes = nil)
4
+ @message = mes
5
+ end
6
+
7
+ def to_s
8
+ @message.to_s
9
+ end
10
+
11
+ def user?
12
+ self.class == User
13
+ end
14
+
15
+ class User < Eye::Reason
16
+ def to_s
17
+ "#{super} by user"
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,60 @@
1
+ require 'celluloid/io'
2
+ require 'celluloid/autostart'
3
+
4
+ class Eye::Server
5
+ include Celluloid::IO
6
+
7
+ attr_reader :socket_path, :server
8
+
9
+ def initialize(socket_path)
10
+ @socket_path = socket_path
11
+ @server = begin
12
+ UNIXServer.open(socket_path)
13
+ rescue Errno::EADDRINUSE
14
+ unlink_socket_file
15
+ UNIXServer.open(socket_path)
16
+ end
17
+ end
18
+
19
+ def run
20
+ loop { async.handle_connection @server.accept }
21
+ end
22
+
23
+ def handle_connection(socket)
24
+ text = socket.read
25
+
26
+ begin
27
+ command, *args = Marshal.load(text)
28
+ rescue => ex
29
+ error "Failed to read from socket: #{ex.message}"
30
+ return
31
+ end
32
+
33
+ response = command(command, *args)
34
+ socket.write(Marshal.dump(response))
35
+
36
+ rescue Errno::EPIPE
37
+ # client timeouted
38
+ # do nothing
39
+
40
+ ensure
41
+ socket.close
42
+ end
43
+
44
+ def command(cmd, *args)
45
+ Eye::Control.command(cmd, *args)
46
+ end
47
+
48
+ def unlink_socket_file
49
+ File.delete(@socket_path) if @socket_path
50
+ rescue
51
+ end
52
+
53
+ finalizer :close_socket
54
+
55
+ def close_socket
56
+ @server.close if @server
57
+ unlink_socket_file
58
+ end
59
+
60
+ end
@@ -0,0 +1,5 @@
1
+ require 'sigar'
2
+ require 'logger'
3
+
4
+ Eye::Sigar = ::Sigar.new
5
+ Eye::Sigar.logger = ::Logger.new(nil)
@@ -0,0 +1,139 @@
1
+ require 'shellwords'
2
+ require 'pathname'
3
+ require 'etc'
4
+ require 'timeout'
5
+
6
+ module Eye::System
7
+ class << self
8
+ # Check that pid really exits
9
+ # very fast
10
+ # return result hash
11
+ def check_pid_alive(pid)
12
+ res = if pid
13
+ ::Process.kill(0, pid)
14
+ else
15
+ false
16
+ end
17
+
18
+ {:result => res}
19
+ rescue => ex
20
+ {:error => ex}
21
+ end
22
+
23
+ # Check that pid really exits
24
+ # very fast
25
+ # return true/false
26
+ def pid_alive?(pid)
27
+ res = check_pid_alive(pid)
28
+ !!res[:result]
29
+ end
30
+
31
+ # Send signal to process (uses for kill)
32
+ # code: TERM(15), KILL(9), QUIT(3), ...
33
+ def send_signal(pid, code = :TERM)
34
+ code = 0 if code == '0'
35
+ if code.to_s.to_i != 0
36
+ code = code.to_i
37
+ code = -code if code < 0
38
+ end
39
+ code = code.to_s.upcase if code.is_a?(String) || code.is_a?(Symbol)
40
+
41
+ if pid
42
+ ::Process.kill(code, pid)
43
+ {:result => :ok}
44
+ else
45
+ {:error => Exception.new('no_pid')}
46
+ end
47
+
48
+ rescue => ex
49
+ {:error => ex}
50
+ end
51
+
52
+ # Daemonize cmd, and detach
53
+ # options:
54
+ # :pid_file
55
+ # :working_dir
56
+ # :environment
57
+ # :stdin, :stdout, :stderr
58
+ def daemonize(cmd, cfg = {})
59
+ pid = ::Process::spawn(prepare_env(cfg), *Shellwords.shellwords(cmd), spawn_options(cfg))
60
+
61
+ {:pid => pid, :exitstatus => 0}
62
+
63
+ rescue Errno::ENOENT, Errno::EACCES => ex
64
+ {:error => ex}
65
+
66
+ ensure
67
+ Process.detach(pid) if pid
68
+ end
69
+
70
+ # Execute cmd with blocking, return status (be careful: inside actor blocks it mailbox, use with defer)
71
+ # options
72
+ # :working_dir
73
+ # :environment
74
+ # :stdin, :stdout, :stderr
75
+ def execute(cmd, cfg = {})
76
+ pid = ::Process::spawn(prepare_env(cfg), *Shellwords.shellwords(cmd), spawn_options(cfg))
77
+
78
+ timeout = cfg[:timeout] || 1.second
79
+ status = 0
80
+
81
+ Timeout.timeout(timeout) do
82
+ _, st = Process.waitpid2(pid)
83
+ status = st.exitstatus || st.termsig
84
+ end
85
+
86
+ {:pid => pid, :exitstatus => status}
87
+
88
+ rescue Timeout::Error => ex
89
+ if pid
90
+ warn "[#{cfg[:name]}] sending :KILL signal to <#{pid}> due to timeout (#{timeout}s)"
91
+ send_signal(pid, 9)
92
+ end
93
+ {:error => ex}
94
+
95
+ rescue Errno::ENOENT, Errno::EACCES => ex
96
+ {:error => ex}
97
+
98
+ ensure
99
+ Process.detach(pid) if pid
100
+ end
101
+
102
+ # normalize file
103
+ def normalized_file(file, working_dir = nil)
104
+ Pathname.new(file).expand_path(working_dir).to_s
105
+ end
106
+
107
+ def spawn_options(config = {})
108
+ options = {
109
+ pgroup: true,
110
+ chdir: config[:working_dir] || '/'
111
+ }
112
+
113
+ options[:out] = [config[:stdout], 'a'] if config[:stdout]
114
+ options[:err] = [config[:stderr], 'a'] if config[:stderr]
115
+ options[:in] = config[:stdin] if config[:stdin]
116
+ options[:umask] = config[:umask] if config[:umask]
117
+ options[:close_others] = false if config[:preserve_fds]
118
+ options[:unsetenv_others] = true if config[:clear_env]
119
+
120
+ if Eye::Local.root?
121
+ options[:uid] = Etc.getpwnam(config[:uid]).uid if config[:uid]
122
+ options[:gid] = Etc.getpwnam(config[:gid]).gid if config[:gid]
123
+ end
124
+
125
+ options
126
+ end
127
+
128
+ def prepare_env(config = {})
129
+ env = {}
130
+
131
+ (config[:environment] || {}).each do |k,v|
132
+ env[k.to_s] = v && v.to_s
133
+ end
134
+
135
+ env
136
+ end
137
+ end
138
+
139
+ end
@@ -0,0 +1,99 @@
1
+ require 'celluloid'
2
+
3
+ class Eye::SystemResources
4
+
5
+ # cached system resources
6
+ class << self
7
+
8
+ def memory(pid)
9
+ cache.proc_mem(pid).try(:resident)
10
+ end
11
+
12
+ def cpu(pid)
13
+ if cpu = cache.proc_cpu(pid)
14
+ cpu.percent * 100
15
+ end
16
+ end
17
+
18
+ def children(parent_pid)
19
+ cache.children(parent_pid)
20
+ end
21
+
22
+ def start_time(pid) # unixtime
23
+ if cpu = cache.proc_cpu(pid)
24
+ cpu.start_time.to_i / 1000
25
+ end
26
+ end
27
+
28
+ # total cpu usage in seconds
29
+ def cputime(pid)
30
+ if cpu = cache.proc_cpu(pid)
31
+ cpu.total.to_f / 1000
32
+ end
33
+ end
34
+
35
+ # last child in a children tree
36
+ def leaf_child(pid)
37
+ c = children(pid)
38
+ return if c.empty?
39
+ c += children(c.shift) while c.size > 1
40
+ c[0]
41
+ end
42
+
43
+ def resources(pid)
44
+ { :memory => memory(pid),
45
+ :cpu => cpu(pid),
46
+ :start_time => start_time(pid),
47
+ :pid => pid
48
+ }
49
+ end
50
+
51
+ def cache
52
+ @cache ||= Cache.new
53
+ end
54
+ end
55
+
56
+ class Cache
57
+ include Celluloid
58
+
59
+ attr_reader :expire
60
+
61
+ def initialize
62
+ clear
63
+ setup_expire
64
+ end
65
+
66
+ def setup_expire(expire = 5)
67
+ @expire = expire
68
+ @timer.cancel if @timer
69
+ @timer = every(@expire) { clear }
70
+ end
71
+
72
+ def clear
73
+ @memory = {}
74
+ @cpu = {}
75
+ @ppids = {}
76
+ end
77
+
78
+ def proc_mem(pid)
79
+ @memory[pid] ||= Eye::Sigar.proc_mem(pid) if pid
80
+
81
+ rescue ArgumentError # when incorrect PID
82
+ end
83
+
84
+ def proc_cpu(pid)
85
+ @cpu[pid] ||= Eye::Sigar.proc_cpu(pid) if pid
86
+
87
+ rescue ArgumentError # when incorrect PID
88
+ end
89
+
90
+ def children(pid)
91
+ if pid
92
+ @ppids[pid] ||= Eye::Sigar.proc_list("State.Ppid.eq=#{pid}")
93
+ else
94
+ []
95
+ end
96
+ end
97
+ end
98
+
99
+ end