about summary refs log tree commit homepage
diff options
context:
space:
mode:
authorEric Wong <normalperson@yhbt.net>2009-03-29 17:38:01 -0700
committerEric Wong <normalperson@yhbt.net>2009-03-29 20:59:06 -0700
commitf65783b5a107fb515b46b17998b276d0fafd4be1 (patch)
treeec59efc0669bf81e6b04076ddecff571f11631ef
parent2886debbe557b93b9964038225e4cae58ebcedc9 (diff)
downloadunicorn-f65783b5a107fb515b46b17998b276d0fafd4be1.tar.gz
Instead of having global options for all listeners,
make all socket options per-listener.  This allows
reverse-proxies to pick different listeners to get
different options on different sockets.

  Given a cluster of machines (10.0.0.1, 10.0.0.2, 10.0.0.3)
  running Unicorn with the following config:

  ------------------ 8< ----------------
  listen "/tmp/local.sock", :backlog => 1
  listen "*:8080" # use the backlog=1024 default
  ------------------ 8< ----------------

  It is possible to configure a reverse proxy to try to use
  "/tmp/local.sock" first and then fall back to using the
  TCP listener on port 8080 in a failover configuration.

  Thus the nginx upstream configuration on 10.0.0.1 to
  compliment this would be:
  ------------------ 8< ----------------
  upstream unicorn_cluster {
    # reject connections ASAP if we are overloaded
    server unix:/tmp/local.sock;

    # fall back to other machines in the cluster via "backup"
    # listeners which have a large backlog queue.
    server 10.0.0.2:8080 backup;
    server 10.0.0.3:8080 backup;
  }
  ------------------ 8< ----------------

This removes the global "backlog" config option which
was inflexible with multiple machines in a cluster
and exposes the ability to change SO_SNDBUF/SO_RCVBUF
via setsockopt(2) for the first time.
-rw-r--r--TODO2
-rw-r--r--lib/unicorn.rb3
-rw-r--r--lib/unicorn/configurator.rb71
-rw-r--r--test/exec/test_exec.rb1
-rw-r--r--test/unit/test_configurator.rb24
5 files changed, 76 insertions, 25 deletions
diff --git a/TODO b/TODO
index 415f85e..9342cf1 100644
--- a/TODO
+++ b/TODO
@@ -20,8 +20,6 @@
 
   * tests for timeout
 
-  * per-listener backlog, sndbuf, rcvbuf settings in Configurator
-
   * QA behaviour on 1.9 (with Rails 2.3.x+)
 
   * manpages (why do so few Ruby executables come with proper manpages?)
diff --git a/lib/unicorn.rb b/lib/unicorn.rb
index 34189ef..d56f251 100644
--- a/lib/unicorn.rb
+++ b/lib/unicorn.rb
@@ -59,6 +59,7 @@ module Unicorn
       @request = @rd_sig = @wr_sig = nil
       @reexec_pid = 0
       @config = Configurator.new(options.merge(:use_defaults => true))
+      @listener_opts = {}
       @config.commit!(self, :skip => [:listeners, :pid])
       @listeners = []
     end
@@ -135,7 +136,7 @@ module Unicorn
     def listen(address)
       return if String === address && listener_names.include?(address)
 
-      if io = bind_listen(address, { :backlog => @backlog })
+      if io = bind_listen(address, @listener_opts[address] || {})
         if Socket == io.class
           @io_purgatory << io
           io = server_cast(io)
diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb
index b4713c5..4a085a1 100644
--- a/lib/unicorn/configurator.rb
+++ b/lib/unicorn/configurator.rb
@@ -43,7 +43,6 @@ module Unicorn
           server.logger.info("forked child re-executing...")
         },
       :pid => nil,
-      :backlog => 1024,
       :preload_app => false,
       :stderr_path => nil,
       :stdout_path => nil,
@@ -83,23 +82,6 @@ module Unicorn
       @set[key]
     end
 
-    # Changes the listen() syscall backlog to +nr+ for yet-to-be-created
-    # sockets.  Due to limitations of the OS, this cannot affect
-    # existing listener sockets in any way, sockets must be completely
-    # closed and rebound (inherited sockets preserve their existing
-    # backlog setting).  Some operating systems allow negative values
-    # here to specify the maximum allowable value.  See the listen(2)
-    # syscall documentation of your OS for the exact semantics of this.
-    #
-    # If you are running unicorn on multiple machines, lowering this number
-    # can help your load balancer detect when a machine is overloaded
-    # and give requests to a different machine.
-    def backlog(nr)
-      Integer === nr or raise ArgumentError,
-         "not an integer: backlog=#{nr.inspect}"
-      @set[:backlog] = nr
-    end
-
     # sets object to the +new+ Logger-like object.  The new logger-like
     # object must respond to the following methods:
     #  +debug+, +info+, +warn+, +error+, +fatal+, +close+
