aboutsummaryrefslogtreecommitdiffstats
path: root/railties/bin/process/reaper
blob: 3b58698653ef9057bb87d909f5b34561846ad0e1 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
#!/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 usr1
    `kill -s USR1 #{@pid}`
  end

  def to_s
    "[#{@pid}] #{@command}"
  end
end

OPTIONS = {
  :action      => "graceful",
  :dispatcher  => File.expand_path(File.dirname(__FILE__) + '/../../public/dispatch.fcgi'),
  :spinner     => File.expand_path(File.dirname(__FILE__) + '/spinner'),
  :toggle_spin => true,
  :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.

  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("-s", "--spinner=path", "default: #{OPTIONS[:spinner]}", String)                       { |OPTIONS[:spinner]| }
  opts.on("-t", "--toggle-spin", "Whether to send a USR1 to the spinner before and after the reaping (default: true)") { |OPTIONS[:toggle_spin]| }
  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("usr1", OPTIONS[:spinner]) if OPTIONS[:toggle_spin]
ProgramProcess.process_keywords(OPTIONS[:action], OPTIONS[:dispatcher])
nudge(OPTIONS[:nudge], OPTIONS[:iterations]) if OPTIONS[:nudge]
ProgramProcess.process_keywords("usr1", OPTIONS[:spinner]) if OPTIONS[:toggle_spin]