reel-eye 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (93) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +32 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +3 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +22 -0
  7. data/README.md +170 -0
  8. data/Rakefile +20 -0
  9. data/bin/eye +322 -0
  10. data/bin/loader_eye +58 -0
  11. data/examples/notify.eye +18 -0
  12. data/examples/process_thin.rb +29 -0
  13. data/examples/processes/em.rb +57 -0
  14. data/examples/processes/forking.rb +20 -0
  15. data/examples/processes/sample.rb +144 -0
  16. data/examples/processes/thin.ru +12 -0
  17. data/examples/puma.eye +34 -0
  18. data/examples/rbenv.eye +11 -0
  19. data/examples/sidekiq.eye +23 -0
  20. data/examples/test.eye +81 -0
  21. data/examples/thin-farm.eye +29 -0
  22. data/examples/unicorn.eye +31 -0
  23. data/eye.gemspec +42 -0
  24. data/lib/eye.rb +28 -0
  25. data/lib/eye/application.rb +74 -0
  26. data/lib/eye/checker.rb +138 -0
  27. data/lib/eye/checker/cpu.rb +27 -0
  28. data/lib/eye/checker/file_ctime.rb +25 -0
  29. data/lib/eye/checker/file_size.rb +34 -0
  30. data/lib/eye/checker/http.rb +98 -0
  31. data/lib/eye/checker/memory.rb +27 -0
  32. data/lib/eye/checker/socket.rb +152 -0
  33. data/lib/eye/child_process.rb +101 -0
  34. data/lib/eye/client.rb +32 -0
  35. data/lib/eye/config.rb +88 -0
  36. data/lib/eye/control.rb +2 -0
  37. data/lib/eye/controller.rb +53 -0
  38. data/lib/eye/controller/commands.rb +73 -0
  39. data/lib/eye/controller/helpers.rb +61 -0
  40. data/lib/eye/controller/load.rb +214 -0
  41. data/lib/eye/controller/options.rb +48 -0
  42. data/lib/eye/controller/send_command.rb +115 -0
  43. data/lib/eye/controller/show_history.rb +62 -0
  44. data/lib/eye/controller/status.rb +131 -0
  45. data/lib/eye/dsl.rb +48 -0
  46. data/lib/eye/dsl/application_opts.rb +33 -0
  47. data/lib/eye/dsl/chain.rb +12 -0
  48. data/lib/eye/dsl/child_process_opts.rb +8 -0
  49. data/lib/eye/dsl/config_opts.rb +48 -0
  50. data/lib/eye/dsl/group_opts.rb +27 -0
  51. data/lib/eye/dsl/helpers.rb +12 -0
  52. data/lib/eye/dsl/main.rb +40 -0
  53. data/lib/eye/dsl/opts.rb +140 -0
  54. data/lib/eye/dsl/process_opts.rb +21 -0
  55. data/lib/eye/dsl/pure_opts.rb +110 -0
  56. data/lib/eye/dsl/validation.rb +59 -0
  57. data/lib/eye/group.rb +134 -0
  58. data/lib/eye/group/chain.rb +81 -0
  59. data/lib/eye/http.rb +31 -0
  60. data/lib/eye/http/router.rb +25 -0
  61. data/lib/eye/loader.rb +23 -0
  62. data/lib/eye/logger.rb +80 -0
  63. data/lib/eye/notify.rb +86 -0
  64. data/lib/eye/notify/jabber.rb +30 -0
  65. data/lib/eye/notify/mail.rb +44 -0
  66. data/lib/eye/process.rb +86 -0
  67. data/lib/eye/process/child.rb +58 -0
  68. data/lib/eye/process/commands.rb +256 -0
  69. data/lib/eye/process/config.rb +70 -0
  70. data/lib/eye/process/controller.rb +76 -0
  71. data/lib/eye/process/data.rb +47 -0
  72. data/lib/eye/process/monitor.rb +95 -0
  73. data/lib/eye/process/notify.rb +32 -0
  74. data/lib/eye/process/scheduler.rb +78 -0
  75. data/lib/eye/process/states.rb +86 -0
  76. data/lib/eye/process/states_history.rb +66 -0
  77. data/lib/eye/process/system.rb +97 -0
  78. data/lib/eye/process/trigger.rb +54 -0
  79. data/lib/eye/process/validate.rb +23 -0
  80. data/lib/eye/process/watchers.rb +69 -0
  81. data/lib/eye/reason.rb +20 -0
  82. data/lib/eye/server.rb +52 -0
  83. data/lib/eye/settings.rb +46 -0
  84. data/lib/eye/system.rb +154 -0
  85. data/lib/eye/system_resources.rb +86 -0
  86. data/lib/eye/trigger.rb +53 -0
  87. data/lib/eye/trigger/flapping.rb +28 -0
  88. data/lib/eye/utils.rb +14 -0
  89. data/lib/eye/utils/alive_array.rb +31 -0
  90. data/lib/eye/utils/celluloid_chain.rb +70 -0
  91. data/lib/eye/utils/leak_19.rb +7 -0
  92. data/lib/eye/utils/tail.rb +20 -0
  93. metadata +390 -0
