about summary refs log tree commit homepage
path: root/lib/unicorn/configurator.rb
diff options
context:
space:
mode:
Diffstat (limited to 'lib/unicorn/configurator.rb')
-rw-r--r--lib/unicorn/configurator.rb237
1 files changed, 138 insertions, 99 deletions
diff --git a/lib/unicorn/configurator.rb b/lib/unicorn/configurator.rb
index 860962a..f6d13ab 100644
--- a/lib/unicorn/configurator.rb
+++ b/lib/unicorn/configurator.rb
@@ -1,38 +1,24 @@
+# -*- encoding: binary -*-
+
 require 'socket'
 require 'logger'
 
 module Unicorn
 
-  # Implements a simple DSL for configuring a unicorn server.
+  # Implements a simple DSL for configuring a Unicorn server.
   #
-  # Example (when used with the unicorn config file):
-  #   worker_processes 4
-  #   listen '/tmp/my_app.sock', :backlog => 1
-  #   listen '0.0.0.0:9292'
-  #   timeout 10
-  #   pid "/tmp/my_app.pid"
-  #   after_fork do |server,worker|
-  #     server.listen("127.0.0.1:#{9293 + worker.nr}") rescue nil
-  #   end
-  class Configurator
-    # The default logger writes its output to $stderr
-    DEFAULT_LOGGER = Logger.new($stderr) unless defined?(DEFAULT_LOGGER)
+  # See http://unicorn.bogomips.org/examples/unicorn.conf.rb for an
+  # example config file.  An example config file for use with nginx is
+  # also available at http://unicorn.bogomips.org/examples/nginx.conf
+  class Configurator < Struct.new(:set, :config_file)
 
     # Default settings for Unicorn
     DEFAULTS = {
       :timeout => 60,
-      :listeners => [],
-      :logger => DEFAULT_LOGGER,
+      :logger => Logger.new($stderr),
       :worker_processes => 1,
       :after_fork => lambda { |server, worker|
           server.logger.info("worker=#{worker.nr} spawned pid=#{$$}")
-
-          # per-process listener ports for debugging/admin:
-          # "rescue nil" statement is needed because USR2 will
-          # cause the master process to reexecute itself and the
-          # per-worker ports can be taken, necessitating another
-          # HUP after QUIT-ing the original master:
-          # server.listen("127.0.0.1:#{8081 + worker.nr}") rescue nil
         },
       :before_fork => lambda { |server, worker|
           server.logger.info("worker=#{worker.nr} spawning...")
@@ -42,42 +28,44 @@ module Unicorn
         },
       :pid => nil,
       :preload_app => false,
-      :stderr_path => nil,
-      :stdout_path => nil,
     }
 
-    attr_reader :config_file #:nodoc:
-
     def initialize(defaults = {}) #:nodoc:
-      @set = Hash.new(:unset)
+      self.set = Hash.new(:unset)
       use_defaults = defaults.delete(:use_defaults)
-      @config_file = defaults.delete(:config_file)
-      @config_file.freeze
-      @set.merge!(DEFAULTS) if use_defaults
+      self.config_file = defaults.delete(:config_file)
+      set.merge!(DEFAULTS) if use_defaults
       defaults.each { |key, value| self.send(key, value) }
+      Hash === set[:listener_opts] or
+          set[:listener_opts] = Hash.new { |hash,key| hash[key] = {} }
+      Array === set[:listeners] or set[:listeners] = []
       reload
     end
 
     def reload #:nodoc:
-      instance_eval(File.read(@config_file)) if @config_file
+      instance_eval(File.read(config_file), config_file) if config_file
+
+      # working_directory binds immediately (easier error checking that way),
+      # now ensure any paths we changed are correctly set.
+      [ :pid, :stderr_path, :stdout_path ].each do |var|
+        String === (path = set[var]) or next
+        path = File.expand_path(path)
+        test(?w, path) || test(?w, File.dirname(path)) or \
+              raise ArgumentError, "directory for #{var}=#{path} not writable"
+      end
     end
 
     def commit!(server, options = {}) #:nodoc:
       skip = options[:skip] || []
-      @set.each do |key, value|
-        (Symbol === value && value == :unset) and next
+      set.each do |key, value|
+        value == :unset and next
         skip.include?(key) and next
-        setter = "#{key}="
-        if server.respond_to?(setter)
-          server.send(setter, value)
-        else
-          server.instance_variable_set("@#{key}", value)
-        end
+        server.__send__("#{key}=", value)
       end
     end
 
     def [](key) # :nodoc:
-      @set[key]
+      set[key]
     end
 
     # sets object to the +new+ Logger-like object.  The new logger-like
@@ -89,7 +77,7 @@ module Unicorn
         raise ArgumentError, "logger=#{new} does not respond to method=#{m}"
       end
 
-      @set[:logger] = new
+      set[:logger] = new
     end
 
     # sets after_fork hook to a given block.  This block will be called by
