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

root/branches/2-1-caching/railties/lib/fcgi_handler.rb

Revision 8132, 5.8 kB (checked in by bitsweat, 1 year ago)

FastCGI handler ignores unsupported signals like USR2 on Windows [Grzegorz Derebecki]

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       provider.each_cgi do |cgi|
77         process_request(cgi)
78
79         case when_ready
80           when :reload
81             reload!
82           when :restart
83             close_connection(cgi)
84             restart!
85           when :exit
86             close_connection(cgi)
87             break
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       @when_ready = 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     end
109
110     def logger
111       @logger ||= Logger.new(@log_file_path)
112     end
113
114     def dispatcher_log(level, msg)
115       time_str = Time.now.strftime("%d/%b/%Y:%H:%M:%S")
116       logger.send(level, "[#{time_str} :: #{$$}] #{msg}")
117     rescue Exception => log_error  # Logger errors
118       STDERR << "Couldn't write to #{@log_file_path.inspect}: #{msg}\n"
119       STDERR << "  #{log_error.class}: #{log_error.message}\n"
120     end
121
122     def dispatcher_error(e, msg = "")
123       error_message =
124         "Dispatcher failed to catch: #{e} (#{e.class})\n" +
125         "  #{e.backtrace.join("\n  ")}\n#{msg}"
126       dispatcher_log(:error, error_message)
127     end
128
129     def install_signal_handlers
130       GLOBAL_SIGNALS.each { |signal| install_signal_handler(signal) }
131     end
132
133     def install_signal_handler(signal, handler = nil)
134       if SIGNALS.include?(signal) && self.class.method_defined?(name = "#{SIGNALS[signal]}_handler")
135         handler ||= method(name).to_proc
136
137         begin
138           trap(signal, handler)
139         rescue ArgumentError
140           dispatcher_log :warn, "Ignoring unsupported signal #{signal}."
141         end
142       else
143         dispatcher_log :warn, "Ignoring unsupported signal #{signal}."
144       end
145     end
146
147     def with_signal_handler(signal)
148       install_signal_handler(signal)
149       yield
150     ensure
151       install_signal_handler(signal, 'DEFAULT')
152     end
153
154     def exit_now_handler(signal)
155       dispatcher_log :info, "asked to stop immediately"
156       exit
157     end
158
159     def exit_handler(signal)
160       dispatcher_log :info, "asked to stop ASAP"
161       @when_ready = :exit
162     end
163
164     def reload_handler(signal)
165       dispatcher_log :info, "asked to reload ASAP"
166       @when_ready = :reload
167     end
168
169     def restart_handler(signal)
170       dispatcher_log :info, "asked to restart ASAP"
171       @when_ready = :restart
172     end
173
174     def restart!
175       config       = ::Config::CONFIG
176       ruby         = File::join(config['bindir'], config['ruby_install_name']) + config['EXEEXT']
177       command_line = [ruby, $0, ARGV].flatten.join(' ')
178
179       dispatcher_log :info, "restarted"
180
181       # close resources as they won't be closed by
182       # the OS when using exec
183       logger.close rescue nil
184       RAILS_DEFAULT_LOGGER.close rescue nil
185
186       exec(command_line)
187     end
188
189     def reload!
190       run_gc! if gc_request_period
191       restore!
192       @when_ready = nil
193       dispatcher_log :info, "reloaded"
194     end
195
196     # Make a note of $" so we can safely reload this instance.
197     def mark_features!
198       @features = $".clone
199     end
200
201     def restore!
202       $".replace @features
203       Dispatcher.reset_application!
204       ActionController::Routing::Routes.reload
205     end
206
207     def run_gc!
208       @gc_request_countdown = gc_request_period
209       GC.enable; GC.start; GC.disable
210     end
211
212     def gc_countdown
213       if gc_request_period
214         @gc_request_countdown ||= gc_request_period
215         @gc_request_countdown -= 1
216         run_gc! if @gc_request_countdown <= 0
217       end
218     end
219
220     def close_connection(cgi)
221       cgi.instance_variable_get("@request").finish if cgi
222     end
223 end
Note: See TracBrowser for help on using the browser.