@@ -0,0 +1,66 @@
1
+ class Eye::Process::StatesHistory < Eye::Utils::Tail
2
+
3
+ def push(state, reason = nil, tm = Time.now)
4
+ super(state: state, at: tm.to_i, reason: reason)
5
+ end
6
+
7
+ def states
8
+ self.map{|c| c[:state] }
9
+ end
10
+
11
+ def states_for_period(period, from_time = nil)
12
+ tm = Time.now - period
13
+ tm = [tm, from_time].max if from_time
14
+ tm = tm.to_f
15
+ self.select{|s| s[:at] >= tm }.map{|c| c[:state] }
16
+ end
17
+
18
+ def last_state
19
+ last[:state]
20
+ end
21
+
22
+ def last_reason
23
+ last[:reason]
24
+ end
25
+
26
+ def last_state_changed_at
27
+ Time.at(last[:at])
28
+ end
29
+
30
+ def seq?(*seq)
31
+ str = states * ','
32
+ substr = seq.flatten * ','
33
+ str.include?(substr)
34
+ end
35
+
36
+ def end?(*seq)
37
+ str = states * ','
38
+ substr = seq.flatten * ','
39
+ str.end_with?(substr)
40
+ end
41
+
42
+ def any?(*seq)
43
+ states.any? do |st|
44
+ seq.flatten.include?(st)
45
+ end
46
+ end
47
+
48
+ def noone?(*seq)
49
+ !states.all? do |st|
50
+ seq.flatten.include?(st)
51
+ end
52
+ end
53
+
54
+ def all?(*seq)
55
+ states.all? do |st|
56
+ seq.flatten.include?(st)
57
+ end
58
+ end
59
+
60
+ def state_count(state)
61
+ states.count do |st|
62
+ st == state
63
+ end
64
+ end
65
+
66
+ end
@@ -0,0 +1,97 @@
1
+ require 'timeout'
2
+
3
+ module Eye::Process::System
4
+
5
+ def load_pid_from_file
6
+ if File.exists?(self[:pid_file_ex])
7
+ _pid = File.read(self[:pid_file_ex]).to_i
8
+ _pid > 0 ? _pid : nil
9
+ end
10
+ end
11
+
12
+ def set_pid_from_file
13
+ self.pid = load_pid_from_file
14
+ end
15
+
16
+ def save_pid_to_file
17
+ if self.pid
18
+ File.open(self[:pid_file_ex], 'w') do |f|
19
+ f.write self.pid
20
+ end
21
+ true
22
+ else
23
+ false
24
+ end
25
+ end
26
+
27
+ def clear_pid_file
28
+ File.unlink(self[:pid_file_ex])
29
+ true
30
+ rescue
31
+ nil
32
+ end
33
+
34
+ def pid_file_ctime
35
+ File.ctime(self[:pid_file_ex]) rescue Time.now
36
+ end
37
+
38
+ def process_realy_running?
39
+ res = Eye::System.check_pid_alive(self.pid)
40
+ debug "process_realy_running?: (#{self.pid}) #{res.inspect}"
41
+ !!res[:result]
42
+ end
43
+
44
+ def send_signal(code)
45
+ res = Eye::System.send_signal(self.pid, code)
46
+
47
+ msg = "send_signal #{code} to #{self.pid}"
48
+ msg += ", error<#{res[:error]}>" if res[:error]
49
+ info msg
50
+
51
+ res[:result] == :ok
52
+ end
53
+
54
+ # non blocking actor timeout
55
+ def wait_for_condition(timeout, step = 0.1, &block)
56
+ defer{ wait_for_condition_sync(timeout, step, &block) }
57
+ end
58
+
59
+ def execute(cmd, cfg = {})
60
+ defer{ Eye::System::execute cmd, cfg }
61
+ end
62
+
63
+ def failsafe_load_pid
64
+ pid = load_pid_from_file
65
+
66
+ if !pid
67
+ # this is can be symlink changed case
68
+ sleep 0.1
69
+ pid = load_pid_from_file
70
+ end
71
+
72
+ pid
73
+ end
74
+
75
+ def failsafe_save_pid
76
+ save_pid_to_file
77
+ true
78
+ rescue => ex
79
+ error "failsafe_save_pid: #{ex.message}"
80
+ false
81
+ end
82
+
83
+ private
84
+
85
+ def wait_for_condition_sync(timeout, step, &block)
86
+ res = nil
87
+
88
+ Timeout::timeout(timeout.to_f) do
89
+ sleep step.to_f until res = yield
90
+ end
91
+
92
+ res
93
+ rescue Timeout::Error
94
+ false
95
+ end
96
+
97
+ end
@@ -0,0 +1,54 @@
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
16
+ return if unmonitored?
17
+
18
+ self.triggers.each do |trigger|
19
+ unless trigger.check(self.states_history)
20
+ on_flapping(trigger) if trigger.class == Eye::Trigger::Flapping
21
+ end
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def add_trigger(cfg = {})
28
+ trigger = Eye::Trigger.create(cfg, logger.prefix)
29
+ self.triggers << trigger
30
+ end
31
+
32
+ def on_flapping(trigger)
33
+ notify :error, 'flapping!'
34
+ schedule :unmonitor, Eye::Reason.new(:flapping)
35
+
36
+ @retry_times ||= 0
37
+ retry_in = trigger.retry_in
38
+
39
+ return unless retry_in
40
+ return if trigger.retry_times && @retry_times >= trigger.retry_times
41
+
42
+ schedule_in(retry_in.to_f, :retry_action)
43
+ end
44
+
45
+ def retry_action
46
+ debug "trigger retry timer"
47
+ return unless unmonitored?
48
+ return unless state_reason.to_s.include?('flapping') # TODO: remove hackety
49
+
50
+ schedule :start, Eye::Reason.new(:'retry start after flapping')
51
+ @retry_times += 1
52
+ end
53
+
54
+ end
@@ -0,0 +1,23 @@
1
+ require 'shellwords'
2
+
3
+ module Eye::Process::Validate
4
+
5
+ class Error < Exception; end
6
+
7
+ def validate(config)
8
+ if (str = config[:start_command])
9
+ # it should parse with Shellwords and not raise
10
+ spl = Shellwords.shellwords(str) * '#'
11
+
12
+ if config[:daemonize]
13
+ if spl =~ %r[sh#\-c|#&&#|;#]
14
+ raise Error, "#{config[:name]}, start_command in daemonize not supported shell concats like '&&'"
15
+ end
16
+ end
17
+ end
18
+
19
+ Shellwords.shellwords(config[:stop_command]) if config[:stop_command]
20
+ Shellwords.shellwords(config[:restart_command]) if config[:restart_command]
21
+ end
22
+
23
+ end
@@ -0,0 +1,69 @@
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 childs pids
15
+ if self[:monitor_children]
16
+ add_watcher(:check_childs, self[:childs_update_period]) do
17
+ add_or_update_childs
18
+ end
19
+ end
20
+
21
+ # monitor conditional watchers
22
+ start_checkers
23
+ else
24
+ warn 'try add_watchers, but its already here'
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 "add 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{|type, cfg| start_checker(cfg) }
50
+ end
51
+
52
+ def start_checker(cfg)
53
+ subject = Eye::Checker.create(pid, cfg, logger.prefix)
54
+
55
+ # ex: {:type => :memory, :every => 5.seconds, :below => 100.megabytes, :times => [3,5]}
56
+ add_watcher("check_#{cfg[:type]}".to_sym, subject.every, subject, &method(:watcher_tick).to_proc)
57
+ end
58
+
59
+ def watcher_tick(subject)
60
+ unless subject.check
61
+ return unless up?
62
+
63
+ action = subject.fire || :restart
64
+ notify :warn, "Bounded #{subject.check_name}: #{subject.last_human_values} send to :#{action}"
65
+ schedule action, Eye::Reason.new("bounded #{subject.check_name}")
66
+ end
67
+ end
68
+
69
+ end
data/lib/eye/reason.rb ADDED
@@ -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
data/lib/eye/server.rb ADDED
@@ -0,0 +1,52 @@
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
+ command, *args = socket.readline.strip.split('|')
25
+ response = command(command, *args)
26
+ socket.write(Marshal.dump(response))
27
+
28
+ rescue Errno::EPIPE
29
+ # client timeouted
30
+ # do nothing
31
+
32
+ ensure
33
+ socket.close
34
+ end
35
+
36
+ def command(cmd, *args)
37
+ Eye::Control.command(cmd, *args)
38
+ end
39
+
40
+ def unlink_socket_file
41
+ File.delete(@socket_path) if @socket_path
42
+ rescue
43
+ end
44
+
45
+ finalizer :close_socket
46
+
47
+ def close_socket
48
+ @server.close if @server
49
+ unlink_socket_file
50
+ end
51
+
52
+ end
@@ -0,0 +1,46 @@
1
+ require 'fileutils'
2
+
3
+ module Eye::Settings
4
+ module_function
5
+
6
+ def dir
7
+ if Process::UID.eid == 0 # root
8
+ '/var/run/eye'
9
+ else
10
+ File.expand_path(File.join(home, '.eye'))
11
+ end
12
+ end
13
+
14
+ def eyeconfig
15
+ if Process::UID.eid == 0 # root
16
+ '/etc/eye.conf'
17
+ else
18
+ File.expand_path(File.join(home, '.eyeconfig'))
19
+ end
20
+ end
21
+
22
+ def home
23
+ ENV['EYE_HOME'] || ENV['HOME']
24
+ end
25
+
26
+ def path(path)
27
+ File.join(dir, path)
28
+ end
29
+
30
+ def ensure_eye_dir
31
+ FileUtils.mkdir_p( dir )
32
+ end
33
+
34
+ def socket_path
35
+ path('sock')
36
+ end
37
+
38
+ def pid_path
39
+ path('pid')
40
+ end
41
+
42
+ def client_timeout
43
+ 5
44
+ end
45
+
46
+ end
data/lib/eye/system.rb ADDED
@@ -0,0 +1,154 @@
1
+ require 'shellwords'
2
+ require 'pathname'
3
+
4
+ module Eye::System
5
+ class << self
6
+ # Check that pid realy exits
7
+ # very fast
8
+ # return result hash
9
+ def check_pid_alive(pid)
10
+ res = if pid
11
+ ::Process.kill(0, pid)
12
+ else
13
+ false
14
+ end
15
+
16
+ {:result => res}
17
+ rescue => ex
18
+ {:error => ex}
19
+ end
20
+
21
+ # Check that pid realy exits
22
+ # very fast
23
+ # return true/false
24
+ def pid_alive?(pid)
25
+ res = check_pid_alive(pid)
26
+ !!res[:result]
27
+ end
28
+
29
+ # Send signal to process (uses for kill)
30
+ # code: TERM(15), KILL(9), QUIT(3), ...
31
+ def send_signal(pid, code = :TERM)
32
+ code = 0 if code == '0'
33
+ if code.to_s.to_i != 0
34
+ code = code.to_i
35
+ code = -code if code < 0
36
+ end
37
+ code = code.to_s.upcase if code.is_a?(String) || code.is_a?(Symbol)
38
+
39
+ if pid
40
+ ::Process.kill(code, pid)
41
+ {:result => :ok}
42
+ else
43
+ {:error => Exception.new('no_pid')}
44
+ end
45
+
46
+ rescue => ex
47
+ {:error => ex}
48
+ end
49
+
50
+ # Daemonize cmd, and detach
51
+ # options:
52
+ # :pid_file
53
+ # :working_dir
54
+ # :environment
55
+ # :stdin, :stdout, :stderr
56
+ def daemonize(cmd, cfg = {})
57
+ opts = spawn_options(cfg)
58
+ pid = Process::spawn(prepare_env(cfg), *Shellwords.shellwords(cmd), opts)
59
+ Process.detach(pid)
60
+ {:pid => pid}
61
+
62
+ rescue Errno::ENOENT, Errno::EACCES => ex
63
+ {:error => ex}
64
+ end
65
+
66
+ # Execute cmd with blocking, return status (be careful: inside actor blocks it mailbox, use with defer)
67
+ # options
68
+ # :working_dir
69
+ # :environment
70
+ # :stdin, :stdout, :stderr
71
+ def execute(cmd, cfg = {})
72
+ opts = spawn_options(cfg)
73
+ pid = Process::spawn(prepare_env(cfg), *Shellwords.shellwords(cmd), opts)
74
+
75
+ timeout = cfg[:timeout] || 1.second
76
+ Timeout.timeout(timeout) do
77
+ Process.waitpid(pid)
78
+ end
79
+
80
+ {:pid => pid}
81
+
82
+ rescue Timeout::Error => ex
83
+ if pid
84
+ Eye.warn "[#{cfg[:name]}] send signal 9 to #{pid} (because of timeouted<#{timeout}> execution)"
85
+ send_signal(pid, 9)
86
+ end
87
+ {:error => ex}
88
+
89
+ rescue Errno::ENOENT, Errno::EACCES => ex
90
+ {:error => ex}
91
+
92
+ ensure
93
+ Process.detach(pid) if pid
94
+ end
95
+
96
+ # get table
97
+ # {pid => {:rss =>, :cpu =>, :ppid => , :start_time => }}
98
+ # slow
99
+ def ps_aux
100
+ str = Process.send('`', PS_AUX_CMD).force_encoding('binary')
101
+ h = {}
102
+ str.each_line do |line|
103
+ chunk = line.strip.split(/\s+/)
104
+ h[chunk[0].to_i] = { :ppid => chunk[1].to_i, :cpu => chunk[2].to_i,
105
+ :rss => chunk[3].to_i, :start_time => chunk[4] }
106
+ end
107
+ h
108
+ end
109
+
110
+ # normalize file
111
+ def normalized_file(file, working_dir = nil)
112
+ Pathname.new(file).expand_path(working_dir).to_s
113
+ end
114
+
115
+ def host
116
+ @host ||= `hostname`.chomp
117
+ end
118
+
119
+ # set host for tests
120
+ def host=(hostname)
121
+ @host = hostname
122
+ end
123
+
124
+ private
125
+
126
+ PS_AUX_CMD = if RUBY_PLATFORM.include?('darwin')
127
+ 'ps axo pid,ppid,pcpu,rss,start'
128
+ else
129
+ 'ps axo pid,ppid,pcpu,rss,start_time'
130
+ end
131
+
132
+ def spawn_options(config = {})
133
+ o = {pgroup: true, chdir: config[:working_dir] || '/'}
134
+ o.update(out: [config[:stdout], 'a']) if config[:stdout]
135
+ o.update(err: [config[:stderr], 'a']) if config[:stderr]
136
+ o.update(in: config[:stdin]) if config[:stdin]
137
+ o
138
+ end
139
+
140
+ def prepare_env(config = {})
141
+ env = {}
142
+
143
+ (config[:environment] || {}).each do |k,v|
144
+ env[k.to_s] = v.to_s if v
145
+ end
146
+
147
+ # set PWD for unicorn respawn
148
+ env['PWD'] = config[:working_dir] if config[:working_dir]
149
+
150
+ env
151
+ end
152
+ end
153
+
154
+ end