@@ -177,10 +159,57 @@ module Unicorn
       @set[:listeners] = addresses
     end
 
-    # adds an +address+ to the existing listener set
-    def listen(address)
+    # adds an +address+ to the existing listener set.
+    #
+    # The following options may be specified (but are generally not needed):
+    #
+    # +backlog+: this is the backlog of the listen() syscall.
+    #
+    #   Some operating systems allow negative values here to specify the
+    #   maximum allowable value.  In most cases, this number is only
+    #   recommendation and there are other OS-specific tunables and
+    #   variables that can affect this number.  See the listen(2)
+    #   syscall documentation of your OS for the exact semantics of
+    #   this.
+    #
+    #   If you are running unicorn on multiple machines, lowering this number
+    #   can help your load balancer detect when a machine is overloaded
+    #   and give requests to a different machine.
+    #
+    #   Default: 1024
+    #
+    # +rcvbuf+, +sndbuf+: maximum send and receive buffer sizes of sockets
+    #
+    #   These correspond to the SO_RCVBUF and SO_SNDBUF settings which
+    #   can be set via the setsockopt(2) syscall.  Some kernels
+    #   (e.g. Linux 2.4+) have intelligent auto-tuning mechanisms and
+    #   there is no need (and it is sometimes detrimental) to specify them.
+    #
+    #   See the socket API documentation of your operating system
+    #   to determine the exact semantics of these settings and
+    #   other operating system-specific knobs where they can be
+    #   specified.
+    #
+    #   Defaults: operating system defaults
+    #
+    # Due to limitations of the operating system, options specified here
+    # cannot affect existing listener sockets in any way, sockets must be
+    # completely closed and rebound.
+    def listen(address, opt = { :backlog => 1024 })
+      address = expand_addr(address)
+      if String === address
+        Hash === @set[:listener_opts] or
+          @set[:listener_opts] = Hash.new { |hash,key| hash[key] = {} }
+        [ :backlog, :sndbuf, :rcvbuf ].each do |key|
+          value = opt[key] or next
+          Integer === value or
+            raise ArgumentError, "not an integer: #{key}=#{value.inspect}"
+        end
+        @set[:listener_opts][address].merge!(opt)
+      end
+
       @set[:listeners] = [] unless Array === @set[:listeners]
-      @set[:listeners] << expand_addr(address)
+      @set[:listeners] << address
     end
 
     # sets the +path+ for the PID file of the unicorn master process
diff --git a/test/exec/test_exec.rb b/test/exec/test_exec.rb
index caada3b..5f91dc5 100644
--- a/test/exec/test_exec.rb
+++ b/test/exec/test_exec.rb
@@ -47,7 +47,6 @@ end
   HEAVY_CFG = <<-EOS
 worker_processes 4
 timeout 30
-backlog 128
 logger Logger.new('#{COMMON_TMP.path}')
 before_fork do |server, worker_nr|
   server.logger.info "before_fork: worker=\#{worker_nr}"
diff --git a/test/unit/test_configurator.rb b/test/unit/test_configurator.rb
index 8de0b13..623d717 100644
--- a/test/unit/test_configurator.rb
+++ b/test/unit/test_configurator.rb
@@ -45,4 +45,28 @@ class TestConfigurator < Test::Unit::TestCase
     assert_nil @logger
   end
 
+  def test_listen_options
+    tmp = Tempfile.new('unicorn_config')
+    expect = { :sndbuf => 1, :rcvbuf => 2, :backlog => 10 }.freeze
+    listener = "127.0.0.1:12345"
+    tmp.syswrite("listen '#{listener}', #{expect.inspect}\n")
+    cfg = nil
+    assert_nothing_raised do
+      cfg = Unicorn::Configurator.new(:config_file => tmp.path)
+    end
+    assert_nothing_raised { cfg.commit!(self) }
+    assert(listener_opts = instance_variable_get("@listener_opts"))
+    assert_equal expect, listener_opts[listener]
+  end
+
+  def test_listen_option_bad
+    tmp = Tempfile.new('unicorn_config')
+    expect = { :sndbuf => "five" }
+    listener = "127.0.0.1:12345"
+    tmp.syswrite("listen '#{listener}', #{expect.inspect}\n")
+    assert_raises(ArgumentError) do
+      Unicorn::Configurator.new(:config_file => tmp.path)
+    end
+  end
+
 end