Ruby on Rails | Screencasts | Download | Documentation | Weblog | Community | Source

root/trunk/railties/lib/fcgi_handler.rb

Revision 9151, 6.1 kB (checked in by bitsweat, 4 months ago)

Handle exit, reload, and restart immediately if not processing a request. References #11471 [guillaume, Jeremy Kemper]

Line 
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   # Initialize and run the FastCGI instance, passing arguments through to new.
23   def self.process!(*args, &block)
24     new(*args, &block).process!
25   end
26
27   # Initialize the FastCGI instance with the path to a crash log
28   # detailing unhandled exceptions (default RAILS_ROOT/log/fastcgi.crash.log)
29   # and the number of requests to process between garbage collection runs
30   # (default nil for normal GC behavior.)  Optionally, pass a block which
31   # takes this instance as an argument for further configuration.
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     # Yield for additional configuration.
37     yield self if block_given?
38
39     # Safely install signal handlers.
40     install_signal_handlers
41
42     # Start error timestamp at 11 seconds ago.
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       # Retry if exceptions occur more than 10 seconds apart.
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  # 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_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     # 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(cgi)
237       cgi.instance_variable_get("@request").finish if cgi
238     end
239 end
Note: See TracBrowser for help on using the browser.