From 975bbe55e1eb6561f3049c64aa8fbffd0c1714e1 Mon Sep 17 00:00:00 2001 From: Ahmed TAHRI Date: Fri, 8 Nov 2024 07:42:20 +0100 Subject: [PATCH] :bug: improve outgoing port reuse in same process execution flow (sync&async) --- CHANGES.rst | 5 +++++ src/urllib3/_version.py | 2 +- src/urllib3/contrib/resolver/_async/protocols.py | 8 ++++++++ src/urllib3/contrib/resolver/protocols.py | 7 +++++++ src/urllib3/contrib/ssa/__init__.py | 9 ++++----- test/conftest.py | 2 +- test/with_traefik/asynchronous/test_connection.py | 3 --- test/with_traefik/test_connection.py | 3 --- 8 files changed, 26 insertions(+), 13 deletions(-) diff --git a/CHANGES.rst b/CHANGES.rst index 1f87d8438e..8caa6f0c3e 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -1,3 +1,8 @@ +2.11.910 (2024-11-08) +===================== + +- Improved reliability of reusing a specific outgoing port. The feature is no longer experimental. + 2.11.909 (2024-11-07) ===================== diff --git a/src/urllib3/_version.py b/src/urllib3/_version.py index 4adb171ae3..8acc78d916 100644 --- a/src/urllib3/_version.py +++ b/src/urllib3/_version.py @@ -1,4 +1,4 @@ # This file is protected via CODEOWNERS from __future__ import annotations -__version__ = "2.11.909" +__version__ = "2.11.910" diff --git a/src/urllib3/contrib/resolver/_async/protocols.py b/src/urllib3/contrib/resolver/_async/protocols.py index ba3e500c1a..3e78f3a930 100644 --- a/src/urllib3/contrib/resolver/_async/protocols.py +++ b/src/urllib3/contrib/resolver/_async/protocols.py @@ -3,6 +3,7 @@ import asyncio import ipaddress import socket +import struct import typing from abc import ABCMeta, abstractmethod from datetime import datetime, timedelta, timezone @@ -133,6 +134,13 @@ async def create_connection( # type: ignore[override] ): # Defensive: we can't do anything better than this. pass + try: + sock.setsockopt( + socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0) + ) + except (OSError, AttributeError): + pass + # If provided, set socket level options before connecting. _set_socket_options(sock, socket_options) diff --git a/src/urllib3/contrib/resolver/protocols.py b/src/urllib3/contrib/resolver/protocols.py index 44d32b75df..240923e88c 100644 --- a/src/urllib3/contrib/resolver/protocols.py +++ b/src/urllib3/contrib/resolver/protocols.py @@ -245,6 +245,13 @@ def create_connection( ): # Defensive: we can't do anything better than this. pass + try: + sock.setsockopt( + socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0) + ) + except (OSError, AttributeError): + pass + sock.bind(source_address) # If provided, set socket level options before connecting. diff --git a/src/urllib3/contrib/ssa/__init__.py b/src/urllib3/contrib/ssa/__init__.py index 544ff3f698..89c596044b 100644 --- a/src/urllib3/contrib/ssa/__init__.py +++ b/src/urllib3/contrib/ssa/__init__.py @@ -6,7 +6,6 @@ import typing import warnings -SHUT_RD = 0 # taken from the "_socket" module StandardTimeoutError = socket.timeout try: @@ -126,7 +125,7 @@ def close(self) -> None: if hasattr(self._sock, "shutdown"): try: - self._sock.shutdown(SHUT_RD) + self._sock.shutdown(socket.SHUT_RD) shutdown_called = True except TypeError: uvloop_edge_case_bug = True @@ -143,7 +142,7 @@ def close(self) -> None: pass else: try: - direct_sock.shutdown(SHUT_RD) + direct_sock.shutdown(socket.SHUT_RD) shutdown_called = True except OSError: warnings.warn( @@ -174,7 +173,7 @@ def close(self) -> None: and not _CPYTHON_SELECTOR_CLOSE_BUG_EXIST ): try: - self._sock._sock.detach() + self._sock._sock.close() except (AttributeError, OSError): pass @@ -198,7 +197,7 @@ def close(self) -> None: async def wait_for_readiness(self) -> None: await self._established.wait() - def setsockopt(self, __level: int, __optname: int, __value: int) -> None: + def setsockopt(self, __level: int, __optname: int, __value: int | bytes) -> None: self._sock.setsockopt(__level, __optname, __value) def getsockopt(self, __level: int, __optname: int) -> int: diff --git a/test/conftest.py b/test/conftest.py index 570bd2550b..31d537f398 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -369,7 +369,7 @@ def requires_traefik() -> None: try: sock = socket.create_connection( - (os.environ.get("TRAEFIK_HTTPBIN_IPV4", "127.0.0.1"), 8888), timeout=0.3 + (os.environ.get("TRAEFIK_HTTPBIN_IPV4", "127.0.0.1"), 8888), timeout=1 ) except (ConnectionRefusedError, socket.gaierror, TimeoutError): _TRAEFIK_AVAILABLE = False diff --git a/test/with_traefik/asynchronous/test_connection.py b/test/with_traefik/asynchronous/test_connection.py index a4aa5ded87..aa8aee95a5 100644 --- a/test/with_traefik/asynchronous/test_connection.py +++ b/test/with_traefik/asynchronous/test_connection.py @@ -206,9 +206,6 @@ async def test_quic_extract_ssl_ctx_ca_root(self) -> None: await conn.close() - @pytest.mark.xfail( - reason="experimental support for reusable outgoing port", strict=False - ) async def test_fast_reuse_outgoing_port(self) -> None: for _ in range(4): conn = AsyncHTTPSConnection( diff --git a/test/with_traefik/test_connection.py b/test/with_traefik/test_connection.py index f13df70ff3..8864eefef6 100644 --- a/test/with_traefik/test_connection.py +++ b/test/with_traefik/test_connection.py @@ -200,9 +200,6 @@ def test_quic_extract_ssl_ctx_ca_root(self) -> None: conn.close() - @pytest.mark.xfail( - reason="experimental support for reusable outgoing port", strict=False - ) def test_fast_reuse_outgoing_port(self) -> None: for _ in range(4): conn = HTTPSConnection(