@@ -98,25 +86,18 @@ module Unicorn
     #
     #  after_fork do |server,worker|
     #    # per-process listener ports for debugging/admin:
-    #    # "rescue nil" statement is needed because USR2 will
-    #    # cause the master process to reexecute itself and the
-    #    # per-worker ports can be taken, necessitating another
-    #    # HUP after QUIT-ing the original master:
-    #    server.listen("127.0.0.1:#{9293 + worker.nr}") rescue nil
+    #    addr = "127.0.0.1:#{9293 + worker.nr}"
+    #
+    #    # the negative :tries parameter indicates we will retry forever
+    #    # waiting on the existing process to exit with a 5 second :delay
+    #    # Existing options for Unicorn::Configurator#listen such as
+    #    # :backlog, :rcvbuf, :sndbuf are available here as well.
+    #    server.listen(addr, :tries => -1, :delay => 5, :backlog => 128)
     #
     #    # drop permissions to "www-data" in the worker
     #    # generally there's no reason to start Unicorn as a priviledged user
     #    # as it is not recommended to expose Unicorn to public clients.
-    #    uid, gid = Process.euid, Process.egid
-    #    user, group = 'www-data', 'www-data'
-    #    target_uid = Etc.getpwnam(user).uid
-    #    target_gid = Etc.getgrnam(group).gid
-    #    worker.tempfile.chown(target_uid, target_gid)
-    #    if uid != target_uid || gid != target_gid
-    #      Process.initgroups(user, target_gid)
-    #      Process::GID.change_privilege(target_gid)
-    #      Process::UID.change_privilege(target_uid)
-    #    end
+    #    worker.user('www-data', 'www-data') if Process.euid == 0
     #  end
     def after_fork(*args, &block)
       set_hook(:after_fork, block_given? ? block : args[0])
@@ -146,22 +127,43 @@ module Unicorn
     # to the scheduling limitations by the worker process.  Due the
     # low-complexity, low-overhead implementation, timeouts of less
     # than 3.0 seconds can be considered inaccurate and unsafe.
+    #
+    # For running Unicorn behind nginx, it is recommended to set
+    # "fail_timeout=0" for in your nginx configuration like this
+    # to have nginx always retry backends that may have had workers
+    # SIGKILL-ed due to timeouts.
+    #
+    #    # See http://wiki.nginx.org/NginxHttpUpstreamModule for more details
+    #    # on nginx upstream configuration:
+    #    upstream unicorn_backend {
+    #      # for UNIX domain socket setups:
+    #      server unix:/path/to/unicorn.sock fail_timeout=0;
+    #
+    #      # for TCP setups
+    #      server 192.168.0.7:8080 fail_timeout=0;
+    #      server 192.168.0.8:8080 fail_timeout=0;
+    #      server 192.168.0.9:8080 fail_timeout=0;
+    #    }
     def timeout(seconds)
       Numeric === seconds or raise ArgumentError,
                                   "not numeric: timeout=#{seconds.inspect}"
       seconds >= 3 or raise ArgumentError,
                                   "too low: timeout=#{seconds.inspect}"
-      @set[:timeout] = seconds
+      set[:timeout] = seconds
     end
 
     # sets the current number of worker_processes to +nr+.  Each worker
-    # process will serve exactly one client at a time.
+    # process will serve exactly one client at a time.  You can
+    # increment or decrement this value at runtime by sending SIGTTIN
+    # or SIGTTOU respectively to the master process without reloading
+    # the rest of your Unicorn configuration.  See the SIGNALS document
+    # for more information.
     def worker_processes(nr)
       Integer === nr or raise ArgumentError,
                              "not an integer: worker_processes=#{nr.inspect}"
       nr >= 0 or raise ArgumentError,
                              "not non-negative: worker_processes=#{nr.inspect}"
-      @set[:worker_processes] = nr
+      set[:worker_processes] = nr
     end
 
     # sets listeners to the given +addresses+, replacing or augmenting the
@@ -172,14 +174,14 @@ module Unicorn
     def listeners(addresses) # :nodoc:
       Array === addresses or addresses = Array(addresses)
       addresses.map! { |addr| expand_addr(addr) }
-      @set[:listeners] = addresses
+      set[:listeners] = addresses
     end
 
     # 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.
+    # +: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
@@ -194,7 +196,7 @@ module Unicorn
     #
     # Default: 1024
     #
-    # +rcvbuf+, +sndbuf+: maximum send and receive buffer sizes of sockets
+    # +:rcvbuf+, +:sndbuf+: maximum receive and send 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
@@ -208,13 +210,13 @@ module Unicorn
     #
     # Defaults: operating system defaults
     #
-    # +tcp_nodelay+: disables Nagle's algorithm on TCP sockets
+    # +:tcp_nodelay+: disables Nagle's algorithm on TCP sockets
     #
     # This has no effect on UNIX sockets.
     #
     # Default: operating system defaults (usually Nagle's algorithm enabled)
     #
