0cd2dc51c60448c08705480b8948d0b94ba257de
[feedcatcher.git] / vendor / rails / railties / lib / fcgi_handler.rb
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 # Initialize and run the FastCGI instance, passing arguments through to new.
22 def self.process!(*args, &block)
23 new(*args, &block).process!
24 end
25
26 # Initialize the FastCGI instance with the path to a crash log
27 # detailing unhandled exceptions (default RAILS_ROOT/log/fastcgi.crash.log)
28 # and the number of requests to process between garbage collection runs
29 # (default nil for normal GC behavior.) Optionally, pass a block which
30 # takes this instance as an argument for further configuration.
31 def initialize(log_file_path = nil, gc_request_period = nil)
32 self.log_file_path = log_file_path || "#{RAILS_ROOT}/log/fastcgi.crash.log"
33 self.gc_request_period = gc_request_period
34
35 # Yield for additional configuration.
36 yield self if block_given?
37
38 # Safely install signal handlers.
39 install_signal_handlers
40
41 @app = Dispatcher.new
42
43 # Start error timestamp at 11 seconds ago.
44 @last_error_on = Time.now - 11
45 end
46
47 def process!(provider = FCGI)
48 mark_features!
49
50 dispatcher_log :info, 'starting'
51 process_each_request provider
52 dispatcher_log :info, 'stopping gracefully'
53
54 rescue Exception => error
55 case error
56 when SystemExit
57 dispatcher_log :info, 'stopping after explicit exit'
58 when SignalException
59 dispatcher_error error, 'stopping after unhandled signal'
60 else
61 # Retry if exceptions occur more than 10 seconds apart.
62 if Time.now - @last_error_on > 10
63 @last_error_on = Time.now
64 dispatcher_error error, 'retrying after unhandled exception'
65 retry
66 else
67 dispatcher_error error, 'stopping after unhandled exception within 10 seconds of the last'
68 end
69 end
70 end
71
72 protected
73 def process_each_request(provider)
74 request = nil
75
76 catch :exit do
77 provider.each do |request|
78 process_request(request)
79
80 case when_ready
81 when :reload
82 reload!
83 when :restart
84 close_connection(request)
85 restart!
86 when :exit
87 close_connection(request)
88 throw :exit
89 end
90 end
91 end
92 rescue SignalException => signal
93 raise unless signal.message == 'SIGUSR1'
94 close_connection(request)
95 end
96
97 def process_request(request)
98 @processing, @when_ready = true, nil
99 gc_countdown
100
101 with_signal_handler 'USR1' do
102 begin
103 ::Rack::Handler::FastCGI.serve(request, @app)
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 # Logger errors
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 # close resources as they won't be closed by
198 # the OS when using exec
199 logger.close rescue nil
200 Rails.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 # Make a note of $" so we can safely reload this instance.
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(request)
237 request.finish if request
238 end
239 end