Skip to content
This repository
Browse code

r1588@asus: jeremy | 2005-07-02 03:14:45 -0700

 Optional periodic garbage collection for dispatch.fcgi.  Graceful exit on TERM also (a la Apache1).  Ignore signals the platform does not support, such as USR1 on Windows.


git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1592 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
  • Loading branch information...
commit 5650bc90940bd850a2f44bd3b58549d6b336dcd0 1 parent b829493
Jeremy Kemper authored July 02, 2005
4  railties/CHANGELOG
... ...
@@ -1,5 +1,9 @@
1 1
 *SVN*
2 2
 
  3
+* SIGTERM also gracefully exits dispatch.fcgi.  Ignore SIGUSR1 on Windows.
  4
+
  5
+* Add the option to manually manage garbage collection in the FastCGI dispatcher.  Set the number of requests between GC runs in your public/dispatch.fcgi.  [skaes@web.de]
  6
+
3 7
 * Allow dynamic application reloading for dispatch.fcgi processes by sending a SIGHUP. If the process is currently handling a request, the request will be allowed to complete first. This allows production fcgi's to be reloaded without having to restart them.
4 8
 
5 9
 * RailsFCGIHandler (dispatch.fcgi) no longer tries to explicitly flush $stdout (CgiProcess#out always calls flush)
20  railties/dispatches/dispatch.fcgi
... ...
@@ -1,5 +1,23 @@
1 1
 #!/usr/local/bin/ruby
2  
-
  2
+#
  3
+# You may specify the path to the FastCGI crash log (a log of unhandled
  4
+# exceptions which forced the FastCGI instance to exit, great for debugging)
  5
+# and the number of requests to process before running garbage collection.
  6
+#
  7
+# By default, the FastCGI crash log is RAILS_ROOT/log/fastcgi.crash.log
  8
+# and the GC period is nil (turned off).  A reasonable number of requests
  9
+# could range from 10-100 depending on the memory footprint of your app.
  10
+#
  11
+# Example:
  12
+#   # Default log path, normal GC behavior.
  13
+#   RailsFCGIHandler.process!
  14
+#
  15
+#   # Default log path, 50 requests between GC.
  16
+#   RailsFCGIHandler.process! nil, 50
  17
+#
  18
+#   # Custom log path, normal GC behavior.
  19
+#   RailsFCGIHandler.process! '/var/log/myapp_fcgi_crash.log'
  20
+#
3 21
 require File.dirname(__FILE__) + "/../config/environment"
4 22
 require 'fcgi_handler'
5 23
 
79  railties/lib/fcgi_handler.rb
@@ -3,41 +3,77 @@
3 3
 require 'dispatcher'
4 4
 
5 5
 class RailsFCGIHandler
  6
+  SIGNALS = {
  7
+    'HUP'  => :reload,
  8
+    'TERM' => :graceful_exit,
  9
+    'USR1' => :graceful_exit
  10
+  }
  11
+
6 12
   attr_reader :when_ready
7 13
   attr_reader :processing
8 14
 
9  
-  def self.process!
10  
-    new.process!
  15
+  attr_accessor :log_file_path
  16
+  attr_accessor :gc_request_period
  17
+
  18
+
  19
+  # Initialize and run the FastCGI instance, passing arguments through to new.
  20
+  def self.process!(*args, &block)
  21
+    new(*args, &block).process!
11 22
   end
12 23
 
13  
-  def initialize(log_file_path = "#{RAILS_ROOT}/log/fastcgi.crash.log")
  24
+  # Initialize the FastCGI instance with the path to a crash log
  25
+  # detailing unhandled exceptions (default RAILS_ROOT/log/fastcgi.crash.log)
  26
+  # and the number of requests to process between garbage collection runs
  27
+  # (default nil for normal GC behavior.)  Optionally, pass a block which
  28
+  # takes this instance as an argument for further configuration.
  29
+  def initialize(log_file_path = nil, gc_request_period = nil)
14 30
     @when_ready = nil
15 31
     @processing = false
16 32
 
17  
-    trap("HUP",  method(:restart_handler).to_proc)
18  
-    trap("USR1", method(:trap_handler).to_proc)
  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?
19 38
 
20  
-    # initialize to 11 seconds ago to minimize special cases
  39
+    # Safely install signal handlers.
  40
+    install_signal_handlers
  41
+
  42
+    # Start error timestamp at 11 seconds ago.
21 43
     @last_error_on = Time.now - 11
22 44
 
23  
-    @log_file_path = log_file_path
24 45
     dispatcher_log(:info, "starting")
25 46
   end
26 47
 
27 48
   def process!
  49
+    # Make a note of $" so we can safely reload this instance.
28 50
     mark!
29 51
 
  52
+    # Begin countdown to garbage collection.
  53
+    run_gc! if gc_request_period
  54
+
30 55
     FCGI.each_cgi do |cgi| 
31  
-      if when_ready == :restart
  56
+      # Safely reload this instance if requested.
  57
+      if when_ready == :reload
  58
+        run_gc! if gc_request_period
32 59
         restore!
33 60
         @when_ready = nil
34  
-        dispatcher_log(:info, "restarted")
  61
+        dispatcher_log(:info, "reloaded")
35 62
       end
36 63
 
37 64
       process_request(cgi)
  65
+
  66
+      # Break if graceful exit requested.
38 67
       break if when_ready == :exit
  68
+
  69
+      # Garbage collection countdown.
  70
+      if gc_request_period
  71
+        @gc_request_countdown -= 1
  72
+        run_gc! if @gc_request_countdown <= 0
  73
+      end
39 74
     end
40 75
 
  76
+    GC.enable
41 77
     dispatcher_log(:info, "terminated gracefully")
42 78
 
43 79
   rescue SystemExit => exit_error
@@ -75,7 +111,19 @@ def dispatcher_error(e,msg="")
75 111
       dispatcher_log(:error, error_message)
76 112
     end
77 113
 
78  
-    def trap_handler(signal)
  114
+    def install_signal_handlers
  115
+      SIGNALS.each do |signal, handler_name|
  116
+        install_signal_handler signal, method("#{handler_name}_handler").to_proc
  117
+      end
  118
+    end
  119
+
  120
+    def install_signal_handler(signal, handler)
  121
+      trap signal, handler
  122
+    rescue ArgumentError
  123
+      dispatcher_log :warn, "Ignoring unsupported signal #{signal}."
  124
+    end
  125
+
  126
+    def graceful_exit_handler(signal)
79 127
       if processing
80 128
         dispatcher_log :info, "asked to terminate ASAP"
81 129
         @when_ready = :exit
@@ -85,9 +133,9 @@ def trap_handler(signal)
85 133
       end
86 134
     end
87 135
 
88  
-    def restart_handler(signal)
89  
-      @when_ready = :restart
90  
-      dispatcher_log :info, "asked to restart ASAP"
  136
+    def reload_handler(signal)
  137
+      @when_ready = :reload
  138
+      dispatcher_log :info, "asked to reload ASAP"
91 139
     end
92 140
 
93 141
     def process_request(cgi)
@@ -109,4 +157,9 @@ def restore!
109 157
       Dispatcher.reset_application!
110 158
       ActionController::Routing::Routes.reload
111 159
     end
  160
+
  161
+    def run_gc!
  162
+      @gc_request_countdown = gc_request_period
  163
+      GC.enable; GC.start; GC.disable
  164
+    end
112 165
 end
68  railties/test/fcgi_dispatcher_test.rb
@@ -9,8 +9,9 @@
9 9
 
10 10
 class RailsFCGIHandler
11 11
   attr_reader :exit_code
12  
-  attr_reader :restarted
  12
+  attr_reader :reloaded
13 13
   attr_accessor :thread
  14
+  attr_reader :gc_runs
14 15
 
15 16
   def trap(signal, handler, &block)
16 17
     handler ||= block
@@ -27,7 +28,14 @@ def send_signal(which)
27 28
   end
28 29
 
29 30
   def restore!
30  
-    @restarted = true
  31
+    @reloaded = true
  32
+  end
  33
+
  34
+  alias_method :old_run_gc!, :run_gc!
  35
+  def run_gc!
  36
+    @gc_runs ||= 0
  37
+    @gc_runs += 1
  38
+    old_run_gc!
31 39
   end
32 40
 end
33 41
 
@@ -57,7 +65,7 @@ def test_interrupted_via_HUP_when_not_in_request
57 65
     assert_nil @handler.exit_code
58 66
     assert_nil @handler.when_ready
59 67
     assert !@handler.processing
60  
-    assert @handler.restarted
  68
+    assert @handler.reloaded
61 69
   end
62 70
 
63 71
   def test_interrupted_via_HUP_when_in_request
@@ -67,7 +75,7 @@ def test_interrupted_via_HUP_when_in_request
67 75
     @handler.send_signal("HUP")
68 76
     @handler.thread.join
69 77
     assert_nil @handler.exit_code
70  
-    assert_equal :restart, @handler.when_ready
  78
+    assert_equal :reload, @handler.when_ready
71 79
     assert !@handler.processing
72 80
   end
73 81
 
@@ -119,3 +127,55 @@ def test_interrupted_via_USR1_when_in_request
119 127
     end
120 128
   end
121 129
 end
  130
+
  131
+class RailsFCGIHandlerPeriodicGCTest < Test::Unit::TestCase
  132
+  def setup
  133
+    @log = StringIO.new
  134
+    FCGI.time_to_sleep = nil
  135
+    FCGI.raise_exception = nil
  136
+    FCGI.each_cgi_count = nil
  137
+    Dispatcher.time_to_sleep = nil
  138
+    Dispatcher.raise_exception = nil
  139
+    Dispatcher.dispatch_hook = nil
  140
+  end
  141
+
  142
+  def teardown
  143
+    FCGI.each_cgi_count = nil
  144
+    Dispatcher.dispatch_hook = nil
  145
+    GC.enable
  146
+  end
  147
+
  148
+  def test_normal_gc
  149
+    @handler = RailsFCGIHandler.new(@log)
  150
+    assert_nil @handler.gc_request_period
  151
+
  152
+    # When GC is enabled, GC.disable disables and returns false.
  153
+    assert_equal false, GC.disable
  154
+  end
  155
+
  156
+  def test_periodic_gc
  157
+    Dispatcher.dispatch_hook = lambda do |cgi|
  158
+      # When GC is disabled, GC.enable enables and returns true.
  159
+      assert_equal true, GC.enable
  160
+      GC.disable
  161
+    end
  162
+
  163
+    @handler = RailsFCGIHandler.new(@log, 10)
  164
+    assert_equal 10, @handler.gc_request_period
  165
+    FCGI.each_cgi_count = 1
  166
+    @handler.process!
  167
+    assert_equal 1, @handler.gc_runs
  168
+
  169
+    FCGI.each_cgi_count = 10
  170
+    @handler.process!
  171
+    assert_equal 3, @handler.gc_runs
  172
+
  173
+    FCGI.each_cgi_count = 25
  174
+    @handler.process!
  175
+    assert_equal 6, @handler.gc_runs
  176
+
  177
+    assert_nil @handler.exit_code
  178
+    assert_nil @handler.when_ready
  179
+    assert !@handler.processing
  180
+  end
  181
+end
2  railties/test/mocks/dispatcher.rb
@@ -2,8 +2,10 @@ class Dispatcher
2 2
   class <<self
3 3
     attr_accessor :time_to_sleep
4 4
     attr_accessor :raise_exception
  5
+    attr_accessor :dispatch_hook
5 6
 
6 7
     def dispatch(cgi)
  8
+      dispatch_hook.call(cgi) if dispatch_hook
7 9
       sleep(time_to_sleep || 0)
8 10
       raise raise_exception, "Something died" if raise_exception
9 11
     end
9  railties/test/mocks/fcgi.rb
@@ -2,11 +2,14 @@ class FCGI
2 2
   class << self
3 3
     attr_accessor :time_to_sleep
4 4
     attr_accessor :raise_exception
  5
+    attr_accessor :each_cgi_count
5 6
 
6 7
     def each_cgi
7  
-      sleep(time_to_sleep || 0)
8  
-      raise raise_exception, "Something died" if raise_exception
9  
-      yield "mock cgi value"
  8
+      (each_cgi_count || 1).times do
  9
+        sleep(time_to_sleep || 0)
  10
+        raise raise_exception, "Something died" if raise_exception
  11
+        yield "mock cgi value"
  12
+      end
10 13
     end
11 14
   end
12 15
 end

0 notes on commit 5650bc9

Please sign in to comment.
Something went wrong with that request. Please try again.