class Bluepill::Process

Constants

CONFIGURABLE_ATTRIBUTES

Attributes

children[R]
logger[RW]
name[RW]
process_running[RW]
skip_ticks_until[RW]
statistics[R]
triggers[RW]
watches[RW]

Public Class Methods

new(process_name, checks, options = {}) click to toggle source
Calls superclass method
# File lib/bluepill/process.rb, line 112
def initialize(process_name, checks, options = {})
  @name = process_name
  @event_mutex = Monitor.new
  @watches = []
  @triggers = []
  @children = []
  @threads = []
  @statistics = ProcessStatistics.new
  @actual_pid = options[:actual_pid]
  self.logger = options[:logger]

  checks.each do |name, opts|
    if Trigger[name]
      add_trigger(name, opts)
    else
      add_watch(name, opts)
    end
  end

  # These defaults are overriden below if it's configured to be something else.
  @monitor_children = false
  @cache_actual_pid = true
  @start_grace_time = @stop_grace_time = @restart_grace_time = 3
  @environment = {}
  @on_start_timeout = 'start'
  @group_start_noblock = @group_stop_noblock = @group_restart_noblock = @group_unmonitor_noblock = true

  CONFIGURABLE_ATTRIBUTES.each do |attribute_name|
    send("#{attribute_name}=", options[attribute_name]) if options.key?(attribute_name)
  end

  # Let state_machine do its initialization stuff
  super() # no arguments intentional
end

Public Instance Methods

actual_pid() click to toggle source
# File lib/bluepill/process.rb, line 423
def actual_pid
  pid_command ? pid_from_command : pid_from_file
end
actual_pid=(pid) click to toggle source
# File lib/bluepill/process.rb, line 447
def actual_pid=(pid)
  ProcessJournal.append_pid_to_journal(name, pid) # be sure to always log the pid
  @actual_pid = pid
end
add_trigger(name, options = {}) click to toggle source
# File lib/bluepill/process.rb, line 213
def add_trigger(name, options = {})
  triggers << Trigger[name].new(self, options.merge(logger: logger))
end
add_watch(name, options = {}) click to toggle source

Watch related methods

# File lib/bluepill/process.rb, line 209
def add_watch(name, options = {})
  watches << ConditionWatch.new(name, options.merge(logger: logger))
end
cache_actual_pid?() click to toggle source
# File lib/bluepill/process.rb, line 419
def cache_actual_pid?
  !!@cache_actual_pid
end
clean_threads() click to toggle source
# File lib/bluepill/process.rb, line 397
def clean_threads
  @threads.each(&:kill)
  @threads.clear
end
cleanup_process() click to toggle source
# File lib/bluepill/process.rb, line 392
def cleanup_process
  ProcessJournal.kill_all_from_journal(name) # finish cleanup
  unlink_pid # TODO: we only write the pid file if we daemonize, should we only unlink it if we daemonize?
end
clear_pid() click to toggle source
# File lib/bluepill/process.rb, line 452
def clear_pid
  @actual_pid = nil
end
daemonize?() click to toggle source
# File lib/bluepill/process.rb, line 402
def daemonize?
  !!daemonize
end
determine_initial_state() click to toggle source
# File lib/bluepill/process.rb, line 239
def determine_initial_state
  if process_running?(true)
    self.state = 'up'
    return
  end
  self.state = (auto_start == false) ? 'unmonitored' : 'down' # we need to check for false value
end
dispatch!(event, reason = nil) click to toggle source

State machine methods

# File lib/bluepill/process.rb, line 175
def dispatch!(event, reason = nil)
  @event_mutex.synchronize do
    @statistics.record_event(event, reason)
    send(event.to_s)
  end
end
handle_user_command(cmd) click to toggle source
# File lib/bluepill/process.rb, line 247
def handle_user_command(cmd)
  case cmd
  when 'start'
    if process_running?(true)
      logger.warning('Refusing to re-run start command on an already running process.')
    else
      dispatch!(:start, 'user initiated')
    end
  when 'stop'
    stop_process
    dispatch!(:unmonitor, 'user initiated')
  when 'restart'
    restart_process
  when 'unmonitor'
    # When the user issues an unmonitor cmd, reset any triggers so that
    # scheduled events gets cleared
    triggers.each(&:reset!)
    dispatch!(:unmonitor, 'user initiated')
  end
end
logger=(logger) click to toggle source
# File lib/bluepill/process.rb, line 168
def logger=(logger)
  @logger = logger
  watches.each { |w| w.logger = logger }
  triggers.each { |t| t.logger = logger }
