Daemonizing Ruby Processes

Mon, Sep 15, 2014

Your application might need a long running process in order to:

  • Listen on a socket (e.g. a web server)
  • Subscribe to a queue (e.g. a resque worker)
  • Poll a DB job table (e.g. a DelayedJob)

Much of the time you will use an appropriate 3rd party server such as Unicorn, Resque, or RackRabbit, but sometimes you just want to write some Ruby code and make it daemonizable. To achieve this you might reach for gems such as Dante or Daemon-kit, and these are good options, but with a little work you can avoid the additional gem dependency and get a deeper appreciation for the underlying Unix process mechanisms along the way.

As part of my work on the RackRabbit server I had to learn a little more about daemonizing and forking unix processes, and so I wrote up a little how-to for those of you who might want to daemonize your own Ruby code without adding any new dependencies.

Before I start, I highly recommend Jesse Storimer’s book “Working with Unix Processes”. Much of this article and the process management work on RackRabbit stems from Jesse’s work in this book:

You should go purchase a copy right now, don’t worry, I’ll wait…

A Ruby Command Line Process

Ok, so consider a simple Ruby script in bin/server that contains an infinite worker loop. In real life, the loop might be listening on a socket for incoming requests, or it might subscribe to a message queue for incoming messages, or it might poll a job table in a database. For our example purposes we will implement a simple idle loop:

#!/usr/bin/env ruby
while true
  puts "Doing some work"
  sleep(2)                # something productive should happen here
end

You can obviously run this Ruby script from the command line:

$ bin/server
Doing some work
Doing some work
Doing some work
...

NOTE: if you are unfamiliar with building command line scripts in Ruby I recommend checking out David Bryant Copeland’s book Build Awesome Command-Line Applications in Ruby

For a production environment we need to be able to run this loop as a daemonized process, detached from the terminal, and redirect its output to a logfile.

This article will walk through the steps it would take to achieve this…

Separation of Concerns

While our example above is a trivial idle loop, in reality your process will be more complex and likely contained within its own Ruby class. It is normal practice to separate the executable script from the main code:

  • lib/server.rb - the Server class that does the actual work.
  • bin/server - a simple wrapper that parses options and instantiates our Server class.

So lets do that.

First, lets move our idle loop to a Server class in lib/server.rb:

class Server

  attr_reader :options

  def initialize(options)
    @options = options
  end

  def run!
    while true
      puts "Doing some work"
      sleep(2)
    end
  end

end

Now our executable script in bin/server becomes a simple wrapper:

#!/usr/bin/env ruby
require 'server'
options = {}                # see next section
Server.new(options).run!

NOTE: In order to run the executable we now need to include our lib directory in the Ruby $LOAD_PATH - we will simplify this requirement later on.

$ ruby -Ilib bin/server
Doing some work
Doing some work
Doing some work
...

Command Line Options

We would like the daemonization options to be controllable from the command line of our script, some options we might like to specify include:

  • --daemonize
  • --pid PIDFILE
  • --log LOGFILE

We can extend our bin/server script using Ruby’s built-in OptionParser

#!/usr/bin/env ruby

require 'optparse'

options        = {}
version        = "1.0.0"
daemonize_help = "run daemonized in the background (default: false)"
pidfile_help   = "the pid filename"
logfile_help   = "the log filename"
include_help   = "an additional $LOAD_PATH"
debug_help     = "set $DEBUG to true"
warn_help      = "enable warnings"

op = OptionParser.new
op.banner =  "An example of how to daemonize a long running Ruby process."
op.separator ""
op.separator "Usage: server [options]"
op.separator ""

op.separator "Process options:"
op.on("-d", "--daemonize",   daemonize_help) {         options[:daemonize] = true  }
op.on("-p", "--pid PIDFILE", pidfile_help)   { |value| options[:pidfile]   = value }
op.on("-l", "--log LOGFILE", logfile_help)   { |value| options[:logfile]   = value }
op.separator ""

op.separator "Ruby options:"
op.on("-I", "--include PATH", include_help) { |value| $LOAD_PATH.unshift(*value.split(":").map{|v| File.expand_path(v)}) }
op.on(      "--debug",        debug_help)   { $DEBUG = true }
op.on(      "--warn",         warn_help)    { $-w = true    }
op.separator ""

op.separator "Common options:"
op.on("-h", "--help")    { puts op.to_s; exit }
op.on("-v", "--version") { puts version; exit }
op.separator ""

op.parse!(ARGV)

require 'server'
Server.new(options).run!

NOTE: By adding standard ruby options, such as -I, we can run the executable with our lib directory in the Ruby $LOAD_PATH a tiny bit simpler - we will remove this requirement completely later on.

