gvarela-bluepill 0.0.27

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,225 @@
1
+ require 'etc'
2
+
3
+ module Bluepill
4
+ # This class represents the system that bluepill is running on.. It's mainly used to memoize
5
+ # results of running ps auxx etc so that every watch in the every process will not result in a fork
6
+ module System
7
+ APPEND_MODE = "a"
8
+ extend self
9
+
10
+ # The position of each field in ps output
11
+ IDX_MAP = {
12
+ :pid => 0,
13
+ :ppid => 1,
14
+ :pcpu => 2,
15
+ :rss => 3
16
+ }
17
+
18
+ def pid_alive?(pid)
19
+ begin
20
+ ::Process.kill(0, pid)
21
+ true
22
+ rescue Errno::ESRCH
23
+ false
24
+ end
25
+ end
26
+
27
+ def cpu_usage(pid)
28
+ ps_axu[pid] && ps_axu[pid][IDX_MAP[:pcpu]].to_f
29
+ end
30
+
31
+ def memory_usage(pid)
32
+ ps_axu[pid] && ps_axu[pid][IDX_MAP[:rss]].to_f
33
+ end
34
+
35
+ def get_children(parent_pid)
36
+ returning(Array.new) do |child_pids|
37
+ ps_axu.each_pair do |pid, chunks|
38
+ child_pids << chunks[IDX_MAP[:pid]].to_i if chunks[IDX_MAP[:ppid]].to_i == parent_pid.to_i
39
+ end
40
+ end
41
+ end
42
+
43
+ # Returns the pid of the child that executes the cmd
44
+ def daemonize(cmd, options = {})
45
+ rd, wr = IO.pipe
46
+
47
+ if child = Daemonize.safefork
48
+ # we do not wanna create zombies, so detach ourselves from the child exit status
49
+ ::Process.detach(child)
50
+
51
+ # parent
52
+ wr.close
53
+
54
+ daemon_id = rd.read.to_i
55
+ rd.close
56
+
57
+ return daemon_id if daemon_id > 0
58
+
59
+ else
60
+ # child
61
+ rd.close
62
+
63
+ drop_privileges(options[:uid], options[:gid])
64
+
65
+ # if we cannot write the pid file as the provided user, err out
66
+ exit unless can_write_pid_file(options[:pid_file], options[:logger])
67
+
68
+ to_daemonize = lambda do
69
+ # Setting end PWD env emulates bash behavior when dealing with symlinks
70
+ Dir.chdir(ENV["PWD"] = options[:working_dir]) if options[:working_dir]
71
+
72
+ redirect_io(*options.values_at(:stdin, :stdout, :stderr))
73
+
74
+ ::Kernel.exec(cmd)
75
+ exit
76
+ end
77
+
78
+ daemon_id = Daemonize.call_as_daemon(to_daemonize, nil, cmd)
79
+
80
+ File.open(options[:pid_file], "w") {|f| f.write(daemon_id)}
81
+
82
+ wr.write daemon_id
83
+ wr.close
84
+
85
+ exit
86
+ end
87
+ end
88
+
89
+ # Returns the stdout, stderr and exit code of the cmd
90
+ def execute_blocking(cmd, options = {})
91
+ rd, wr = IO.pipe
92
+
93
+ if child = Daemonize.safefork
94
+ # parent
95
+ wr.close
96
+
97
+ cmd_status = rd.read
98
+ rd.close
99
+
100
+ ::Process.waitpid(child)
101
+
102
+ return Marshal.load(cmd_status)
103
+
104
+ else
105
+ # child
106
+ rd.close
107
+
108
+ # create a child in which we can override the stdin, stdout and stderr
109
+ cmd_out_read, cmd_out_write = IO.pipe
110
+ cmd_err_read, cmd_err_write = IO.pipe
111
+
112
+ pid = fork {
113
+ # grandchild
114
+ drop_privileges(options[:uid], options[:gid])
115
+
116
+ Dir.chdir(ENV["PWD"] = options[:working_dir]) if options[:working_dir]
117
+
118
+ # close unused fds so ancestors wont hang. This line is the only reason we are not
119
+ # using something like popen3. If this fd is not closed, the .read call on the parent
120
+ # will never return because "wr" would still be open in the "exec"-ed cmd
121
+ wr.close
122
+
123
+ # we do not care about stdin of cmd
124
+ STDIN.reopen("/dev/null")
125
+
126
+ # point stdout of cmd to somewhere we can read
127
+ cmd_out_read.close
128
+ STDOUT.reopen(cmd_out_write)
129
+ cmd_out_write.close
130
+
131
+ # same thing for stderr
132
+ cmd_err_read.close
133
+ STDERR.reopen(cmd_err_write)
134
+ cmd_err_write.close
135
+
136
+ # finally, replace grandchild with cmd
137
+ ::Kernel.exec(cmd)
138
+ }
139
+
140
+ # we do not use these ends of the pipes in the child
141
+ cmd_out_write.close
142
+ cmd_err_write.close
143
+
144
+ # wait for the cmd to finish executing and acknowledge it's death
145
+ ::Process.waitpid(pid)
146
+
147
+ # collect stdout, stderr and exitcode
148
+ result = {
149
+ :stdout => cmd_out_read.read,
150
+ :stderr => cmd_err_read.read,
151
+ :exit_code => $?.exitstatus
152
+ }
153
+
154
+ # We're done with these ends of the pipes as well
155
+ cmd_out_read.close
156
+ cmd_err_read.close
157
+
158
+ # Time to tell the parent about what went down
159
+ wr.write Marshal.dump(result)
160
+ wr.close
161
+
162
+ exit
163
+ end
164
+ end
165
+
166
+ def store
167
+ @store ||= Hash.new
168
+ end
169
+
170
+ def reset_data
171
+ store.clear unless store.empty?
172
+ end
173
+
174
+ def ps_axu
175
+ # TODO: need a mutex here
176
+ store[:ps_axu] ||= begin
177
+ # BSD style ps invocation
178
+ lines = `ps axo pid=,ppid=,pcpu=,rss=`.split("\n")
179
+
180
+ lines.inject(Hash.new) do |mem, line|
181
+ chunks = line.split(/\s+/)
182
+ chunks.delete_if {|c| c.strip.empty? }
183
+ pid = chunks[IDX_MAP[:pid]].strip.to_i
184
+ mem[pid] = chunks
185
+ mem
186
+ end
187
+ end
188
+ end
189
+
190
+ # be sure to call this from a fork otherwise it will modify the attributes
191
+ # of the bluepill daemon
192
+ def drop_privileges(uid, gid)
193
+ uid_num = Etc.getpwnam(uid).uid if uid
194
+ gid_num = Etc.getgrnam(gid).gid if gid
195
+
196
+ ::Process.groups = [gid_num] if gid
197
+ ::Process::Sys.setgid(gid_num) if gid
198
+ ::Process::Sys.setuid(uid_num) if uid
199
+ end
200
+
201
+ def can_write_pid_file(pid_file, logger)
202
+ FileUtils.touch(pid_file)
203
+ File.unlink(pid_file)
204
+ return true
205
+
206
+ rescue Exception => e
207
+ logger.warning "%s - %s" % [e.class.name, e.message]
208
+ e.backtrace.each {|l| logger.warning l}
209
+ return false
210
+ end
211
+
212
+ def redirect_io(io_in, io_out, io_err)
213
+ $stdin.reopen(io_in) if io_in
214
+
215
+ if !io_out.nil? && !io_err.nil? && io_out == io_err
216
+ $stdout.reopen(io_out, APPEND_MODE)
217
+ $stderr.reopen($stdout)
218
+
219
+ else
220
+ $stdout.reopen(io_out, APPEND_MODE) if io_out
221
+ $stderr.reopen(io_err, APPEND_MODE) if io_err
222
+ end
223
+ end
224
+ end
225
+ end
@@ -0,0 +1,60 @@
1
+ module Bluepill
2
+ class Trigger
3
+ @implementations = {}
4
+ def self.inherited(klass)
5
+ @implementations[klass.name.split('::').last.underscore.to_sym] = klass
6
+ end
7
+
8
+ def self.[](name)
9
+ @implementations[name]
10
+ end
11
+
12
+ attr_accessor :process, :logger, :mutex, :scheduled_events
13
+
14
+ def initialize(process, options = {})
15
+ self.process = process
16
+ self.logger = options[:logger]
17
+ self.mutex = Mutex.new
18
+ self.scheduled_events = []
19
+ end
20
+
21
+ def reset!
22
+ self.cancel_all_events
23
+ end
24
+
25
+ def notify(transition)
26
+ raise "Implement in subclass"
27
+ end
28
+
29
+ def dispatch!(event)
30
+ self.process.dispatch!(event)
31
+ end
32
+
33
+ def schedule_event(event, delay)
34
+ # TODO: maybe wrap this in a ScheduledEvent class with methods like cancel
35
+ thread = Thread.new(self) do |trigger|
36
+ begin
37
+ sleep delay.to_f
38
+ trigger.logger.info("Retrying from flapping")
39
+ trigger.process.dispatch!(event)
40
+ trigger.mutex.synchronize do
41
+ trigger.scheduled_events.delete_if { |_, thread| thread == Thread.current }
42
+ end
43
+ rescue StandardError => e
44
+ trigger.logger.err(e)
45
+ trigger.logger.err(e.backtrace.join("\n"))
46
+ end
47
+ end
48
+
49
+ self.scheduled_events.push([event, thread])
50
+ end
51
+
52
+ def cancel_all_events
53
+ self.logger.info "Canceling all scheduled events"
54
+ self.mutex.synchronize do
55
+ self.scheduled_events.each {|_, thread| thread.kill}
56
+ end
57
+ end
58
+
59
+ end
60
+ end
@@ -0,0 +1,59 @@
1
+ module Bluepill
2
+ module Triggers
3
+ class Flapping < Bluepill::Trigger
4
+ TRIGGER_STATES = [:starting, :restarting]
5
+
6
+ PARAMS = [:times, :within, :retry_in]
7
+
8
+ attr_accessor *PARAMS
9
+ attr_reader :timeline
10
+
11
+ def initialize(process, options = {})
12
+ options.reverse_merge!(:times => 5, :within => 1, :retry_in => 5)
13
+
14
+ options.each_pair do |name, val|
15
+ instance_variable_set("@#{name}", val) if PARAMS.include?(name)
16
+ end
17
+
18
+ @timeline = Util::RotationalArray.new(@times)
19
+ super
20
+ end
21
+
22
+ def notify(transition)
23
+ if TRIGGER_STATES.include?(transition.to_name)
24
+ self.timeline << Time.now.to_i
25
+ self.check_flapping
26
+ end
27
+ end
28
+
29
+ def reset!
30
+ @timeline.clear
31
+ super
32
+ end
33
+
34
+ def check_flapping
35
+ num_occurances = (@timeline.nitems == self.times)
36
+
37
+ # The process has not flapped if we haven't encountered enough incidents
38
+ return unless num_occurances
39
+
40
+ # Check if the incident happend within the timeframe
41
+ duration = (@timeline.last - @timeline.first) <= self.within
42
+
43
+ if duration
44
+ self.logger.info "Flapping detected: retrying in #{self.retry_in} seconds"
45
+
46
+ self.schedule_event(:start, self.retry_in)
47
+
48
+ # this happens in the process' thread so we don't have to worry about concurrency issues with this event
49
+ self.dispatch!(:unmonitor)
50
+
51
+ @timeline.clear
52
+
53
+ # This will prevent a transition from happening in the process state_machine
54
+ throw :halt
55
+ end
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,58 @@
1
+ module Bluepill
2
+ module Util
3
+ class RotationalArray < Array
4
+ def initialize(size)
5
+ super(size)
6
+
7
+ @capacity = size
8
+ @counter = 0
9
+ end
10
+
11
+ def push(value)
12
+ idx = rotational_idx(@counter)
13
+ self[idx] = value
14
+
15
+ @counter += 1
16
+ self
17
+ end
18
+
19
+ alias_method :<<, :push
20
+
21
+ def pop
22
+ raise "Cannot call pop on a rotational array"
23
+ end
24
+
25
+ def shift
26
+ raise "Cannot call shift on a rotational array"
27
+ end
28
+
29
+ def unshift
30
+ raise "Cannot call unshift on a rotational array"
31
+ end
32
+
33
+ def last
34
+ return if @counter.zero?
35
+
36
+ self[rotational_idx(@counter - 1)]
37
+ end
38
+
39
+ def first
40
+ return if @counter.zero?
41
+ return self[0] if @counter <= @capacity
42
+
43
+ self[rotational_idx(@counter)]
44
+ end
45
+
46
+ def clear
47
+ @counter = 0
48
+ super
49
+ end
50
+
51
+ private
52
+
53
+ def rotational_idx(idx)
54
+ idx % @capacity
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,3 @@
1
+ module Bluepill
2
+ VERSION = "0.0.26"
3
+ end
data/lib/bluepill.rb ADDED
@@ -0,0 +1,32 @@
1
+
2
+ require 'thread'
3
+ require 'monitor'
4
+ require 'syslog'
5
+ require 'timeout'
6
+ require 'logger'
7
+
8
+ require 'active_support/inflector'
9
+ require 'active_support/core_ext/hash'
10
+ require 'active_support/core_ext/numeric'
11
+ require 'active_support/core_ext/object/misc'
12
+ require 'active_support/duration'
13
+
14
+ require 'state_machine'
15
+
16
+ require 'bluepill/application'
17
+ require 'bluepill/controller'
18
+ require 'bluepill/socket'
19
+ require "bluepill/process"
20
+ require "bluepill/group"
21
+ require "bluepill/logger"
22
+ require "bluepill/condition_watch"
23
+ require 'bluepill/trigger'
24
+ require 'bluepill/triggers/flapping'
25
+ require "bluepill/dsl"
26
+ require "bluepill/system"
27
+
28
+ require "bluepill/process_conditions"
29
+
30
+ require "bluepill/util/rotational_array"
31
+
32
+ require "bluepill/version"
data/lib/example.rb ADDED
@@ -0,0 +1,81 @@
1
+ require 'rubygems'
2
+ require 'bluepill'
3
+ require 'logger'
4
+
5
+ ROOT_DIR = "/tmp/bp"
6
+
7
+ # Watch with
8
+ # watch -n0.2 'ps axu | egrep "(CPU|forking|bluepill|sleep)" | grep -v grep | sort'
9
+ Bluepill.application(:sample_app) do |app|
10
+ 0.times do |i|
11
+ app.process("process_#{i}") do |process|
12
+ process.pid_file = "#{ROOT_DIR}/pids/process_#{i}.pid"
13
+
14
+ # I could not figure out a portable way to
15
+ # specify the path to the sample forking server across the diff developer laptops.
16
+ # Since this code is eval'ed we cannot reliably use __FILE__
17
+ process.start_command = "/Users/rohith/work/bluepill/bin/sample_forking_server #{4242 + i}"
18
+ process.stop_command = "kill -INT {{PID}}"
19
+ process.daemonize = true
20
+
21
+ process.start_grace_time = 1.seconds
22
+ process.restart_grace_time = 7.seconds
23
+ process.stop_grace_time = 7.seconds
24
+
25
+ process.uid = "rohith"
26
+ process.gid = "staff"
27
+
28
+ # process.checks :cpu_usage, :every => 10, :below => 0.5, :times => [5, 5]
29
+ process.checks :flapping, :times => 2, :within => 30.seconds, :retry_in => 7.seconds
30
+
31
+ process.monitor_children do |child_process|
32
+ # child_process.checks :cpu_usage,
33
+ # :every => 10,
34
+ # :below => 0.5,
35
+ # :times => [5, 5]
36
+
37
+ # child_process.checks :mem_usage,
38
+ # :every => 3,
39
+ # :below => 600.kilobytes,
40
+ # :times => [3, 5],
41
+ # :fires => [:stop]
42
+
43
+ child_process.stop_command = "kill -QUIT {{PID}}"
44
+ # child_process.checks :flapping, :times => 2, :within => 30.seconds, :retry_in => 7.seconds
45
+ end
46
+ end
47
+ end
48
+
49
+ 0.times do |i|
50
+ app.process("group_process_#{i}") do |process|
51
+ process.group = "group_1"
52
+ process.pid_file = "/Users/rohith/ffs/tmp/pids/mongrel_#{i}.pid"
53
+ process.start_command = "cd ~/ffs && mongrel_rails start -P #{process.pid_file} -p 3000 -d"
54
+
55
+ process.start_grace_time = 10.seconds
56
+
57
+ process.uid = "rohith"
58
+ process.gid = "staff"
59
+
60
+ # process.checks :always_true, :every => 10
61
+ end
62
+ end
63
+
64
+ 1.times do |i|
65
+ app.process("group_process_#{i}") do |process|
66
+ process.uid = "rohith"
67
+ process.gid = "wheel"
68
+
69
+ process.stderr = "/tmp/err.log"
70
+ process.stdout = "/tmp/err.log"
71
+
72
+
73
+ process.group = "grouped"
74
+ process.start_command = %Q{cd /tmp && ruby -e '$stderr.puts("hello stderr");$stdout.puts("hello stdout"); $stdout.flush; $stderr.flush; sleep 10'}
75
+ process.daemonize = true
76
+ process.pid_file = "/tmp/noperm/p_#{process.group}_#{i}.pid"
77
+
78
+ # process.checks :always_true, :every => 5
79
+ end
80
+ end
81
+ end
metadata ADDED
@@ -0,0 +1,126 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: gvarela-bluepill
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.27
5
+ platform: ruby
6
+ authors:
7
+ - Arya Asemanfar
8
+ - Gary Tsang
9
+ - Rohith Ravi
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+
14
+ date: 2009-12-04 00:00:00 -07:00
15
+ default_executable: bluepill
16
+ dependencies:
17
+ - !ruby/object:Gem::Dependency
18
+ name: daemons
19
+ type: :runtime
20
+ version_requirement:
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: 1.0.9
26
+ version:
27
+ - !ruby/object:Gem::Dependency
28
+ name: blankslate
29
+ type: :runtime
30
+ version_requirement:
31
+ version_requirements: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: 2.1.2.2
36
+ version:
37
+ - !ruby/object:Gem::Dependency
38
+ name: state_machine
39
+ type: :runtime
40
+ version_requirement:
41
+ version_requirements: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: 0.8.0
46
+ version:
47
+ - !ruby/object:Gem::Dependency
48
+ name: activesupport
49
+ type: :runtime
50
+ version_requirement:
51
+ version_requirements: !ruby/object:Gem::Requirement
52
+ requirements:
53
+ - - ">="
54
+ - !ruby/object:Gem::Version
55
+ version: 2.3.5
56
+ version:
57
+ description: Bluepill keeps your daemons up while taking up as little resources as possible. After all you probably want the resources of your server to be used by whatever daemons you are running rather than the thing that's supposed to make sure they are brought back up, should they die or misbehave.
58
+ email: entombedvirus@gmail.com
59
+ executables:
60
+ - bluepill
61
+ extensions: []
62
+
63
+ extra_rdoc_files:
64
+ - LICENSE
65
+ - README.md
66
+ files:
67
+ - .gitignore
68
+ - DESIGN.md
69
+ - LICENSE
70
+ - README.md
71
+ - Rakefile
72
+ - VERSION
73
+ - bin/bluepill
74
+ - bluepill.gemspec
75
+ - lib/bluepill.rb
76
+ - lib/bluepill/application.rb
77
+ - lib/bluepill/application/client.rb
78
+ - lib/bluepill/application/server.rb
79
+ - lib/bluepill/condition_watch.rb
80
+ - lib/bluepill/controller.rb
81
+ - lib/bluepill/dsl.rb
82
+ - lib/bluepill/group.rb
83
+ - lib/bluepill/logger.rb
84
+ - lib/bluepill/process.rb
85
+ - lib/bluepill/process_conditions.rb
86
+ - lib/bluepill/process_conditions/always_true.rb
87
+ - lib/bluepill/process_conditions/cpu_usage.rb
88
+ - lib/bluepill/process_conditions/mem_usage.rb
89
+ - lib/bluepill/process_conditions/process_condition.rb
90
+ - lib/bluepill/socket.rb
91
+ - lib/bluepill/system.rb
92
+ - lib/bluepill/trigger.rb
93
+ - lib/bluepill/triggers/flapping.rb
94
+ - lib/bluepill/util/rotational_array.rb
95
+ - lib/bluepill/version.rb
96
+ - lib/example.rb
97
+ has_rdoc: true
98
+ homepage: http://github.com/gvarela/bluepill
99
+ licenses: []
100
+
101
+ post_install_message:
102
+ rdoc_options:
103
+ - --charset=UTF-8
104
+ require_paths:
105
+ - lib
106
+ required_ruby_version: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: "0"
111
+ version:
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: "0"
117
+ version:
118
+ requirements: []
119
+
120
+ rubyforge_project:
121
+ rubygems_version: 1.3.5
122
+ signing_key:
123
+ specification_version: 3
124
+ summary: A process monitor written in Ruby with stability and minimalism in mind.
125
+ test_files: []
126
+