socket_helper: reduce constant lookups and caching
[unicorn.git] / lib / unicorn / socket_helper.rb
blob2ecf43844dd57c519c1e3f383050408923d48013
1 # -*- encoding: binary -*-
2 # :enddoc:
3 require 'socket'
5 module Unicorn
6   module SocketHelper
8     # prevents IO objects in here from being GC-ed
9     # kill this when we drop 1.8 support
10     IO_PURGATORY = []
12     # internal interface, only used by Rainbows!/Zbatery
13     DEFAULTS = {
14       # The semantics for TCP_DEFER_ACCEPT changed in Linux 2.6.32+
15       # with commit d1b99ba41d6c5aa1ed2fc634323449dd656899e9
16       # This change shouldn't affect Unicorn users behind nginx (a
17       # value of 1 remains an optimization), but Rainbows! users may
18       # want to use a higher value on Linux 2.6.32+ to protect against
19       # denial-of-service attacks
20       :tcp_defer_accept => 1,
22       # FreeBSD, we need to override this to 'dataready' if we
23       # eventually get HTTPS support
24       :accept_filter => 'httpready',
26       # same default value as Mongrel
27       :backlog => 1024,
29       # favor latency over bandwidth savings
30       :tcp_nopush => nil,
31       :tcp_nodelay => true,
32     }
34     # configure platform-specific options (only tested on Linux 2.6 so far)
35     def accf_arg(af_name)
36       [ af_name, nil ].pack('a16a240')
37     end if RUBY_PLATFORM =~ /freebsd/ && Socket.const_defined?(:SO_ACCEPTFILTER)
39     def prevent_autoclose(io)
40       if io.respond_to?(:autoclose=)
41         io.autoclose = false
42       else
43         IO_PURGATORY << io
44       end
45     end
47     def set_tcp_sockopt(sock, opt)
48       # just in case, even LANs can break sometimes.  Linux sysadmins
49       # can lower net.ipv4.tcp_keepalive_* sysctl knobs to very low values.
50       Socket.const_defined?(:SO_KEEPALIVE) and
51         sock.setsockopt(:SOL_SOCKET, :SO_KEEPALIVE, 1)
53       if Socket.const_defined?(:TCP_NODELAY)
54         val = opt[:tcp_nodelay]
55         val = DEFAULTS[:tcp_nodelay] if val.nil?
56         sock.setsockopt(:IPPROTO_TCP, :TCP_NODELAY, val ? 1 : 0)
57       end
59       val = opt[:tcp_nopush]
60       unless val.nil?
61         if Socket.const_defined?(:TCP_CORK) # Linux
62           sock.setsockopt(:IPPROTO_TCP, :TCP_CORK, val)
63         elsif Socket.const_defined?(:TCP_NOPUSH) # FreeBSD
64           sock.setsockopt(:IPPROTO_TCP, :TCP_NOPUSH, val)
65         end
66       end
68       # No good reason to ever have deferred accepts off
69       # (except maybe benchmarking)
70       if Socket.const_defined?(:TCP_DEFER_ACCEPT)
71         # this differs from nginx, since nginx doesn't allow us to
72         # configure the the timeout...
73         seconds = opt[:tcp_defer_accept]
74         seconds = DEFAULTS[:tcp_defer_accept] if [true,nil].include?(seconds)
75         seconds = 0 unless seconds # nil/false means disable this
76         sock.setsockopt(:IPPROTO_TCP, :TCP_DEFER_ACCEPT, seconds)
77       elsif respond_to?(:accf_arg)
78         name = opt[:accept_filter]
79         name = DEFAULTS[:accept_filter] if name.nil?
80         begin
81           sock.setsockopt(:SOL_SOCKET, :SO_ACCEPTFILTER, accf_arg(name))
82         rescue => e
83           logger.error("#{sock_name(sock)} " \
84                        "failed to set accept_filter=#{name} (#{e.inspect})")
85         end
86       end
87     end
89     def set_server_sockopt(sock, opt)
90       opt = DEFAULTS.merge(opt || {})
92       TCPSocket === sock and set_tcp_sockopt(sock, opt)
94       rcvbuf, sndbuf = opt.values_at(:rcvbuf, :sndbuf)
95       if rcvbuf || sndbuf
96         log_buffer_sizes(sock, "before: ")
97         sock.setsockopt(:SOL_SOCKET, :SO_RCVBUF, rcvbuf) if rcvbuf
98         sock.setsockopt(:SOL_SOCKET, :SO_SNDBUF, sndbuf) if sndbuf
99         log_buffer_sizes(sock, " after: ")
100       end
101       sock.listen(opt[:backlog])
102       rescue => e
103         Unicorn.log_error(logger, "#{sock_name(sock)} #{opt.inspect}", e)
104     end
106     def log_buffer_sizes(sock, pfx = '')
107       rcvbuf = sock.getsockopt(:SOL_SOCKET, :SO_RCVBUF).int
108       sndbuf = sock.getsockopt(:SOL_SOCKET, :SO_SNDBUF).int
109       logger.info "#{pfx}#{sock_name(sock)} rcvbuf=#{rcvbuf} sndbuf=#{sndbuf}"
110     end
112     # creates a new server, socket. address may be a HOST:PORT or
113     # an absolute path to a UNIX socket.  address can even be a Socket
114     # object in which case it is immediately returned
115     def bind_listen(address = '0.0.0.0:8080', opt = {})
116       return address unless String === address
118       sock = if address[0] == ?/
119         if File.exist?(address)
120           if File.socket?(address)
121             begin
122               UNIXSocket.new(address).close
123               # fall through, try to bind(2) and fail with EADDRINUSE
124               # (or succeed from a small race condition we can't sanely avoid).
125             rescue Errno::ECONNREFUSED
126               logger.info "unlinking existing socket=#{address}"
127               File.unlink(address)
128             end
129           else
130             raise ArgumentError,
131                   "socket=#{address} specified but it is not a socket!"
132           end
133         end
134         old_umask = File.umask(opt[:umask] || 0)
135         begin
136           Kgio::UNIXServer.new(address)
137         ensure
138           File.umask(old_umask)
139         end
140       elsif /\A\[([a-fA-F0-9:]+)\]:(\d+)\z/ =~ address
141         new_tcp_server($1, $2.to_i, opt.merge(:ipv6=>true))
142       elsif /\A(\d+\.\d+\.\d+\.\d+):(\d+)\z/ =~ address
143         new_tcp_server($1, $2.to_i, opt)
144       else
145         raise ArgumentError, "Don't know how to bind: #{address}"
146       end
147       set_server_sockopt(sock, opt)
148       sock
149     end
151     def new_tcp_server(addr, port, opt)
152       # n.b. we set FD_CLOEXEC in the workers
153       sock = Socket.new(opt[:ipv6] ? :AF_INET6 : :AF_INET, :SOCK_STREAM)
154       if opt.key?(:ipv6only)
155         Socket.const_defined?(:IPV6_V6ONLY) or
156           abort "Socket::IPV6_V6ONLY not defined, upgrade Ruby and/or your OS"
157         sock.setsockopt(:IPPROTO_IPV6, :IPV6_V6ONLY, opt[:ipv6only] ? 1 : 0)
158       end
159       sock.setsockopt(:SOL_SOCKET, :SO_REUSEADDR, 1)
160       if Socket.const_defined?(:SO_REUSEPORT) && opt[:reuseport]
161         sock.setsockopt(:SOL_SOCKET, :SO_REUSEPORT, 1)
162       end
163       sock.bind(Socket.pack_sockaddr_in(port, addr))
164       prevent_autoclose(sock)
165       Kgio::TCPServer.for_fd(sock.fileno)
166     end
168     # returns rfc2732-style (e.g. "[::1]:666") addresses for IPv6
169     def tcp_name(sock)
170       port, addr = Socket.unpack_sockaddr_in(sock.getsockname)
171       /:/ =~ addr ? "[#{addr}]:#{port}" : "#{addr}:#{port}"
172     end
173     module_function :tcp_name
175     # Returns the configuration name of a socket as a string.  sock may
176     # be a string value, in which case it is returned as-is
177     # Warning: TCP sockets may not always return the name given to it.
178     def sock_name(sock)
179       case sock
180       when String then sock
181       when UNIXServer
182         Socket.unpack_sockaddr_un(sock.getsockname)
183       when TCPServer
184         tcp_name(sock)
185       when Socket
186         begin
187           tcp_name(sock)
188         rescue ArgumentError
189           Socket.unpack_sockaddr_un(sock.getsockname)
190         end
191       else
192         raise ArgumentError, "Unhandled class #{sock.class}: #{sock.inspect}"
193       end
194     end
196     module_function :sock_name
198     # casts a given Socket to be a TCPServer or UNIXServer
199     def server_cast(sock)
200       begin
201         Socket.unpack_sockaddr_in(sock.getsockname)
202         Kgio::TCPServer.for_fd(sock.fileno)
203       rescue ArgumentError
204         Kgio::UNIXServer.for_fd(sock.fileno)
205       end
206     end
208   end # module SocketHelper
209 end # module Unicorn