end
monitor_children?() click to toggle source
# File lib/bluepill/process.rb, line 406
def monitor_children?
  !!monitor_children
end
notify_triggers(transition) click to toggle source
# File lib/bluepill/process.rb, line 197
def notify_triggers(transition)
  triggers.each do |trigger|
    begin
      trigger.notify(transition)
    rescue => e
      logger.err e.backtrace
      raise e
    end
  end
end
pid_from_command() click to toggle source
# File lib/bluepill/process.rb, line 442
def pid_from_command
  pid = `#{pid_command}`.strip
  (pid =~ /\A\d+\z/) ? pid.to_i : nil
end
pid_from_file() click to toggle source
# File lib/bluepill/process.rb, line 427
def pid_from_file
  return @actual_pid if cache_actual_pid? && @actual_pid
  @actual_pid = begin
    if pid_file
      if File.exist?(pid_file)
        str = File.read(pid_file)
        str.to_i unless str.empty?
      else
        logger.warning("pid_file #{pid_file} does not exist or cannot be read")
        nil
      end
    end
  end
end
pre_start_process() click to toggle source
# File lib/bluepill/process.rb, line 306
def pre_start_process
  return unless pre_start_command
  logger.warning "Executing pre start command: #{pre_start_command}"
  result = System.execute_blocking(pre_start_command, system_command_options)
  return if result[:exit_code].zero?
  logger.warning 'Pre start command execution returned non-zero exit code:'
  logger.warning result.inspect
end
prepare_command(command) click to toggle source
# File lib/bluepill/process.rb, line 493
def prepare_command(command)
  command.to_s.gsub('{{PID}}', actual_pid.to_s)
end
process_running?(force = false) click to toggle source

System Process Methods

# File lib/bluepill/process.rb, line 269
def process_running?(force = false)
  @process_running = nil if force # clear existing state if forced

  @process_running ||= signal_process(0)
  # the process isn't running, so we should clear the PID
  clear_pid unless @process_running
  @process_running
end
record_transition(transition) click to toggle source
# File lib/bluepill/process.rb, line 182
def record_transition(transition)
  return if transition.loopback?
  @transitioned = true

  # When a process changes state, we should clear the memory of all the watches
  watches.each(&:clear_history!)

  # Also, when a process changes state, we should re-populate its child list
  if monitor_children?
    logger.warning 'Clearing child list'
    children.clear
  end
  logger.info "Going from #{transition.from_name} => #{transition.to_name}"
end
refresh_children!() click to toggle source
# File lib/bluepill/process.rb, line 471
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(actual_pid) - @children.collect(&:actual_pid)

  unless new_children_pids.empty?
    logger.info "Existing children: #{@children.collect(&: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|
    ProcessJournal.append_pid_to_journal(name, child_pid) if daemonize?
    child_name = "<child(pid:#{child_pid})>"
    logger = self.logger.prefix_with(child_name)

    child = child_process_factory.create_child_process(child_name, child_pid, logger)
    @children << child
  end
end
restart_process() click to toggle source
# File lib/bluepill/process.rb, line 369
def restart_process
  if restart_command
    cmd = prepare_command(restart_command)

    logger.warning "Executing restart command: #{cmd}"

    with_timeout(restart_grace_time, 'restart') do
      result = System.execute_blocking(cmd, system_command_options)

      unless result[:exit_code].zero?
        logger.warning 'Restart command execution returned non-zero exit code:'
        logger.warning result.inspect
      end
    end

    skip_ticks_for(restart_grace_time)
  else
    logger.warning 'No restart_command specified. Must stop and start to restart'
    stop_process
    start_process
  end
end
run_watches() click to toggle source
# File lib/bluepill/process.rb, line 217
def run_watches
  now = Time.now.to_i

  threads = watches.collect do |watch|
    [watch, Thread.new { Thread.current[:events] = watch.run(actual_pid, now) }]
  end

  @transitioned = false

  threads.each_with_object([]) do |(watch, thread), events|
    thread.join
    next if thread[:events].size.zero?
    logger.info "#{watch.name} dispatched: #{thread[:events].join(',')}"
    thread[:events].each do |event|
      events << [event, watch.to_s]
    end
  end.each do |event, reason| # rubocop:disable Style/MultilineBlockChain
    break if @transitioned
    dispatch!(event, reason)
  end
end
signal_process(code) click to toggle source
# File lib/bluepill/process.rb, line 410
def signal_process(code)
  code = code.to_s.upcase if code.is_a?(String) || code.is_a?(Symbol)
  ::Process.kill(code, actual_pid)
  true
rescue => e
  logger.err "Failed to signal process #{actual_pid} with code #{code}: #{e}"
  false
end
skip_ticks_for(seconds) click to toggle source

Internal State Methods

# File lib/bluepill/process.rb, line 461
def skip_ticks_for(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 = (skip_ticks_until || Time.now.to_i) + seconds.to_i
end
skipping_ticks?() click to toggle source
# File lib/bluepill/process.rb, line 467
def skipping_ticks?
  skip_ticks_until && skip_ticks_until > Time.now.to_i
end
start_process() click to toggle source
# File lib/bluepill/process.rb, line 278
def start_process
  ProcessJournal.kill_all_from_journal(name) # be sure nothing else is running from previous runs
  pre_start_process
  logger.warning "Executing start command: #{start_command}"
  if daemonize?
    daemon_id = System.daemonize(start_command, system_command_options)
    if daemon_id
      ProcessJournal.append_pid_to_journal(name, daemon_id)
      children.each do |child|
        ProcessJournal.append_pid_to_journal(name, child.actual_pid)
      end if monitor_children?
    end
    daemon_id
  else
    # This is a self-daemonizing process
    with_timeout(start_grace_time, on_start_timeout) do
      result = System.execute_blocking(start_command, system_command_options)

      unless result[:exit_code].zero?
        logger.warning 'Start command execution returned non-zero exit code:'
        logger.warning result.inspect
      end
    end
  end

  skip_ticks_for(start_grace_time)
end
stop_process() click to toggle source
# File lib/bluepill/process.rb, line 315
def stop_process
  if monitor_children
    System.get_children(actual_pid).each do |child_pid|
      ProcessJournal.append_pid_to_journal(name, child_pid)
    end
  end

  if stop_command
    cmd = prepare_command(stop_command)
    logger.warning "Executing stop command: #{cmd}"

    with_timeout(stop_grace_time, 'stop') do
      result = System.execute_blocking(cmd, system_command_options)

      unless result[:exit_code].zero?
        logger.warning 'Stop command execution returned non-zero exit code:'
        logger.warning result.inspect
      end
    end
    cleanup_process
  elsif stop_signals
    # issue stop signals with configurable delay between each
    logger.warning "Sending stop signals to #{actual_pid}"
    @threads << Thread.new(self, stop_signals.clone) do |process, stop_signals|
      signal = stop_signals.shift
      logger.info "Sending signal #{signal} to #{process.actual_pid}"
      process.signal_process(signal) # send first signal

      until stop_signals.empty?
        # we already checked to make sure stop_signals had an odd number of items
        delay = stop_signals.shift
        signal = stop_signals.shift

        logger.debug "Sleeping for #{delay} seconds"
        sleep delay
        # break unless signal_process(0) #break unless the process can be reached
        unless process.signal_process(0)
          logger.debug 'Process has terminated.'
          break
        end
        logger.info "Sending signal #{signal} to #{process.actual_pid}"
        process.signal_process(signal)
      end
      cleanup_process
    end
  else
    logger.warning "Executing default stop command. Sending TERM signal to #{actual_pid}"
    signal_process('TERM')
    cleanup_process
  end

  skip_ticks_for(stop_grace_time)
end
system_command_options() click to toggle source
# File lib/bluepill/process.rb, line 497
def system_command_options
  {
    uid: uid,
    gid: gid,
    working_dir: working_dir,
    environment: environment,
    pid_file: pid_file,
    logger: logger,
    stdin: stdin,
    stdout: stdout,
    stderr: stderr,
    supplementary_groups: supplementary_groups,
  }
end
tick() click to toggle source
Calls superclass method
# File lib/bluepill/process.rb, line 147
def tick
  return if skipping_ticks?
  self.skip_ticks_until = nil

  # clear the memoization per tick
  @process_running = nil

  # Deal with thread cleanup here since the stopping state isn't used
  clean_threads if unmonitored?

  # run state machine transitions
  super

  return unless up?
  run_watches

  return unless monitor_children?
  refresh_children!
  children.each(&:tick)
end
with_timeout(secs, next_state = nil, &blk) click to toggle source
# File lib/bluepill/process.rb, line 512
def with_timeout(secs, next_state = nil, &blk)
  # Attempt to execute the passed block. If the block takes
  # too long, transition to the indicated next state.
  Timeout.timeout(secs.to_f, &blk)
rescue Timeout::Error
  logger.err 'Execution is taking longer than expected.'
  logger.err 'Did you forget to tell bluepill to daemonize this process?'
  dispatch!(next_state)
end