-    # +tcp_nopush+: enables TCP_CORK in Linux or TCP_NOPUSH in FreeBSD
+    # +:tcp_nopush+: enables TCP_CORK in Linux or TCP_NOPUSH in FreeBSD
     #
     # This will prevent partial TCP frames from being sent out.
     # Enabling +tcp_nopush+ is generally not needed or recommended as
@@ -224,12 +226,33 @@ module Unicorn
     #
     # This has no effect on UNIX sockets.
     #
+    # +:tries+: times to retry binding a socket if it is already in use
+    #
+    # A negative number indicates we will retry indefinitely, this is
+    # useful for migrations and upgrades when individual workers
+    # are binding to different ports.
+    #
+    # Default: 5
+    #
+    # +:delay+: seconds to wait between successive +tries+
+    #
+    # Default: 0.5 seconds
+    #
+    # +:umask+: sets the file mode creation mask for UNIX sockets
+    #
+    # Typically UNIX domain sockets are created with more liberal
+    # file permissions than the rest of the application.  By default,
+    # we create UNIX domain sockets to be readable and writable by
+    # all local users to give them the same accessibility as
+    # locally-bound TCP listeners.
+    #
+    # This has no effect on TCP listeners.
+    #
+    # Default: 0 (world read/writable)
     def listen(address, opt = {})
       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|
+        [ :umask, :backlog, :sndbuf, :rcvbuf, :tries ].each do |key|
           value = opt[key] or next
           Integer === value or
             raise ArgumentError, "not an integer: #{key}=#{value.inspect}"
@@ -239,11 +262,14 @@ module Unicorn
           TrueClass === value || FalseClass === value or
             raise ArgumentError, "not boolean: #{key}=#{value.inspect}"
         end
-        @set[:listener_opts][address].merge!(opt)
+        unless (value = opt[:delay]).nil?
+          Numeric === value or
+            raise ArgumentError, "not numeric: delay=#{value.inspect}"
+        end
+        set[:listener_opts][address].merge!(opt)
       end
 
-      @set[:listeners] = [] unless Array === @set[:listeners]
-      @set[:listeners] << address
+      set[:listeners] << address
     end
 
     # sets the +path+ for the PID file of the unicorn master process
@@ -265,7 +291,7 @@ module Unicorn
     def preload_app(bool)
       case bool
       when TrueClass, FalseClass
-        @set[:preload_app] = bool
+        set[:preload_app] = bool
       else
         raise ArgumentError, "preload_app=#{bool.inspect} not a boolean"
       end
@@ -286,35 +312,21 @@ module Unicorn
       set_path(:stdout_path, path)
     end
 
-    private
-
-    def set_path(var, path) #:nodoc:
-      case path
-      when NilClass
-      when String
-        path = File.expand_path(path)
-        File.writable?(File.dirname(path)) or \
-               raise ArgumentError, "directory for #{var}=#{path} not writable"
-      else
-        raise ArgumentError
+    # sets the working directory for Unicorn.  This ensures USR2 will
+    # start a new instance of Unicorn in this directory.  This may be
+    # a symlink.
+    def working_directory(path)
+      # just let chdir raise errors
+      path = File.expand_path(path)
+      if config_file &&
+         config_file[0] != ?/ &&
+         ! test(?r, "#{path}/#{config_file}")
+        raise ArgumentError,
+              "config_file=#{config_file} would not be accessible in" \
+              " working_directory=#{path}"
       end
-      @set[var] = path
-    end
-
-    def set_hook(var, my_proc, req_arity = 2) #:nodoc:
-      case my_proc
-      when Proc
-        arity = my_proc.arity
-        (arity == req_arity) or \
-          raise ArgumentError,
-                "#{var}=#{my_proc.inspect} has invalid arity: " \
-                "#{arity} (need #{req_arity})"
-      when NilClass
-        my_proc = DEFAULTS[var]
-      else
-        raise ArgumentError, "invalid type: #{var}=#{my_proc.inspect}"
-      end
-      @set[var] = my_proc
+      Dir.chdir(path)
+      HttpServer::START_CTX[:cwd] = ENV["PWD"] = path
     end
 
     # expands "unix:path/to/foo" to a socket relative to the current path
@@ -340,5 +352,32 @@ module Unicorn
       end
     end
 
+  private
+
+    def set_path(var, path) #:nodoc:
+      case path
+      when NilClass, String
+        set[var] = path
+      else
+        raise ArgumentError
+      end
+    end
+
+    def set_hook(var, my_proc, req_arity = 2) #:nodoc:
+      case my_proc
+      when Proc
+        arity = my_proc.arity
+        (arity == req_arity) or \
+          raise ArgumentError,
+                "#{var}=#{my_proc.inspect} has invalid arity: " \
+                "#{arity} (need #{req_arity})"
+      when NilClass
+        my_proc = DEFAULTS[var]
+      else
+        raise ArgumentError, "invalid type: #{var}=#{my_proc.inspect}"
+      end
+      set[var] = my_proc
+    end
+
   end
 end