lib/bluepill/process.rb in bluepill-0.0.5 vs lib/bluepill/process.rb in bluepill-0.0.6
- old
+ new
@@ -5,98 +5,121 @@
class Process
CONFIGURABLE_ATTRIBUTES = [
:start_command,
:stop_command,
:restart_command,
+
:daemonize,
:pid_file,
+
:start_grace_time,
:stop_grace_time,
:restart_grace_time,
+
:uid,
- :gid
+ :gid,
+
+ :monitor_children,
+ :child_process_template
]
attr_accessor :name, :watches, :triggers, :logger, :skip_ticks_until
attr_accessor *CONFIGURABLE_ATTRIBUTES
+ attr_reader :children
- state_machine :initial => :unmonitored do
- state :unmonitored, :up, :down, :restarting
-
+ state_machine :initial => :unmonitored do
+ # These are the idle states, i.e. only an event (either external or internal) will trigger a transition.
+ # The distinction between down and unmonitored is that down
+ # means we know it is not running and unmonitored is that we don't care if it's running.
+ state :unmonitored, :up, :down
+
+ # These are transitionary states, we expect the process to change state after a certain period of time.
+ state :starting, :stopping, :restarting
+
event :tick do
- transition :unmonitored => :unmonitored
-
+ transition :starting => :up, :if => :process_running?
+ transition :starting => :down, :unless => :process_running?
+
transition :up => :up, :if => :process_running?
transition :up => :down, :unless => :process_running?
-
- transition :down => :up, :if => lambda {|process| process.process_running? || process.start_process }
+ # The process failed to die after entering the stopping state. Change the state to reflect
+ # reality.
+ transition :stopping => :up, :if => :process_running?
+ transition :stopping => :down, :unless => :process_running?
+
+ transition :down => :up, :if => :process_running?
+ transition :down => :starting, :unless => :process_running?
+
transition :restarting => :up, :if => :process_running?
transition :restarting => :down, :unless => :process_running?
end
-
+
event :start do
- transition :unmonitored => :up, :if => lambda {|process| process.process_running? || process.start_process }
- transition [:restarting, :up] => :up
- transition :down => :up, :if => :start_process
+ transition [:unmonitored, :down] => :starting
end
-
+
event :stop do
- transition [:unmonitored, :down] => :unmonitored
- transition [:up, :restarting] => :unmonitored, :if => :stop_process
+ transition :up => :stopping
end
-
- event :restart do
- transition all => :restarting, :if => :restart_process
- end
-
+
event :unmonitor do
- transition all => :unmonitored
+ transition any => :unmonitored
end
-
- after_transition any => any do |process, transition|
- unless transition.loopback?
- process.record_transition(transition.from_name, transition.to_name)
- end
+
+ event :restart do
+ transition [:up, :down] => :restarting
end
-
- after_transition any => any do |process, transition|
- process.notify_triggers(transition)
- end
+
+ before_transition any => any, :do => :notify_triggers
+
+ after_transition any => :starting, :do => :start_process
+ after_transition any => :stopping, :do => :stop_process
+ after_transition any => :restarting, :do => :restart_process
+
+ after_transition any => any, :do => :record_transition
end
def initialize(process_name, options = {})
@name = process_name
@event_mutex = Monitor.new
@transition_history = Util::RotationalArray.new(10)
@watches = []
@triggers = []
+ @children = []
- @stop_grace_time = @start_grace_time = @restart_grace_time = 3
+ @monitor_children = options[:monitor_children] || false
+ %w(start_grace_time stop_grace_time restart_grace_time).each do |grace|
+ instance_variable_set("@#{grace}", options[grace.to_sym] || 3)
+ end
+
CONFIGURABLE_ATTRIBUTES.each do |attribute_name|
self.send("#{attribute_name}=", options[attribute_name]) if options.has_key?(attribute_name)
end
- raise ArgumentError, "Please specify a pid_file or the demonize option" if pid_file.nil? && !daemonize?
-
# Let state_machine do its initialization stuff
super()
end
def tick
return if self.skipping_ticks?
self.skip_ticks_until = nil
# clear the memoization per tick
@process_running = nil
-
+
# run state machine transitions
super
-
- if process_running?
+
+ if self.up?
run_watches
+
+ if monitor_children?
+ refresh_children!
+ children.each {|child| child.tick}
+ end
end
end
def logger=(logger)
@logger = logger
@@ -109,20 +132,29 @@
@event_mutex.synchronize do
self.send("#{event}!")
end
end
- def record_transition(from, to)
- @transitioned = true
- logger.info "Going from #{from} => #{to}"
- self.watches.each { |w| w.clear_history! }
+ def record_transition(transition)
+ unless transition.loopback?
+ @transitioned = true
+
+ # When a process changes state, we should clear the memory of all the watches
+ self.watches.each { |w| w.clear_history! }
+
+ # Also, when a process changes state, we should re-populate its child list
+ if self.monitor_children?
+ self.logger.warning "Clearing child list"
+ self.children.clear
+ end
+ logger.info "Going from #{transition.from_name} => #{transition.to_name}"
+ end
end
def notify_triggers(transition)
self.triggers.each {|trigger| trigger.notify(transition)}
end
-
# Watch related methods
def add_watch(name, options = {})
self.watches << ConditionWatch.new(name, options.merge(:logger => self.logger))
end
@@ -151,90 +183,133 @@
break if @transitioned
self.dispatch!(event)
end
end
+ def handle_user_command(cmd)
+ case cmd
+ when "boot!"
+ # This is only called when bluepill is initially starting up
+ if process_running?(true)
+ # process was running even before bluepill was
+ self.state = 'up'
+ else
+ self.state = 'starting'
+ end
+
+ when "start"
+ if process_running?(true) && daemonize?
+ logger.warning("Refusing to re-run start command on an automatically daemonized process to preserve currently running process pid file.")
+ return
+ end
+ dispatch!(:start)
+
+ when "stop"
+ stop_process
+ dispatch!(:unmonitor)
+
+ when "restart"
+ restart_process
+
+ when "unmonitor"
+ # When the user issues an unmonitor cmd, reset any triggers so that
+ # scheduled events gets cleared
+ triggers.each {|t| t.reset! }
+ dispatch!(:unmonitor)
+ end
+ end
# System Process Methods
def process_running?(force = false)
@process_running = nil if force
@process_running ||= signal_process(0)
+ self.clear_pid unless @process_running
+ @process_running
end
def start_process
+ logger.warning "Executing start command: #{start_command}"
+
if self.daemonize?
starter = lambda { drop_privileges; ::Kernel.exec(start_command) }
child_pid = Daemonize.call_as_daemon(starter)
File.open(pid_file, "w") {|f| f.write(child_pid)}
else
# This is a self-daemonizing process
- system(start_command)
+ unless System.execute_blocking(start_command)
+ logger.warning "Start command execution returned non-zero exit code"
+ end
end
- self.clear_pid
-
- skip_ticks_for(start_grace_time)
-
- true
+
+ self.skip_ticks_for(start_grace_time)
end
- def stop_process
+ def stop_process
if stop_command
- system(stop_command)
+ cmd = stop_command.to_s.gsub("{{PID}}", actual_pid.to_s)
+ logger.warning "Executing stop command: #{cmd}"
+
+ unless System.execute_blocking(cmd)
+ logger.warning "Stop command execution returned non-zero exit code"
+ end
+
else
+ logger.warning "Executing default stop command. Sending TERM signal to #{actual_pid}"
signal_process("TERM")
-
- wait_until = Time.now.to_i + stop_grace_time
- while process_running?(true)
- if wait_until <= Time.now.to_i
- signal_process("KILL")
- break
- end
- sleep 0.2
- end
end
- self.unlink_pid
- self.clear_pid
-
- skip_ticks_for(stop_grace_time)
+ self.unlink_pid # TODO: we only write the pid file if we daemonize, should we only unlink it if we daemonize?
- true
+ self.skip_ticks_for(stop_grace_time)
end
def restart_process
if restart_command
- system(restart_command)
- skip_ticks_for(restart_grace_time)
- self.clear_pid
+ logger.warning "Executing restart command: #{restart_command}"
+
+ unless System.execute_blocking(restart_command)
+ logger.warning "Restart command execution returned non-zero exit code"
+ end
+
+ self.skip_ticks_for(restart_grace_time)
else
- stop_process
- start_process
+ logger.warning "No restart_command specified. Must stop and start to restart"
+ self.stop_process
+ # the tick will bring it back.
end
-
- true
end
def daemonize?
!!self.daemonize
end
+ def monitor_children?
+ !!self.monitor_children
+ end
+
def signal_process(code)
::Process.kill(code, actual_pid)
true
rescue
false
end
def actual_pid
- @actual_pid ||= File.read(pid_file).to_i if File.exists?(pid_file)
+ @actual_pid ||= begin
+ File.read(pid_file).to_i if pid_file && File.exists?(pid_file)
+ end
end
+ def actual_pid=(pid)
+ @actual_pid = pid
+ end
+
def clear_pid
@actual_pid = nil
end
def unlink_pid
- File.unlink(pid_file) if File.exists?(pid_file)
+ File.unlink(pid_file) if pid_file && File.exists?(pid_file)
end
def drop_privileges
begin
require 'etc'
@@ -252,14 +327,46 @@
end
# Internal State Methods
def skip_ticks_for(seconds)
- self.skip_ticks_until = (self.skip_ticks_until || Time.now.to_i) + seconds
+ # TODO: should this be addative or longest wins?
+ # i.e. if two calls for skip_ticks_for come in for 5 and 10, should it skip for 10 or 15?
+ self.skip_ticks_until = (self.skip_ticks_until || Time.now.to_i) + seconds.to_i
end
def skipping_ticks?
self.skip_ticks_until && self.skip_ticks_until > Time.now.to_i
+ end
+
+ def refresh_children!
+ # First prune the list of dead children
+ @children.delete_if {|child| !child.process_running?(true) }
+
+ # Add new found children to the list
+ new_children_pids = System.get_children(self.actual_pid) - @children.map {|child| child.actual_pid}
+
+ unless new_children_pids.empty?
+ logger.info "Existing children: #{@children.collect{|c| c.actual_pid}.join(",")}. Got new children: #{new_children_pids.inspect} for #{actual_pid}"
+ end
+
+ # Construct a new process wrapper for each new found children
+ new_children_pids.each do |child_pid|
+ child = self.child_process_template.deep_copy
+
+ child.name = "<child(pid:#{child_pid})>"
+ child.actual_pid = child_pid
+ child.logger = self.logger.prefix_with(child.name)
+
+ child.initialize_state_machines
+ child.state = "up"
+
+ @children << child
+ end
+ end
+
+ def deep_copy
+ Marshal.load(Marshal.dump(self))
end
end
end
\ No newline at end of file