#!/usr/local/bin/ruby require 'optparse' require 'net/http' require 'uri' def nudge(url, iterations) print "Nudging #{url}: " iterations.times { Net::HTTP.get_response(URI.parse(url)); print "."; STDOUT.flush } puts end if RUBY_PLATFORM =~ /mswin32/ then abort("Reaper is only for Unix") end class ProgramProcess class << self def process_keywords(action, *keywords) processes = keywords.collect { |keyword| find_by_keyword(keyword) }.flatten if processes.empty? puts "Couldn't find any process matching: #{keywords.join(" or ")}" else processes.each do |process| puts "#{action.capitalize}ing #{process}" process.send(action) end end end def find_by_keyword(keyword) process_lines_with_keyword(keyword).split("\n").collect { |line| next if line.include?("inq") || line.include?("ps -ax") || line.include?("grep") pid, *command = line.split new(pid, command.join(" ")) }.compact end private def process_lines_with_keyword(keyword) `ps -ax -o 'pid command' | grep #{keyword}` end end def initialize(pid, command) @pid, @command = pid, command end def find end def reload `kill -s HUP #{@pid}` end def graceful `kill -s TERM #{@pid}` end def kill `kill -9 #{@pid}` end def to_s "[#{@pid}] #{@command}" end end OPTIONS = { :action => "graceful", :dispatcher => File.expand_path(File.dirname(__FILE__) + '/../../public/dispatch.fcgi'), :iterations => 10, :nudge => false } ARGV.options do |opts| opts.banner = "Usage: reaper [options]" opts.separator "" opts.on <<-EOF Description: The reaper is used to reload, gracefully exit, and forcefully exit FCGI processes running a Rails Dispatcher. This is commonly done when a new version of the application is available, so the existing processes can be updated to use the latest code. The reaper actions are: * reload : Only reloads the application, but not the framework (like the development environment) * graceful: Marks all of the processes for exit after the next request * kill : Forcefully exists all processes regardless of whether they're currently serving a request Graceful exist is the most common and default action. But since the processes won't exist until after their next request, it's often necessary to ensure that such a request occurs right after they've been marked. That's what nudging is for. A nudge is simply a request to a URL where the dispatcher is serving. You should perform one nudge per FCGI process you have running if they're setup in a round-robin. Be sure to do one nudge per FCGI process across all your servers. So three servers with 10 processes each should nudge 30 times to be sure all processes are restarted. NOTE: You're responsible for restarting the processes after they exit. This can be automated by using the spinner. Examples: reaper -a reload reaper -n http://www.example.com -i 10 # gracefully exit, nudge 10 times EOF opts.on(" Options:") opts.on("-a", "--action=name", "reload|graceful|kill (default: #{OPTIONS[:action]})", String) { |OPTIONS[:action]| } opts.on("-d", "--dispatcher=path", "default: #{OPTIONS[:dispatcher]}", String) { |OPTIONS[:dispatcher]| } opts.on("-n", "--nudge=url", "Should point to URL that's handled by the FCGI process", String) { |OPTIONS[:nudge]| } opts.on("-i", "--iterations=number", "One nudge per FCGI process running (default: #{OPTIONS[:iterations]})", Integer) { |OPTIONS[:iterations]| } opts.separator "" opts.on("-h", "--help", "Show this help message.") { puts opts; exit } opts.parse! end ProgramProcess.process_keywords(OPTIONS[:action], OPTIONS[:dispatcher]) nudge(OPTIONS[:nudge], OPTIONS[:iterations]) if OPTIONS[:nudge]