Froze rails gems
[depot.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 # Start error timestamp at 11 seconds ago.
42 @last_error_on = Time.now - 11
43 end
44
45 def process!(provider = FCGI)
46 mark_features!
47
48 dispatcher_log :info, 'starting'
49 process_each_request provider
50 dispatcher_log :info, 'stopping gracefully'
51
52 rescue Exception => error
53 case error
54 when SystemExit
55 dispatcher_log :info, 'stopping after explicit exit'
56 when SignalException
57 dispatcher_error error, 'stopping after unhandled signal'
58 else
59 # Retry if exceptions occur more than 10 seconds apart.
60 if Time.now - @last_error_on > 10
61 @last_error_on = Time.now
62 dispatcher_error error, 'retrying after unhandled exception'
63 retry
64 else
65 dispatcher_error error, 'stopping after unhandled exception within 10 seconds of the last'
66 end
67 end
68 end
69
70 protected
71 def process_each_request(provider)
72 cgi = nil
73
74 catch :exit do
75 provider.each_cgi do |cgi|
76 process_request(cgi)
77
78 case when_ready
79 when :reload
80 reload!
81 when :restart
82 close_connection(cgi)
83 restart!
84 when :exit
85 close_connection(cgi)
86 throw :exit
87 end
88 end
89 end
90 rescue SignalException => signal
91 raise unless signal.message == 'SIGUSR1'
92 close_connection(cgi)
93 end
94
95 def process_request(cgi)
96 @processing, @when_ready = true, nil
97 gc_countdown
98
99 with_signal_handler 'USR1' do
100 begin
101 Dispatcher.dispatch(cgi)
102 rescue SignalException, SystemExit
103 raise
104 rescue Exception => error
105 dispatcher_error error, 'unhandled dispatch error'
106 end
107 end
108 ensure
109 @processing = false
110 end
111
112 def logger
113 @logger ||= Logger.new(@log_file_path)
114 end
115
116 def dispatcher_log(level, msg)
117 time_str = Time.now.strftime("%d/%b/%Y:%H:%M:%S")
118 logger.send(level, "[#{time_str} :: #{$$}] #{msg}")
119 rescue Exception => log_error # Logger errors
120 STDERR << "Couldn't write to #{@log_file_path.inspect}: #{msg}\n"
121 STDERR << " #{log_error.class}: #{log_error.message}\n"
122 end
123
124 def dispatcher_error(e, msg = "")
125 error_message =
126 "Dispatcher failed to catch: #{e} (#{e.class})\n" +
127 " #{e.backtrace.join("\n ")}\n#{msg}"
128 dispatcher_log(:error, error_message)
129 end
130
131 def install_signal_handlers
132 GLOBAL_SIGNALS.each { |signal| install_signal_handler(signal) }
133 end
134
135 def install_signal_handler(signal, handler = nil)
136 if SIGNALS.include?(signal) && self.class.method_defined?(name = "#{SIGNALS[signal]}_handler")
137 handler ||= method(name).to_proc
138
139 begin
140 trap(signal, handler)
141 rescue ArgumentError
142 dispatcher_log :warn, "Ignoring unsupported signal #{signal}."
143 end
144 else
145 dispatcher_log :warn, "Ignoring unsupported signal #{signal}."
146 end
147 end
148
149 def with_signal_handler(signal)
150 install_signal_handler(signal)
151 yield
152 ensure
153 install_signal_handler(signal, 'DEFAULT')
154 end
155
156 def exit_now_handler(signal)
157 dispatcher_log :info, "asked to stop immediately"
158 exit
159 end
160
161 def exit_handler(signal)
162 dispatcher_log :info, "asked to stop ASAP"
163 if @processing
164 @when_ready = :exit
165 else
166 throw :exit
167 end
168 end
169
170 def reload_handler(signal)
171 dispatcher_log :info, "asked to reload ASAP"
172 if @processing
173 @when_ready = :reload
174 else
175 reload!
176 end
177 end
178
179 def restart_handler(signal)
180 dispatcher_log :info, "asked to restart ASAP"
181 if @processing
182 @when_ready = :restart
183 else
184 restart!
185 end
186 end
187
188 def restart!
189 config = ::Config::CONFIG
190 ruby = File::join(config['bindir'], config['ruby_install_name']) + config['EXEEXT']
191 command_line = [ruby, $0, ARGV].flatten.join(' ')
192
193 dispatcher_log :info, "restarted"
194
195 # close resources as they won't be closed by
196 # the OS when using exec
197 logger.close rescue nil
198 Rails.logger.close rescue nil
199
200 exec(command_line)
201 end
202
203 def reload!
204 run_gc! if gc_request_period
205 restore!
206 @when_ready = nil
207 dispatcher_log :info, "reloaded"
208 end
209
210 # Make a note of $" so we can safely reload this instance.
211 def mark_features!
212 @features = $".clone
213 end
214
215 def restore!
216 $".replace @features
217 Dispatcher.reset_application!
218 ActionController::Routing::Routes.reload
219 end
220
221 def run_gc!
222 @gc_request_countdown = gc_request_period
223 GC.enable; GC.start; GC.disable
224 end
225
226 def gc_countdown
227 if gc_request_period
228 @gc_request_countdown ||= gc_request_period
229 @gc_request_countdown -= 1
230 run_gc! if @gc_request_countdown <= 0
231 end
232 end
233
234 def close_connection(cgi)
235 cgi.instance_variable_get("@request").finish if cgi
236 end
237 end