| 1 |
require 'fcgi' |
|---|
| 2 |
require 'logger' |
|---|
| 3 |
require 'dispatcher' |
|---|
| 4 |
require 'rbconfig' |
|---|
| 5 |
|
|---|
| 6 |
class RailsFCGIHandler |
|---|
| 7 |
SIGNALS = { |
|---|
| 8 |
'HUP' => :reload, |
|---|
| 9 |
'INT' => :exit_now, |
|---|
| 10 |
'TERM' => :exit_now, |
|---|
| 11 |
'USR1' => :exit, |
|---|
| 12 |
'USR2' => :restart |
|---|
| 13 |
} |
|---|
| 14 |
GLOBAL_SIGNALS = SIGNALS.keys - %w(USR1) |
|---|
| 15 |
|
|---|
| 16 |
attr_reader :when_ready |
|---|
| 17 |
|
|---|
| 18 |
attr_accessor :log_file_path |
|---|
| 19 |
attr_accessor :gc_request_period |
|---|
| 20 |
|
|---|
| 21 |
|
|---|
| 22 |
|
|---|
| 23 |
def self.process!(*args, &block) |
|---|
| 24 |
new(*args, &block).process! |
|---|
| 25 |
end |
|---|
| 26 |
|
|---|
| 27 |
|
|---|
| 28 |
|
|---|
| 29 |
|
|---|
| 30 |
|
|---|
| 31 |
|
|---|
| 32 |
def initialize(log_file_path = nil, gc_request_period = nil) |
|---|
| 33 |
self.log_file_path = log_file_path || "#{RAILS_ROOT}/log/fastcgi.crash.log" |
|---|
| 34 |
self.gc_request_period = gc_request_period |
|---|
| 35 |
|
|---|
| 36 |
|
|---|
| 37 |
yield self if block_given? |
|---|
| 38 |
|
|---|
| 39 |
|
|---|
| 40 |
install_signal_handlers |
|---|
| 41 |
|
|---|
| 42 |
|
|---|
| 43 |
@last_error_on = Time.now - 11 |
|---|
| 44 |
end |
|---|
| 45 |
|
|---|
| 46 |
def process!(provider = FCGI) |
|---|
| 47 |
mark_features! |
|---|
| 48 |
|
|---|
| 49 |
dispatcher_log :info, 'starting' |
|---|
| 50 |
process_each_request provider |
|---|
| 51 |
dispatcher_log :info, 'stopping gracefully' |
|---|
| 52 |
|
|---|
| 53 |
rescue Exception => error |
|---|
| 54 |
case error |
|---|
| 55 |
when SystemExit |
|---|
| 56 |
dispatcher_log :info, 'stopping after explicit exit' |
|---|
| 57 |
when SignalException |
|---|
| 58 |
dispatcher_error error, 'stopping after unhandled signal' |
|---|
| 59 |
else |
|---|
| 60 |
|
|---|
| 61 |
if Time.now - @last_error_on > 10 |
|---|
| 62 |
@last_error_on = Time.now |
|---|
| 63 |
dispatcher_error error, 'retrying after unhandled exception' |
|---|
| 64 |
retry |
|---|
| 65 |
else |
|---|
| 66 |
dispatcher_error error, 'stopping after unhandled exception within 10 seconds of the last' |
|---|
| 67 |
end |
|---|
| 68 |
end |
|---|
| 69 |
end |
|---|
| 70 |
|
|---|
| 71 |
|
|---|
| 72 |
protected |
|---|
| 73 |
def process_each_request(provider) |
|---|
| 74 |
cgi = nil |
|---|
| 75 |
|
|---|
| 76 |
catch :exit do |
|---|
| 77 |
provider.each_cgi do |cgi| |
|---|
| 78 |
process_request(cgi) |
|---|
| 79 |
|
|---|
| 80 |
case when_ready |
|---|
| 81 |
when :reload |
|---|
| 82 |
reload! |
|---|
| 83 |
when :restart |
|---|
| 84 |
close_connection(cgi) |
|---|
| 85 |
restart! |
|---|
| 86 |
when :exit |
|---|
| 87 |
close_connection(cgi) |
|---|
| 88 |
throw :exit |
|---|
| 89 |
end |
|---|
| 90 |
end |
|---|
| 91 |
end |
|---|
| 92 |
rescue SignalException => signal |
|---|
| 93 |
raise unless signal.message == 'SIGUSR1' |
|---|
| 94 |
close_connection(cgi) |
|---|
| 95 |
end |
|---|
| 96 |
|
|---|
| 97 |
def process_request(cgi) |
|---|
| 98 |
@processing, @when_ready = true, nil |
|---|
| 99 |
gc_countdown |
|---|
| 100 |
|
|---|
| 101 |
with_signal_handler 'USR1' do |
|---|
| 102 |
begin |
|---|
| 103 |
Dispatcher.dispatch(cgi) |
|---|
| 104 |
rescue SignalException, SystemExit |
|---|
| 105 |
raise |
|---|
| 106 |
rescue Exception => error |
|---|
| 107 |
dispatcher_error error, 'unhandled dispatch error' |
|---|
| 108 |
end |
|---|
| 109 |
end |
|---|
| 110 |
ensure |
|---|
| 111 |
@processing = false |
|---|
| 112 |
end |
|---|
| 113 |
|
|---|
| 114 |
def logger |
|---|
| 115 |
@logger ||= Logger.new(@log_file_path) |
|---|
| 116 |
end |
|---|
| 117 |
|
|---|
| 118 |
def dispatcher_log(level, msg) |
|---|
| 119 |
time_str = Time.now.strftime("%d/%b/%Y:%H:%M:%S") |
|---|
| 120 |
logger.send(level, "[#{time_str} :: #{$$}] #{msg}") |
|---|
| 121 |
rescue Exception => log_error |
|---|
| 122 |
STDERR << "Couldn't write to #{@log_file_path.inspect}: #{msg}\n" |
|---|
| 123 |
STDERR << " #{log_error.class}: #{log_error.message}\n" |
|---|
| 124 |
end |
|---|
| 125 |
|
|---|
| 126 |
def dispatcher_error(e, msg = "") |
|---|
| 127 |
error_message = |
|---|
| 128 |
"Dispatcher failed to catch: #{e} (#{e.class})\n" + |
|---|
| 129 |
" #{e.backtrace.join("\n ")}\n#{msg}" |
|---|
| 130 |
dispatcher_log(:error, error_message) |
|---|
| 131 |
end |
|---|
| 132 |
|
|---|
| 133 |
def install_signal_handlers |
|---|
| 134 |
GLOBAL_SIGNALS.each { |signal| install_signal_handler(signal) } |
|---|
| 135 |
end |
|---|
| 136 |
|
|---|
| 137 |
def install_signal_handler(signal, handler = nil) |
|---|
| 138 |
if SIGNALS.include?(signal) && self.class.method_defined?(name = "#{SIGNALS[signal]}_handler") |
|---|
| 139 |
handler ||= method(name).to_proc |
|---|
| 140 |
|
|---|
| 141 |
begin |
|---|
| 142 |
trap(signal, handler) |
|---|
| 143 |
rescue ArgumentError |
|---|
| 144 |
dispatcher_log :warn, "Ignoring unsupported signal #{signal}." |
|---|
| 145 |
end |
|---|
| 146 |
else |
|---|
| 147 |
dispatcher_log :warn, "Ignoring unsupported signal #{signal}." |
|---|
| 148 |
end |
|---|
| 149 |
end |
|---|
| 150 |
|
|---|
| 151 |
def with_signal_handler(signal) |
|---|
| 152 |
install_signal_handler(signal) |
|---|
| 153 |
yield |
|---|
| 154 |
ensure |
|---|
| 155 |
install_signal_handler(signal, 'DEFAULT') |
|---|
| 156 |
end |
|---|
| 157 |
|
|---|
| 158 |
def exit_now_handler(signal) |
|---|
| 159 |
dispatcher_log :info, "asked to stop immediately" |
|---|
| 160 |
exit |
|---|
| 161 |
end |
|---|
| 162 |
|
|---|
| 163 |
def exit_handler(signal) |
|---|
| 164 |
dispatcher_log :info, "asked to stop ASAP" |
|---|
| 165 |
if @processing |
|---|
| 166 |
@when_ready = :exit |
|---|
| 167 |
else |
|---|
| 168 |
throw :exit |
|---|
| 169 |
end |
|---|
| 170 |
end |
|---|
| 171 |
|
|---|
| 172 |
def reload_handler(signal) |
|---|
| 173 |
dispatcher_log :info, "asked to reload ASAP" |
|---|
| 174 |
if @processing |
|---|
| 175 |
@when_ready = :reload |
|---|
| 176 |
else |
|---|
| 177 |
reload! |
|---|
| 178 |
end |
|---|
| 179 |
end |
|---|
| 180 |
|
|---|
| 181 |
def restart_handler(signal) |
|---|
| 182 |
dispatcher_log :info, "asked to restart ASAP" |
|---|
| 183 |
if @processing |
|---|
| 184 |
@when_ready = :restart |
|---|
| 185 |
else |
|---|
| 186 |
restart! |
|---|
| 187 |
end |
|---|
| 188 |
end |
|---|
| 189 |
|
|---|
| 190 |
def restart! |
|---|
| 191 |
config = ::Config::CONFIG |
|---|
| 192 |
ruby = File::join(config['bindir'], config['ruby_install_name']) + config['EXEEXT'] |
|---|
| 193 |
command_line = [ruby, $0, ARGV].flatten.join(' ') |
|---|
| 194 |
|
|---|
| 195 |
dispatcher_log :info, "restarted" |
|---|
| 196 |
|
|---|
| 197 |
|
|---|
| 198 |
|
|---|
| 199 |
logger.close rescue nil |
|---|
| 200 |
RAILS_DEFAULT_LOGGER.close rescue nil |
|---|
| 201 |
|
|---|
| 202 |
exec(command_line) |
|---|
| 203 |
end |
|---|
| 204 |
|
|---|
| 205 |
def reload! |
|---|
| 206 |
run_gc! if gc_request_period |
|---|
| 207 |
restore! |
|---|
| 208 |
@when_ready = nil |
|---|
| 209 |
dispatcher_log :info, "reloaded" |
|---|
| 210 |
end |
|---|
| 211 |
|
|---|
| 212 |
|
|---|
| 213 |
def mark_features! |
|---|
| 214 |
@features = $".clone |
|---|
| 215 |
end |
|---|
| 216 |
|
|---|
| 217 |
def restore! |
|---|
| 218 |
$".replace @features |
|---|
| 219 |
Dispatcher.reset_application! |
|---|
| 220 |
ActionController::Routing::Routes.reload |
|---|
| 221 |
end |
|---|
| 222 |
|
|---|
| 223 |
def run_gc! |
|---|
| 224 |
@gc_request_countdown = gc_request_period |
|---|
| 225 |
GC.enable; GC.start; GC.disable |
|---|
| 226 |
end |
|---|
| 227 |
|
|---|
| 228 |
def gc_countdown |
|---|
| 229 |
if gc_request_period |
|---|
| 230 |
@gc_request_countdown ||= gc_request_period |
|---|
| 231 |
@gc_request_countdown -= 1 |
|---|
| 232 |
run_gc! if @gc_request_countdown <= 0 |
|---|
| 233 |
end |
|---|
| 234 |
end |
|---|
| 235 |
|
|---|
| 236 |
def close_connection(cgi) |
|---|
| 237 |
cgi.instance_variable_get("@request").finish if cgi |
|---|
| 238 |
end |
|---|
| 239 |
end |
|---|