From 1622b0893dde7441177b66b06819bebb080d2afa Mon Sep 17 00:00:00 2001 From: Ben Darnell Date: Fri, 3 Jun 2022 11:40:31 -0400 Subject: [PATCH] tcpserver,web: Pass more arguments through listen. In particular, this makes it easier to use reuse_port. --- tornado/tcpserver.py | 77 +++++++++++++++++++++++++--------- tornado/test/tcpserver_test.py | 29 ++++++++++++- tornado/web.py | 27 +++++++++++- 3 files changed, 109 insertions(+), 24 deletions(-) diff --git a/tornado/tcpserver.py b/tornado/tcpserver.py index 476ffc936..9065b62e8 100644 --- a/tornado/tcpserver.py +++ b/tornado/tcpserver.py @@ -24,7 +24,12 @@ from tornado import gen from tornado.log import app_log from tornado.ioloop import IOLoop from tornado.iostream import IOStream, SSLIOStream -from tornado.netutil import bind_sockets, add_accept_handler, ssl_wrap_socket +from tornado.netutil import ( + bind_sockets, + add_accept_handler, + ssl_wrap_socket, + _DEFAULT_BACKLOG, +) from tornado import process from tornado.util import errno_from_exception @@ -140,15 +145,38 @@ class TCPServer(object): 'keyfile "%s" does not exist' % self.ssl_options["keyfile"] ) - def listen(self, port: int, address: str = "") -> None: + def listen( + self, + port: int, + address: Optional[str] = None, + family: socket.AddressFamily = socket.AF_UNSPEC, + backlog: int = _DEFAULT_BACKLOG, + flags: Optional[int] = None, + reuse_port: bool = False, + ) -> None: """Starts accepting connections on the given port. This method may be called more than once to listen on multiple ports. `listen` takes effect immediately; it is not necessary to call - `TCPServer.start` afterwards. It is, however, necessary to start - the `.IOLoop`. + `TCPServer.start` afterwards. It is, however, necessary to start the + event loop if it is not already running. + + All arguments have the same meaning as in + `tornado.netutil.bind_sockets`. + + .. versionchanged:: 6.2 + + Added ``family``, ``backlog``, ``flags``, and ``reuse_port`` + arguments to match `tornado.netutil.bind_sockets`. """ - sockets = bind_sockets(port, address=address) + sockets = bind_sockets( + port, + address=address, + family=family, + backlog=backlog, + flags=flags, + reuse_port=reuse_port, + ) self.add_sockets(sockets) def add_sockets(self, sockets: Iterable[socket.socket]) -> None: @@ -175,34 +203,43 @@ class TCPServer(object): port: int, address: Optional[str] = None, family: socket.AddressFamily = socket.AF_UNSPEC, - backlog: int = 128, + backlog: int = _DEFAULT_BACKLOG, + flags: Optional[int] = None, reuse_port: bool = False, ) -> None: """Binds this server to the given port on the given address. - To start the server, call `start`. If you want to run this server - in a single process, you can call `listen` as a shortcut to the - sequence of `bind` and `start` calls. + To start the server, call `start`. If you want to run this server in a + single process, you can call `listen` as a shortcut to the sequence of + `bind` and `start` calls. Address may be either an IP address or hostname. If it's a hostname, - the server will listen on all IP addresses associated with the - name. Address may be an empty string or None to listen on all - available interfaces. Family may be set to either `socket.AF_INET` - or `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise - both will be used if available. + the server will listen on all IP addresses associated with the name. + Address may be an empty string or None to listen on all available + interfaces. Family may be set to either `socket.AF_INET` or + `socket.AF_INET6` to restrict to IPv4 or IPv6 addresses, otherwise both + will be used if available. - The ``backlog`` argument has the same meaning as for - `socket.listen `. The ``reuse_port`` argument - has the same meaning as for `.bind_sockets`. + The ``backlog`` argument has the same meaning as for `socket.listen + `. The ``reuse_port`` argument has the same + meaning as for `.bind_sockets`. - This method may be called multiple times prior to `start` to listen - on multiple ports or interfaces. + This method may be called multiple times prior to `start` to listen on + multiple ports or interfaces. .. versionchanged:: 4.4 Added the ``reuse_port`` argument. + + .. versionchanged:: 6.2 + Added the ``flags`` argument to match `.bind_sockets`. """ sockets = bind_sockets( - port, address=address, family=family, backlog=backlog, reuse_port=reuse_port + port, + address=address, + family=family, + backlog=backlog, + flags=flags, + reuse_port=reuse_port, ) if self._started: self.add_sockets(sockets) diff --git a/tornado/test/tcpserver_test.py b/tornado/test/tcpserver_test.py index fc9e98a23..8b59db577 100644 --- a/tornado/test/tcpserver_test.py +++ b/tornado/test/tcpserver_test.py @@ -125,14 +125,15 @@ class TestMultiprocess(unittest.TestCase): try: result = subprocess.run( sys.executable, - capture_output=True, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, input=code, encoding="utf8", check=True, ) except subprocess.CalledProcessError as e: raise RuntimeError( - f"Process returned {e.returncode} stdout={e.stdout}" + f"Process returned {e.returncode} output={e.stdout}" ) from e return result.stdout @@ -189,3 +190,27 @@ class TestMultiprocess(unittest.TestCase): ) out = self.run_subproc(code) self.assertEqual("".join(sorted(out)), "012") + + def test_reuse_port(self): + code = textwrap.dedent( + """ + import socket + from tornado.ioloop import IOLoop + from tornado.netutil import bind_sockets + from tornado.process import task_id, fork_processes + from tornado.tcpserver import TCPServer + + # Pick an unused port which we will be able to bind to multiple times. + (sock,) = bind_sockets(0, address='127.0.0.1', + family=socket.AF_INET, reuse_port=True) + port = sock.getsockname()[1] + + fork_processes(3) + server = TCPServer() + server.listen(port, address='127.0.0.1', reuse_port=True) + IOLoop.current().run_sync(lambda: None) + print(task_id(), end='') + """ + ) + out = self.run_subproc(code) + self.assertEqual("".join(sorted(out)), "012") diff --git a/tornado/web.py b/tornado/web.py index 2dab485a4..f3eb6e1e7 100644 --- a/tornado/web.py +++ b/tornado/web.py @@ -72,6 +72,7 @@ import mimetypes import numbers import os.path import re +import socket import sys import threading import time @@ -90,6 +91,7 @@ from tornado import iostream import tornado.locale from tornado import locale from tornado.log import access_log, app_log, gen_log +import tornado.netutil from tornado import template from tornado.escape import utf8, _unicode from tornado.routing import ( @@ -2093,7 +2095,17 @@ class Application(ReversibleRouter): autoreload.start() - def listen(self, port: int, address: str = "", **kwargs: Any) -> HTTPServer: + def listen( + self, + port: int, + address: Optional[str] = None, + *, + family: socket.AddressFamily = socket.AF_UNSPEC, + backlog: int = tornado.netutil._DEFAULT_BACKLOG, + flags: Optional[int] = None, + reuse_port: bool = False, + **kwargs: Any + ) -> HTTPServer: """Starts an HTTP server for this application on the given port. This is a convenience alias for creating an `.HTTPServer` @@ -2111,9 +2123,20 @@ class Application(ReversibleRouter): .. versionchanged:: 4.3 Now returns the `.HTTPServer` object. + + .. versionchanged:: 6.2 + Added support for new keyword arguments in `.TCPServer.listen`, + including ``reuse_port``. """ server = HTTPServer(self, **kwargs) - server.listen(port, address) + server.listen( + port, + address=address, + family=family, + backlog=backlog, + flags=flags, + reuse_port=reuse_port, + ) return server def add_handlers(self, host_pattern: str, host_handlers: _RuleList) -> None: -- 2.47.2