Skip to content

Commit

Permalink
Make it possible to set SO_REUSEPORT to the server's socket
Browse files Browse the repository at this point in the history
  • Loading branch information
VladimirKuzmin committed May 2, 2022
1 parent 14e8526 commit ffa0158
Show file tree
Hide file tree
Showing 5 changed files with 50 additions and 4 deletions.
18 changes: 17 additions & 1 deletion cheroot/server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1588,6 +1588,9 @@ class HTTPServer:
``PEERCREDS``-provided IDs.
"""

reuse_port = False
"""If True, set SO_REUSEPORT on the socket."""

keep_alive_conn_limit = 10
"""The maximum number of waiting keep-alive connections that will be kept open.
Expand All @@ -1597,6 +1600,7 @@ def __init__(
self, bind_addr, gateway,
minthreads=10, maxthreads=-1, server_name=None,
peercreds_enabled=False, peercreds_resolve_enabled=False,
reuse_port=False,
):
"""Initialize HTTPServer instance.
Expand All @@ -1607,6 +1611,8 @@ def __init__(
maxthreads (int): maximum number of threads for HTTP thread pool
server_name (str): web server name to be advertised via Server
HTTP header
reuse_port (bool): if True SO_REUSEPORT option would be set to
socket
"""
self.bind_addr = bind_addr
self.gateway = gateway
Expand All @@ -1622,6 +1628,7 @@ def __init__(
self.peercreds_resolve_enabled = (
peercreds_resolve_enabled and peercreds_enabled
)
self.reuse_port = reuse_port
self.clear_stats()

def clear_stats(self):
Expand Down Expand Up @@ -1896,6 +1903,7 @@ def bind(self, family, type, proto=0):
self.bind_addr,
family, type, proto,
self.nodelay, self.ssl_adapter,
self.reuse_port,
)
sock = self.socket = self.bind_socket(sock, self.bind_addr)
self.bind_addr = self.resolve_real_bind_addr(sock)
Expand Down Expand Up @@ -1947,6 +1955,7 @@ def bind_unix_socket(self, bind_addr): # noqa: C901 # FIXME
bind_addr=bind_addr,
family=socket.AF_UNIX, type=socket.SOCK_STREAM, proto=0,
nodelay=self.nodelay, ssl_adapter=self.ssl_adapter,
reuse_port=self.reuse_port,
)

try:
Expand Down Expand Up @@ -1987,14 +1996,21 @@ def bind_unix_socket(self, bind_addr): # noqa: C901 # FIXME
return sock

@staticmethod
def prepare_socket(bind_addr, family, type, proto, nodelay, ssl_adapter):
def prepare_socket(
bind_addr, family, type, proto, nodelay, ssl_adapter, reuse_port,
):
"""Create and prepare the socket object."""
sock = socket.socket(family, type, proto)
connections.prevent_socket_inheritance(sock)

host, port = bind_addr[:2]
IS_EPHEMERAL_PORT = port == 0

if reuse_port:
if not hasattr(socket, 'SO_REUSEPORT'):
raise ValueError('SO_REUSEPORT not supported on this platform')
sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)

if not (IS_WINDOWS or IS_EPHEMERAL_PORT):
"""Enable SO_REUSEADDR for the current socket.
Expand Down
5 changes: 3 additions & 2 deletions cheroot/server.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,10 @@ class HTTPServer:
ssl_adapter: Any
peercreds_enabled: bool
peercreds_resolve_enabled: bool
reuse_port: bool
keep_alive_conn_limit: int
requests: Any
def __init__(self, bind_addr, gateway, minthreads: int = ..., maxthreads: int = ..., server_name: Any | None = ..., peercreds_enabled: bool = ..., peercreds_resolve_enabled: bool = ...) -> None: ...
def __init__(self, bind_addr, gateway, minthreads: int = ..., maxthreads: int = ..., server_name: Any | None = ..., peercreds_enabled: bool = ..., peercreds_resolve_enabled: bool = ..., reuse_port: bool = ...) -> None: ...
stats: Any
def clear_stats(self): ...
def runtime(self): ...
Expand All @@ -152,7 +153,7 @@ class HTTPServer:
def bind(self, family, type, proto: int = ...): ...
def bind_unix_socket(self, bind_addr): ...
@staticmethod
def prepare_socket(bind_addr, family, type, proto, nodelay, ssl_adapter): ...
def prepare_socket(bind_addr, family, type, proto, nodelay, ssl_adapter, reuse_port): ...
@staticmethod
def bind_socket(socket_, bind_addr): ...
@staticmethod
Expand Down
27 changes: 27 additions & 0 deletions cheroot/test/test_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,33 @@ def native_process_conn(conn):
assert any(fn >= resource_limit for fn in native_process_conn.filenos)


@pytest.mark.skipif(
not hasattr(socket, 'SO_REUSEPORT'),
reason='socket.SO_REUSEPORT is not supported on this platform',
)
@pytest.mark.parametrize(
'ip_addr',
(
ANY_INTERFACE_IPV4,
ANY_INTERFACE_IPV6,
),
)
def test_reuse_port(http_server, ip_addr):
"""Check that port initialized exetrnally can be reused."""
family = socket.getaddrinfo(ip_addr, EPHEMERAL_PORT)[0][0]
s = socket.socket(family)
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1)
s.bind((ip_addr, EPHEMERAL_PORT))
server = HTTPServer(
bind_addr=s.getsockname()[:2], gateway=Gateway, reuse_port=True,
)
try:
server.prepare()
finally:
server.stop()
s.close()


if not IS_WINDOWS:
test_high_number_of_file_descriptors = pytest.mark.forked(
test_high_number_of_file_descriptors,
Expand Down
2 changes: 2 additions & 0 deletions cheroot/wsgi.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ def __init__(
max=-1, request_queue_size=5, timeout=10, shutdown_timeout=5,
accepted_queue_size=-1, accepted_queue_timeout=10,
peercreds_enabled=False, peercreds_resolve_enabled=False,
reuse_port=False,
):
"""Initialize WSGI Server instance.
Expand All @@ -75,6 +76,7 @@ def __init__(
server_name=server_name,
peercreds_enabled=peercreds_enabled,
peercreds_resolve_enabled=peercreds_resolve_enabled,
reuse_port=reuse_port,
)
self.wsgi_app = wsgi_app
self.request_queue_size = request_queue_size
Expand Down
2 changes: 1 addition & 1 deletion cheroot/wsgi.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ class Server(server.HTTPServer):
timeout: Any
shutdown_timeout: Any
requests: Any
def __init__(self, bind_addr, wsgi_app, numthreads: int = ..., server_name: Any | None = ..., max: int = ..., request_queue_size: int = ..., timeout: int = ..., shutdown_timeout: int = ..., accepted_queue_size: int = ..., accepted_queue_timeout: int = ..., peercreds_enabled: bool = ..., peercreds_resolve_enabled: bool = ...) -> None: ...
def __init__(self, bind_addr, wsgi_app, numthreads: int = ..., server_name: Any | None = ..., max: int = ..., request_queue_size: int = ..., timeout: int = ..., shutdown_timeout: int = ..., accepted_queue_size: int = ..., accepted_queue_timeout: int = ..., peercreds_enabled: bool = ..., peercreds_resolve_enabled: bool = ..., reuse_port: bool = ...) -> None: ...
@property
def numthreads(self): ...
@numthreads.setter
Expand Down

0 comments on commit ffa0158

Please sign in to comment.