class RFlow::DaemonProcess
Encapsulates a master process being managed by RFlow
that can run in the foreground or daemonize.
Constants
- SIGINFO
Symbolic constant for
SIGINFO
as this is only defined on BSD and not in Ruby.
Public Class Methods
# File lib/rflow/daemon_process.rb, line 10 def initialize(name, role = name, options = {}) @name = name @role = role @pid_file = PIDFile.new(options[:pid_file_path]) if options[:pid_file_path] end
Public Instance Methods
Daemonize by forking and exiting the parent after handling IO streams and checking successful start of the new copy. @return [void]
# File lib/rflow/daemon_process.rb, line 19 def daemonize! RFlow.logger.info "#{@name} daemonizing" establish_daemon_pipe drop_database_connections parent = fork if parent exit_after_daemon_starts else daemonize_process end end
Execute the master process. Writes out a pidfile and updates the process name, installs signal handlers, and spawns all the defined subprocesses. Finally executes {run_process}; when that returns, it will exit with the resulting return code. @return [void]
# File lib/rflow/daemon_process.rb, line 37 def run! write_pid_file register_logging_context update_process_name handle_signals spawn_subprocesses signal_successful_start RFlow.logger.info "#{@role} started" run_process ensure unhandle_signals remove_pid_file end
Default implementation. Subclasses should override to provide logic for actually doing something useful. @return [void]
# File lib/rflow/daemon_process.rb, line 60 def run_process; end
Shut down the application. Cleans up the pid file, removes signal handlers, and signals all child processes with SIGQUIT
. @return [void]
# File lib/rflow/daemon_process.rb, line 69 def shutdown!(reason) RFlow.logger.info "#{@name} shutting down due to #{reason}" remove_pid_file unhandle_signals signal_subprocesses('QUIT') RFlow.logger.info "#{@name} exiting" end
Default implementation. Subclasses should override to provide logic for actually spawning subprocesses. @return [void]
# File lib/rflow/daemon_process.rb, line 55 def spawn_subprocesses; end
A list of {ChildProcess}es to start and signal. @return [Array<ChildProcess>]
# File lib/rflow/daemon_process.rb, line 64 def subprocesses; []; end
Private Instance Methods
# File lib/rflow/daemon_process.rb, line 112 def close_stdio_streams $stdout.sync = $stderr.sync = true [$stdin, $stdout, $stderr].each do |stream| stream.binmode begin; stream.reopen '/dev/null'; rescue ::Exception; end end end
# File lib/rflow/daemon_process.rb, line 106 def daemonize_process @daemon_pipe_r.close Process.daemon(true, true) close_stdio_streams end
Holding database connections over the fork causes problems. Instead, let them be automatically restored after the fork.
# File lib/rflow/daemon_process.rb, line 85 def drop_database_connections ::ActiveRecord::Base.clear_all_connections! end
# File lib/rflow/daemon_process.rb, line 78 def establish_daemon_pipe @daemon_pipe_r, @daemon_pipe_w = IO.pipe [@daemon_pipe_r, @daemon_pipe_w].each {|io| io.fcntl(Fcntl::F_SETFD, Fcntl::FD_CLOEXEC) } end
# File lib/rflow/daemon_process.rb, line 89 def exit_after_daemon_starts @daemon_pipe_w.close # Parent waits for a PID on the pipe indicating that the # child successfully started. child_pid = (@daemon_pipe_r.readpartial(16) rescue nil).to_i @daemon_pipe_r.close if child_pid > 1 RFlow.logger.info "#{@role} indicated successful daemonization" exit 0 else RFlow.logger.error "#{@role} failed to start" STDERR.puts "\n\n*** #{@role} failed to start; see log file for details" exit! 1 end end
# File lib/rflow/daemon_process.rb, line 131 def handle_signals ['SIGTERM', 'SIGINT', 'SIGQUIT', 'SIGHUP', 'SIGCHLD'].each do |signal| trap_signal(signal) do |return_code| exit_status = if signal == 'SIGCHLD' pid, status = Process.wait2 status.exitstatus || 0 else 0 end shutdown! signal exit! exit_status end end trap_signal 'SIGUSR1' do RFlow.logger.reopen signal_subprocesses 'SIGUSR1' end trap_signal 'SIGUSR2' do RFlow.logger.toggle_log_level signal_subprocesses 'SIGUSR2' end trap_signal SIGINFO do RFlow.logger.dump_threads # don't tell child processes to dump, too spammy end end
# File lib/rflow/daemon_process.rb, line 120 def register_logging_context # arrange for process's name to appear in log messages RFlow.logger.clear_logging_context RFlow.logger.add_logging_context @name end
# File lib/rflow/daemon_process.rb, line 198 def remove_pid_file; @pid_file.safe_unlink if @pid_file; end
# File lib/rflow/daemon_process.rb, line 186 def signal_subprocesses(signal) subprocesses.reject {|p| p.pid.nil? }.each do |p| RFlow.logger.info "Signaling #{p.name} with #{signal}" begin Process.kill(signal, p.pid) rescue Errno::ESRCH # process already died and was waited for, ignore end end end
# File lib/rflow/daemon_process.rb, line 179 def signal_successful_start if @daemon_pipe_w @daemon_pipe_w.syswrite($$.to_s) @daemon_pipe_w.close rescue nil end end
# File lib/rflow/daemon_process.rb, line 167 def trap_signal(signal) # Log4r and traps don't mix, so we need to put it in another thread return_code = $? context = RFlow.logger.clone_logging_context Signal.trap signal do Thread.new do RFlow.logger.apply_logging_context context yield return_code end.join end end
# File lib/rflow/daemon_process.rb, line 161 def unhandle_signals ['SIGTERM', 'SIGINT', 'SIGQUIT', 'SIGCHLD', 'SIGHUP', 'SIGUSR1', 'SIGUSR2', SIGINFO].each do |signal| Signal.trap signal, 'DEFAULT' end end
# File lib/rflow/daemon_process.rb, line 126 def update_process_name # set the visible process name to match the process's name $0 = @name end
# File lib/rflow/daemon_process.rb, line 197 def write_pid_file; @pid_file.write if @pid_file; end