Repository: tornadoweb/tornado
Pull request #1514 (2015-09-09 11:42:05)

netutil.bind_sockets: add `reuse_port` option to set SO_REUSEPORT
This option is `False` by default, because not all platforms support it. Also there's a workaround for old versions of Python which don't export `SO_REUSEPORT` constant even if it supports by a kernel.

Related to #1513 


Patch:
diff --git a/tornado/netutil.py b/tornado/netutil.py
index 9aa292c4..4fc8d04d 100644
--- a/tornado/netutil.py
+++ b/tornado/netutil.py
@@ -111,7 +111,7 @@ _DEFAULT_BACKLOG = 128
 
 
 def bind_sockets(port, address=None, family=socket.AF_UNSPEC,
-                 backlog=_DEFAULT_BACKLOG, flags=None):
+                 backlog=_DEFAULT_BACKLOG, flags=None, reuse_port=False):
     """Creates listening sockets bound to the given port and address.
 
     Returns a list of socket objects (multiple sockets are returned if
@@ -130,7 +130,14 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC,
 
     ``flags`` is a bitmask of AI_* flags to `~socket.getaddrinfo`, like
     ``socket.AI_PASSIVE | socket.AI_NUMERICHOST``.
+
+    ``resuse_port`` option sets ``SO_REUSEPORT`` option for every socket
+    in the list. If your platform doesn't support this option ValueError will
+    be raised.
     """
+    if reuse_port and not hasattr(socket, "SO_REUSEPORT"):
+        raise ValueError("the platform doesn't support SO_REUSEPORT")
+
     sockets = []
     if address == "":
         address = None
@@ -165,6 +172,8 @@ def bind_sockets(port, address=None, family=socket.AF_UNSPEC,
         set_close_exec(sock.fileno())
         if os.name != 'nt':
             sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+        if reuse_port:
+            sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
         if af == socket.AF_INET6:
             # On linux, ipv6 sockets accept ipv4 too by default,
             # but this makes it impossible to bind to both


Test patch:
diff --git a/tornado/test/netutil_test.py b/tornado/test/netutil_test.py
index 7d9cad34..9ef5f7cf 100644
--- a/tornado/test/netutil_test.py
+++ b/tornado/test/netutil_test.py
@@ -9,7 +9,7 @@ import time
 
 from tornado.netutil import BlockingResolver, ThreadedResolver, is_valid_ip, bind_sockets
 from tornado.stack_context import ExceptionStackContext
-from tornado.testing import AsyncTestCase, gen_test
+from tornado.testing import AsyncTestCase, gen_test, bind_unused_port
 from tornado.test.util import unittest, skipIfNoNetwork
 
 try:
@@ -200,3 +200,14 @@ class TestPortAllocation(unittest.TestCase):
         finally:
             for sock in sockets:
                 sock.close()
+
+    @unittest.skipIf(not hasattr(socket, "SO_REUSEPORT"), "SO_REUSEPORT is not supported")
+    def test_reuse_port(self):
+        socket, port = bind_unused_port(reuse_port=True)
+        try:
+            sockets = bind_sockets(port, 'localhost', reuse_port=True)
+            self.assertTrue(all(s.getsockname()[1] == port for s in sockets))
+        finally:
+            socket.close()
+            for sock in sockets:
+                sock.close()
diff --git a/tornado/testing.py b/tornado/testing.py
index 304f22db..f5e9f153 100644
--- a/tornado/testing.py
+++ b/tornado/testing.py
@@ -85,12 +85,13 @@ def get_unused_port():
     return port
 
 
-def bind_unused_port():
+def bind_unused_port(reuse_port=False):
     """Binds a server socket to an available port on localhost.
 
     Returns a tuple (socket, port).
     """
-    [sock] = netutil.bind_sockets(None, 'localhost', family=socket.AF_INET)
+    [sock] = netutil.bind_sockets(None, 'localhost', family=socket.AF_INET,
+                                  reuse_port=reuse_port)
     port = sock.getsockname()[1]
     return sock, port