Skip to content

Commit

Permalink
Fix: configurable SO_REUSEPORT socket setting (disabled by default)
Browse files Browse the repository at this point in the history
This led to confusing behavior when starting different TCP services
that default to the same default ports. They didn't fail to start,
but were failing to receive some requests.

The setting is now configurable and disabled by default. You may
enable it by setting the `reuse_port` named argument to true in the
following methods:

    TCPServer.new
    TCPServer.open
    HTTP::Server#bind
    HTTP::Server#listen
  • Loading branch information
ysbaddaden authored and Ary Borenszweig committed Feb 20, 2017
1 parent 367e9de commit b29a846
Show file tree
Hide file tree
Showing 4 changed files with 55 additions and 17 deletions.
11 changes: 11 additions & 0 deletions spec/std/http/server/server_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,17 @@ module HTTP

server.listen
end

it "reuses the TCP port (SO_REUSEPORT)" do
s1 = Server.new(0) { |ctx| }
s1.bind(reuse_port: true)

s2 = Server.new(s1.port) { |ctx| }
s2.bind(reuse_port: true)

s1.close
s2.close
end
end

describe HTTP::Server::RequestProcessor do
Expand Down
12 changes: 10 additions & 2 deletions spec/std/socket_spec.cr
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,17 @@ describe TCPServer do
end
end

it "allows to share the same port (SO_REUSEPORT)" do
it "doesn't reuse the TCP port by default (SO_REUSEPORT)" do
TCPServer.open("::", 0) do |server|
TCPServer.open("::", server.local_address.port) { }
expect_raises(Errno) do
TCPServer.open("::", server.local_address.port) { }
end
end
end

it "reuses the TCP port (SO_REUSEPORT)" do
TCPServer.open("::", 0, reuse_port: true) do |server|
TCPServer.open("::", server.local_address.port, reuse_port: true) { }
end
end
end
Expand Down
25 changes: 21 additions & 4 deletions src/http/server.cr
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ class HTTP::Server
@processor = RequestProcessor.new(handler)
end

# Returns the TCP port the server is connected to.
#
# For example you may let the system choose a port, then report it:
# ```
# server = HTTP::Server.new(0) { }
# server.bind
# server.port # => 12345
# ```
def port
if server = @server
server.local_address.port.to_i
Expand All @@ -138,17 +146,26 @@ class HTTP::Server
end
end

def bind
@server ||= TCPServer.new(@host, @port)
# Creates the underlying `TCPServer` if the doesn't already exist.
#
# You may set *reuse_port* to true to enable the `SO_REUSEPORT` socket option,
# which allows multiple processes to bind to the same port.
def bind(reuse_port = false)
@server ||= TCPServer.new(@host, @port, reuse_port: reuse_port)
end

def listen
server = bind
# Starts the server. Blocks until the server is closed.
#
# See `#bind` for details on the *reuse_port* argument.
def listen(reuse_port = false)
server = bind(reuse_port)
until @wants_close
spawn handle_client(server.accept?)
end
end

# Gracefully terminates the server. It will process currently accepted
# requests, but it won't accept new connections.
def close
@wants_close = true
@processor.close
Expand Down
24 changes: 13 additions & 11 deletions src/socket/tcp_server.cr
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ require "./tcp_socket"
# end
# end
# ```
#
# Options:
# - *backlog* to specify how many pending connections are allowed;
# - *reuse_port* to enable multiple processes to bind to the same port (`SO_REUSEPORT`).
class TCPServer < TCPSocket
include Socket::Server

Expand All @@ -22,15 +26,13 @@ class TCPServer < TCPSocket
super(family)
end

def initialize(host : String, port : Int, backlog = SOMAXCONN, dns_timeout = nil)
# Binds a socket to the *host* and *port* combination.
def initialize(host : String, port : Int, backlog = SOMAXCONN, dns_timeout = nil, reuse_port = false)
Addrinfo.tcp(host, port, timeout: dns_timeout) do |addrinfo|
super(addrinfo.family, addrinfo.type, addrinfo.protocol)

self.reuse_address = true
begin
self.reuse_port = true
rescue Errno
end
self.reuse_port = true if reuse_port

if errno = bind(addrinfo) { |errno| errno }
close
Expand All @@ -45,16 +47,16 @@ class TCPServer < TCPSocket
end

# Creates a new TCP server, listening on all local interfaces (`::`).
def self.new(port : Int, backlog = SOMAXCONN)
new("::", port, backlog)
def self.new(port : Int, backlog = SOMAXCONN, reuse_port = false)
new("::", port, backlog, reuse_port: reuse_port)
end

# Creates a new TCP server and yields it to the block. Eventually closes the
# server socket when the block returns.
#
# Returns the value of the block.
def self.open(host, port, backlog = SOMAXCONN)
server = new(host, port, backlog)
def self.open(host, port, backlog = SOMAXCONN, reuse_port = false)
server = new(host, port, backlog, reuse_port: reuse_port)
begin
yield server
ensure
Expand All @@ -66,8 +68,8 @@ class TCPServer < TCPSocket
# block. Eventually closes the server socket when the block returns.
#
# Returns the value of the block.
def self.open(port : Int, backlog = SOMAXCONN)
server = new(port, backlog)
def self.open(port : Int, backlog = SOMAXCONN, reuse_port = false)
server = new(port, backlog, reuse_port: reuse_port)
begin
yield server
ensure
Expand Down

0 comments on commit b29a846

Please sign in to comment.