Skip to content

Commit 17983d3

Browse files
[PR #10474/7379a866 backport][3.12] Expose setsockopt in TCPConnector API (#10486)
**This is a backport of PR #10474 as merged into master (7379a86).** Co-authored-by: Tim Menninger <[email protected]>
1 parent 0704705 commit 17983d3

File tree

6 files changed

+61
-1
lines changed

6 files changed

+61
-1
lines changed

CHANGES/10474.feature.rst

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
Added ``tcp_sockopts`` to ``TCPConnector`` to allow specifying custom socket options
2+
-- by :user:`TimMenninger`.

CONTRIBUTORS.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,7 @@ Thanos Lefteris
332332
Thijs Vermeir
333333
Thomas Forbes
334334
Thomas Grainger
335+
Tim Menninger
335336
Tolga Tezel
336337
Tomasz Trebski
337338
Toshiaki Tanaka

aiohttp/connector.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
DefaultDict,
2020
Deque,
2121
Dict,
22+
Iterable,
2223
Iterator,
2324
List,
2425
Literal,
@@ -60,6 +61,11 @@
6061
)
6162
from .resolver import DefaultResolver
6263

64+
if sys.version_info >= (3, 12):
65+
from collections.abc import Buffer
66+
else:
67+
Buffer = Union[bytes, bytearray, "memoryview[int]", "memoryview[bytes]"]
68+
6369
if TYPE_CHECKING:
6470
import ssl
6571

@@ -828,6 +834,8 @@ class TCPConnector(BaseConnector):
828834
the happy eyeballs algorithm, set to None.
829835
interleave - “First Address Family Count” as defined in RFC 8305
830836
loop - Optional event loop.
837+
tcp_sockopts - List of tuples of sockopts applied to underlying
838+
socket
831839
"""
832840

833841
allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"tcp"})
@@ -853,6 +861,7 @@ def __init__(
853861
timeout_ceil_threshold: float = 5,
854862
happy_eyeballs_delay: Optional[float] = 0.25,
855863
interleave: Optional[int] = None,
864+
tcp_sockopts: Iterable[Tuple[int, int, Union[int, Buffer]]] = [],
856865
):
857866
super().__init__(
858867
keepalive_timeout=keepalive_timeout,
@@ -879,6 +888,7 @@ def __init__(
879888
self._happy_eyeballs_delay = happy_eyeballs_delay
880889
self._interleave = interleave
881890
self._resolve_host_tasks: Set["asyncio.Task[List[ResolveResult]]"] = set()
891+
self._tcp_sockopts = tcp_sockopts
882892

883893
def close(self) -> Awaitable[None]:
884894
"""Close all ongoing DNS calls."""
@@ -1120,6 +1130,8 @@ async def _wrap_create_connection(
11201130
interleave=self._interleave,
11211131
loop=self._loop,
11221132
)
1133+
for sockopt in self._tcp_sockopts:
1134+
sock.setsockopt(*sockopt)
11231135
connection = await self._loop.create_connection(
11241136
*args, **kwargs, sock=sock
11251137
)

docs/client_advanced.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,21 @@ If your HTTP server uses UNIX domain sockets you can use
461461
session = aiohttp.ClientSession(connector=conn)
462462

463463

464+
Setting socket options
465+
^^^^^^^^^^^^^^^^^^^^^^
466+
467+
Socket options passed to the :class:`~aiohttp.TCPConnector` will be passed
468+
to the underlying socket when creating a connection. For example, we may
469+
want to change the conditions under which we consider a connection dead.
470+
The following would change that to 9*7200 = 18 hours::
471+
472+
import socket
473+
474+
conn = aiohttp.TCPConnector(tcp_sockopts=[(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True),
475+
(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7200),
476+
(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 9) ])
477+
478+
464479
Named pipes in Windows
465480
^^^^^^^^^^^^^^^^^^^^^^
466481

docs/client_reference.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1144,7 +1144,8 @@ is controlled by *force_close* constructor's parameter).
11441144
resolver=None, keepalive_timeout=sentinel, \
11451145
force_close=False, limit=100, limit_per_host=0, \
11461146
enable_cleanup_closed=False, timeout_ceil_threshold=5, \
1147-
happy_eyeballs_delay=0.25, interleave=None, loop=None)
1147+
happy_eyeballs_delay=0.25, interleave=None, loop=None, \
1148+
tcp_sockopts=[])
11481149

11491150
Connector for working with *HTTP* and *HTTPS* via *TCP* sockets.
11501151

@@ -1265,6 +1266,12 @@ is controlled by *force_close* constructor's parameter).
12651266

12661267
.. versionadded:: 3.10
12671268

1269+
:param list tcp_sockopts: options applied to the socket when a connection is
1270+
created. This should be a list of 3-tuples, each a ``(level, optname, value)``.
1271+
Each tuple is deconstructed and passed verbatim to ``<socket>.setsockopt``.
1272+
1273+
.. versionadded:: 3.12
1274+
12681275
.. attribute:: family
12691276

12701277
*TCP* socket family e.g. :data:`socket.AF_INET` or

tests/test_connector.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3581,6 +3581,29 @@ def test_connect() -> Literal[True]:
35813581
assert raw_response_list == [True, True]
35823582

35833583

3584+
async def test_tcp_connector_setsockopts(
3585+
loop: asyncio.AbstractEventLoop, start_connection: mock.AsyncMock
3586+
) -> None:
3587+
"""Check that sockopts get passed to socket"""
3588+
conn = aiohttp.TCPConnector(
3589+
tcp_sockopts=[(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 2)]
3590+
)
3591+
3592+
with mock.patch.object(
3593+
conn._loop, "create_connection", autospec=True, spec_set=True
3594+
) as create_connection:
3595+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
3596+
start_connection.return_value = s
3597+
create_connection.return_value = mock.Mock(), mock.Mock()
3598+
3599+
req = ClientRequest("GET", URL("https://127.0.0.1:443"), loop=loop)
3600+
3601+
with closing(await conn.connect(req, [], ClientTimeout())):
3602+
assert s.getsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT) == 2
3603+
3604+
await conn.close()
3605+
3606+
35843607
def test_default_ssl_context_creation_without_ssl() -> None:
35853608
"""Verify _make_ssl_context does not raise when ssl is not available."""
35863609
with mock.patch.object(connector_module, "ssl", None):

0 commit comments

Comments
 (0)