$ bin/server -Ilib   # a touch simpler than `ruby -Ilib bin/server`
Doing some work
Doing some work
Doing some work
...

Extending the Server

We now have an executable script which can parse command line options and pass them on to a newly constructed Server. Next we must extend the server to act on those options.

We start by adding a little preprocessing on the options and some helper methods:

class Server

  attr_reader :options, :quit

  def initialize(options)

    @options = options

    # daemonization will change CWD so expand relative paths now
    options[:logfile] = File.expand_path(logfile) if logfile?
    options[:pidfile] = File.expand_path(pidfile) if pidfile?

  end

  def daemonize?
    options[:daemonize]
  end

  def logfile
    options[:logfile]
  end

  def pidfile
    options[:pidfile]
  end

  def logfile?
    !logfile.nil?
  end

  def pidfile?
    !pidfile.nil?
  end

  # ...

Next comes the critical addition, extend the #run! method to ensure that the process gets daemonized before performing it’s long running work by using a few key helper methods (which we will visit individually later on):

  • check_pid - ensure the server is not already running
  • daemonize - detach the process from the terminal
  • write_pid - write our PID to a file
  • trap_signals - trap QUIT signal to allow for graceful shutdown
  • redirect_output - ensure output gets redirected to a logfile
  • suppress_output - ensure output gets suppressed (if no logfile is provided)

The extended #run! method looks something like this:

class Server

  def run!

    check_pid
    daemonize if daemonize?
    write_pid
    trap_signals

    if logfile?
      redirect_output
    elsif daemonize?
      suppress_output
    end

    while !quit
      puts "Doing some work"
      sleep(2)
    end

  end

  # ...

NOTE: We also added a :quit attribute, and used it to terminate the long running loop. This attribute will be set true by our unix signal handling in a later section.

PID file management

A daemonized process should write out its PID to a file, and remove the file on exit:

def write_pid
  if pidfile?
    begin
      File.open(pidfile, ::File::CREAT | ::File::EXCL | ::File::WRONLY){|f| f.write("#{Process.pid}") }
      at_exit { File.delete(pidfile) if File.exists?(pidfile) }
    rescue Errno::EEXIST
      check_pid
      retry
    end
  end
end

The PID file can also be used to ensure that an instance is not already running when our server first starts up:

def check_pid
  if pidfile?
    case pid_status(pidfile)
    when :running, :not_owned
      puts "A server is already running. Check #{pidfile}"
      exit(1)
    when :dead
      File.delete(pidfile)
    end
  end
end

def pid_status(pidfile)
  return :exited unless File.exists?(pidfile)
  pid = ::File.read(pidfile).to_i
  return :dead if pid == 0
  Process.kill(0, pid)      # check process status
  :running
rescue Errno::ESRCH
  :dead
rescue Errno::EPERM
  :not_owned
end

Daemonization

The actual act of detaching from the terminal and daemonizing involves forking twice, setting the session ID, and changing the working directory. I couldn’t explain it any better than Jesse in Chapter 18 of his book, and lucky for us he published that as a sample chapter:

but seriously, if you really want to learn this stuff, go buy his book

def daemonize
  exit if fork
  Process.setsid
  exit if fork
  Dir.chdir "/"
end

The main difference in my sample code from Jesse’s book is that I split out the $stdout and $stderr redirecting into a separate method…

Redirecting Output

If the caller specifies a logfile then we must redirect $stdout and $stderr:

def redirect_output
  FileUtils.mkdir_p(File.dirname(logfile), :mode => 0755)
  FileUtils.touch logfile
  File.chmod(0644, logfile)
  $stderr.reopen(logfile, 'a')
  $stdout.reopen($stderr)
  $stdout.sync = $stderr.sync = true
end

If the caller did NOT specify a logfile, but did ask to be daemonized then we must disconnect $stdout and $stderr from the terminal and suppress the output:

def suppress_output
  $stderr.reopen('/dev/null', 'a')
  $stdout.reopen($stderr)
end

Signal Handling

For a simple daemon (e.g. non-forking) we can use the default Ruby signal handlers. The only signal we might want to override is QUIT if we want to perform a graceful shutdown:

def trap_signals
  trap(:QUIT) do   # graceful shutdown of run! loop
    @quit = true
  end
end

Putting it all together

To summarize the steps we just undertook:

You can find the full working example on GitHub:

I found it very insightful to work through what it would take to daemonize a Ruby process without using any other gem dependencies. With this foundation taking the next step to build a Unicorn-style forking server that can run multiple instances of a process (for load balancing) becomes possible (and perhaps a future article!), you can find a more extensive forking server in the RackRabbit project:

… purchase Jesse’s Book:

… explore 3rd party Ruby Daemonizing Gems:

… and read other interesting articles: