From b29a84657794bd90a8d2b2cdbfb56e161944a004 Mon Sep 17 00:00:00 2001 From: Julien Portalier Date: Fri, 17 Feb 2017 11:09:27 +0100 Subject: [PATCH] Fix: configurable SO_REUSEPORT socket setting (disabled by default) 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 --- spec/std/http/server/server_spec.cr | 11 +++++++++++ spec/std/socket_spec.cr | 12 ++++++++++-- src/http/server.cr | 25 +++++++++++++++++++++---- src/socket/tcp_server.cr | 24 +++++++++++++----------- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/spec/std/http/server/server_spec.cr b/spec/std/http/server/server_spec.cr index 236ce34ad21c..fef20485d253 100644 --- a/spec/std/http/server/server_spec.cr +++ b/spec/std/http/server/server_spec.cr @@ -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 diff --git a/spec/std/socket_spec.cr b/spec/std/socket_spec.cr index 6e451bb3ac04..3bf7bd11a580 100644 --- a/spec/std/socket_spec.cr +++ b/spec/std/socket_spec.cr @@ -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 diff --git a/src/http/server.cr b/src/http/server.cr index d5ea3553aa12..b117d8420c18 100644 --- a/src/http/server.cr +++ b/src/http/server.cr @@ -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 @@ -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 diff --git a/src/socket/tcp_server.cr b/src/socket/tcp_server.cr index f979268454d7..4a09b11b6a05 100644 --- a/src/socket/tcp_server.cr +++ b/src/socket/tcp_server.cr @@ -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 @@ -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 @@ -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 @@ -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