Daemonizing Ruby Processes
Mon, Sep 15, 2014Your 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
- Command line options
- Extending the server
- PID file management
- Daemonization
- Redirecting output
- Signal handling
- Putting it all together
- Related Links
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
- theServer
class that does the actual work.bin/server
- a simple wrapper that parses options and instantiates ourServer
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 runningdaemonize
- detach the process from the terminalwrite_pid
- write our PID to a filetrap_signals
- trap QUIT signal to allow for graceful shutdownredirect_output
- ensure output gets redirected to a logfilesuppress_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 settrue
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:
- We started with a simple long-running Ruby Process
- We separated our concerns into
bin/server
andlib/server.rb
- We parsed command line options
- We extending the server in order to…
- Manage the PID file
- Daemonize the process
- Redirect output to a logfile
- Trap the QUIT signal for graceful shutdown
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:
Related Links
… purchase Jesse’s Book:
… explore 3rd party Ruby Daemonizing Gems:
… and read other interesting articles: