From 79f05ce0278311ebfbadb85843ee2138bb0549f6 Mon Sep 17 00:00:00 2001 From: Armen Michaeli Date: Wed, 8 Sep 2021 16:38:03 +0200 Subject: [PATCH] Allow reset of default backlog for server sockets This commit is about three following changes: 1. Control of default backlog throughout the lifetime of a Tornado application, through the now exported (no `_` prefix) module variable. Previously, by way of how Python initializes function argument defaults, the default was used once during module initialization (when procedures are created) which "froze" the default value in the sense there was no way to affect the default during application lifetime -- typically as configuration is loaded, for example. To allow application to configure the default backlog when it has the preferred value available, evaluation of the default backlog immediately before calling `listen`, is thus necessary. 2. Deferring to Python deciding on the backlog, through using `None` as valid value. For Python 3.6, for example, that would imply the minimum between 128 and SOMAXCONN: https://github.com/python/cpython/blob/3.6/Modules/socketmodule.c#L3026 3. Because the number of connections Tornado attempted to `accept` with each iteration of it's I/O loop, was tied to the default backlog value, because this commit allows for `None` or zero (valid for `listen`) values for the latter, the heuristics needed to be amended because neither zero nor `None` would be an acceptable number of connections to accept. This problem is solved by appropriately adding a separate variable that controls the number, with fallback through a non-zero default backlog to 128. --- tornado/netutil.py | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/tornado/netutil.py b/tornado/netutil.py index 15eea9704f..d7d509ea76 100644 --- a/tornado/netutil.py +++ b/tornado/netutil.py @@ -48,15 +48,23 @@ # For undiagnosed reasons, 'latin1' codec may also need to be preloaded. u"foo".encode("latin1") -# Default backlog used when calling sock.listen() -_DEFAULT_BACKLOG = 128 +# Default backlog used when calling sock.listen(); if `None`, no value is +# passed to `socket.listen` implying normally that Python will decide on +# the backlog size for the socket. For Python < 3.6, an error will be +# raised by `socket.listen` if no argument is provided. +DEFAULT_BACKLOG = None + +# Number of connections to try and accept in one sweep of an I/O loop; +# if falsy (e.g. `None` or zero) the number will be decided on +# automatically. +ACCEPT_CALLS_PER_EVENT_LOOP = None def bind_sockets( port: int, address: Optional[str] = None, family: socket.AddressFamily = socket.AF_UNSPEC, - backlog: int = _DEFAULT_BACKLOG, + backlog: Optional[int] = None, flags: Optional[int] = None, reuse_port: bool = False, ) -> List[socket.socket]: @@ -74,7 +82,8 @@ def bind_sockets( both will be used if available. The ``backlog`` argument has the same meaning as for - `socket.listen() `. + `socket.listen() ` if it carries an integer + value; if `None`, the backlog size is chosen automatically. ``flags`` is a bitmask of AI_* flags to `~socket.getaddrinfo`, like ``socket.AI_PASSIVE | socket.AI_NUMERICHOST``. @@ -181,7 +190,7 @@ def bind_sockets( else: raise bound_port = sock.getsockname()[1] - sock.listen(backlog) + listen(sock, backlog) sockets.append(sock) return sockets @@ -189,7 +198,7 @@ def bind_sockets( if hasattr(socket, "AF_UNIX"): def bind_unix_socket( - file: str, mode: int = 0o600, backlog: int = _DEFAULT_BACKLOG + file: str, mode: int = 0o600, backlog: Optional[int] = None ) -> socket.socket: """Creates a listening unix socket. @@ -219,7 +228,7 @@ def bind_unix_socket( raise ValueError("File %s exists and is not a socket", file) sock.bind(file) os.chmod(file, mode) - sock.listen(backlog) + listen(sock, backlog) return sock @@ -258,7 +267,7 @@ def accept_handler(fd: socket.socket, events: int) -> None: # Instead, we use the (default) listen backlog as a rough # heuristic for the number of connections we can reasonably # accept at once. - for i in range(_DEFAULT_BACKLOG): + for i in range(ACCEPT_CALLS_PER_EVENT_LOOP or DEFAULT_BACKLOG or 128): if removed[0]: # The socket was probably closed return @@ -621,3 +630,12 @@ def ssl_wrap_socket( return context.wrap_socket(socket, server_hostname=server_hostname, **kwargs) else: return context.wrap_socket(socket, **kwargs) + + +def listen(socket: socket.socket, backlog: int = None): + """A helper procedure to better delegate `socket.listen` calls where + backlog may or may not be specified. + """ + if backlog is None: + backlog = DEFAULT_BACKLOG + return socket.listen(backlog) if backlog is not None else socket.listen()