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

root/tags/rel_1-2-1/railties/lib/fcgi_handler.rb

Revision 5558, 5.9 kB (checked in by david, 2 years ago)

Merged [5485] from trunk

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     'SIGTRAP' => :breakpoint
14   }
15   GLOBAL_SIGNALS = SIGNALS.keys - %w(USR1)
16
17   attr_reader :when_ready
18
19   attr_accessor :log_file_path
20   attr_accessor :gc_request_period
21
22
23   # Initialize and run the FastCGI instance, passing arguments through to new.
24   def self.process!(*args, &block)
25     new(*args, &block).process!
26   end
27
28   # Initialize the FastCGI instance with the path to a crash log
29   # detailing unhandled exceptions (default RAILS_ROOT/log/fastcgi.crash.log)
30   # and the number of requests to process between garbage collection runs
31   # (default nil for normal GC behavior.)  Optionally, pass a block which
32   # takes this instance as an argument for further configuration.
33   def initialize(log_file_path = nil, gc_request_period = nil)
34     self.log_file_path = log_file_path || "#{RAILS_ROOT}/log/fastcgi.crash.log"
35     self.gc_request_period = gc_request_period
36
37     # Yield for additional configuration.
38     yield self if block_given?
39
40     # Safely install signal handlers.
41     install_signal_handlers
42
43     # Start error timestamp at 11 seconds ago.
44     @last_error_on = Time.now - 11
45
46     dispatcher_log :info, "starting"
47   end
48
49   def process!(provider = FCGI)
50     # Make a note of $" so we can safely reload this instance.
51     mark!
52
53     run_gc! if gc_request_period
54
55     process_each_request!(provider)
56
57     GC.enable
58     dispatcher_log :info, "terminated gracefully"
59
60   rescue SystemExit => exit_error
61     dispatcher_log :info, "terminated by explicit exit"
62
63   rescue Exception => fcgi_error  # FCGI errors
64     # retry on errors that would otherwise have terminated the FCGI process,
65     # but only if they occur more than 10 seconds apart.
66     if !(SignalException === fcgi_error) && Time.now - @last_error_on > 10
67       @last_error_on = Time.now
68       dispatcher_error(fcgi_error, "almost killed by this error")
69       retry
70     else
71       dispatcher_error(fcgi_error, "killed by this error")
72     end
73   end
74
75
76   protected
77     def logger
78       @logger ||= Logger.new(@log_file_path)
79     end
80
81     def dispatcher_log(level, msg)
82       time_str = Time.now.strftime("%d/%b/%Y:%H:%M:%S")
83       logger.send(level, "[#{time_str} :: #{$$}] #{msg}")
84     rescue Exception => log_error  # Logger errors
85       STDERR << "Couldn't write to #{@log_file_path.inspect}: #{msg}\n"
86       STDERR << "  #{log_error.class}: #{log_error.message}\n"
87     end
88
89     def dispatcher_error(e, msg = "")
90       error_message =
91         "Dispatcher failed to catch: #{e} (#{e.class})\n" +
92         "  #{e.backtrace.join("\n  ")}\n#{msg}"
93       dispatcher_log(:error, error_message)
94     end
95
96     def install_signal_handlers
97       GLOBAL_SIGNALS.each { |signal| install_signal_handler(signal) }
98     end
99
100     def install_signal_handler(signal, handler = nil)
101       handler ||= method("#{SIGNALS[signal]}_handler").to_proc
102       trap(signal, handler)
103     rescue ArgumentError
104       dispatcher_log :warn, "Ignoring unsupported signal #{signal}."
105     end
106
107     def with_signal_handler(signal)
108       install_signal_handler(signal)
109       yield
110     ensure
111       install_signal_handler(signal, 'DEFAULT')
112     end
113
114     def exit_now_handler(signal)
115       dispatcher_log :info, "asked to terminate immediately"
116       exit
117     end
118
119     def exit_handler(signal)
120       dispatcher_log :info, "asked to terminate ASAP"
121       @when_ready = :exit
122     end
123
124     def reload_handler(signal)
125       dispatcher_log :info, "asked to reload ASAP"
126       @when_ready = :reload
127     end
128
129     def restart_handler(signal)
130       dispatcher_log :info, "asked to restart ASAP"
131       @when_ready = :restart
132     end
133
134     def breakpoint_handler(signal)
135       dispatcher_log :info, "asked to breakpoint ASAP"
136       @when_ready = :breakpoint
137     end
138
139     def process_each_request!(provider)
140       cgi = nil
141       provider.each_cgi do |cgi|
142         with_signal_handler 'USR1' do
143           process_request(cgi)
144         end
145
146         case when_ready
147           when :reload
148             reload!
149           when :restart
150             close_connection(cgi)
151             restart!
152           when :exit
153             close_connection(cgi)
154             break
155           when :breakpoint
156             close_connection(cgi)
157             breakpoint!
158         end
159
160         gc_countdown
161       end
162     rescue SignalException => signal
163       raise unless signal.message == 'SIGUSR1'
164       close_connection(cgi) if cgi
165     end
166
167     def process_request(cgi)
168       Dispatcher.dispatch(cgi)
169     rescue Exception => e  # errors from CGI dispatch
170       raise if SignalException === e
171       dispatcher_error(e)
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       exec(command_line)
182     end
183
184     def reload!
185       run_gc! if gc_request_period
186       restore!
187       @when_ready = nil
188       dispatcher_log :info, "reloaded"
189     end
190
191     def mark!
192       @features = $".clone
193     end
194
195     def restore!
196       $".replace @features
197       Dispatcher.reset_application!
198       ActionController::Routing::Routes.reload
199     end
200
201     def breakpoint!
202       require 'breakpoint'
203       port = defined?(BREAKPOINT_SERVER_PORT) ? BREAKPOINT_SERVER_PORT : 42531
204       Breakpoint.activate_drb("druby://localhost:#{port}", nil, !defined?(FastCGI))
205       dispatcher_log :info, "breakpointing"
206       breakpoint
207       @when_ready = nil
208     end
209
210     def run_gc!
211       @gc_request_countdown = gc_request_period
212       GC.enable; GC.start; GC.disable
213     end
214
215     def gc_countdown
216       if gc_request_period
217         @gc_request_countdown -= 1
218         run_gc! if @gc_request_countdown <= 0
219       end
220     end
221
222     def close_connection(cgi)
223       cgi.instance_variable_get("@request").finish
224     end
225 end
Note: See TracBrowser for help on using the browser.