From fffaa2f7f729570d3bc28b92281d8875e39c9973 Mon Sep 17 00:00:00 2001 From: Anton Tiurin Date: Wed, 9 Sep 2015 14:36:21 +0300 Subject: [PATCH] netutil.bind_sockets: add `reuse_port` option to set SO_REUSEPORT This option is False by default, because not all platforms support it. Signed-off-by: Anton Tiurin --- tornado/netutil.py | 11 ++++++++++- tornado/test/netutil_test.py | 13 ++++++++++++- tornado/testing.py | 5 +++-- 3 files changed, 25 insertions(+), 4 deletions(-) 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 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