Skip to content

Commit 7379a86

Browse files
authored
Expose setsockopt in TCPConnector API (#10474)
1 parent e565ed5 commit 7379a86

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
@@ -341,6 +341,7 @@ Thanos Lefteris
341341
Thijs Vermeir
342342
Thomas Forbes
343343
Thomas Grainger
344+
Tim Menninger
344345
Tolga Tezel
345346
Tomasz Trebski
346347
Toshiaki Tanaka

aiohttp/connector.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
DefaultDict,
2121
Deque,
2222
Dict,
23+
Iterable,
2324
Iterator,
2425
List,
2526
Literal,
@@ -61,6 +62,11 @@
6162
)
6263
from .resolver import DefaultResolver
6364

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

@@ -820,6 +826,8 @@ class TCPConnector(BaseConnector):
820826
the happy eyeballs algorithm, set to None.
821827
interleave - “First Address Family Count” as defined in RFC 8305
822828
loop - Optional event loop.
829+
tcp_sockopts - List of tuples of sockopts applied to underlying
830+
socket
823831
"""
824832

825833
allowed_protocol_schema_set = HIGH_LEVEL_SCHEMA_SET | frozenset({"tcp"})
@@ -841,6 +849,7 @@ def __init__(
841849
timeout_ceil_threshold: float = 5,
842850
happy_eyeballs_delay: Optional[float] = 0.25,
843851
interleave: Optional[int] = None,
852+
tcp_sockopts: Iterable[Tuple[int, int, Union[int, Buffer]]] = [],
844853
):
845854
super().__init__(
846855
keepalive_timeout=keepalive_timeout,
@@ -871,6 +880,7 @@ def __init__(
871880
self._happy_eyeballs_delay = happy_eyeballs_delay
872881
self._interleave = interleave
873882
self._resolve_host_tasks: Set["asyncio.Task[List[ResolveResult]]"] = set()
883+
self._tcp_sockopts = tcp_sockopts
874884

875885
def _close_immediately(self) -> List[Awaitable[object]]:
876886
for fut in chain.from_iterable(self._throttle_dns_futures.values()):
@@ -1113,6 +1123,8 @@ async def _wrap_create_connection(
11131123
interleave=self._interleave,
11141124
loop=self._loop,
11151125
)
1126+
for sockopt in self._tcp_sockopts:
1127+
sock.setsockopt(*sockopt)
11161128
connection = await self._loop.create_connection(
11171129
*args, **kwargs, sock=sock
11181130
)

docs/client_advanced.rst

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

470470

471+
Setting socket options
472+
^^^^^^^^^^^^^^^^^^^^^^
473+
474+
Socket options passed to the :class:`~aiohttp.TCPConnector` will be passed
475+
to the underlying socket when creating a connection. For example, we may
476+
want to change the conditions under which we consider a connection dead.
477+
The following would change that to 9*7200 = 18 hours::
478+
479+
import socket
480+
481+
conn = aiohttp.TCPConnector(tcp_sockopts=[(socket.SOL_SOCKET, socket.SO_KEEPALIVE, True),
482+
(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 7200),
483+
(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 9) ])
484+
485+
471486
Named pipes in Windows
472487
^^^^^^^^^^^^^^^^^^^^^^
473488

docs/client_reference.rst

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1128,7 +1128,8 @@ is controlled by *force_close* constructor's parameter).
11281128
resolver=None, keepalive_timeout=sentinel, \
11291129
force_close=False, limit=100, limit_per_host=0, \
11301130
enable_cleanup_closed=False, timeout_ceil_threshold=5, \
1131-
happy_eyeballs_delay=0.25, interleave=None, loop=None)
1131+
happy_eyeballs_delay=0.25, interleave=None, loop=None, \
1132+
tcp_sockopts=[])
11321133

11331134
Connector for working with *HTTP* and *HTTPS* via *TCP* sockets.
11341135

@@ -1249,6 +1250,12 @@ is controlled by *force_close* constructor's parameter).
12491250

12501251
.. versionadded:: 3.10
12511252

1253+
:param list tcp_sockopts: options applied to the socket when a connection is
1254+
created. This should be a list of 3-tuples, each a ``(level, optname, value)``.
1255+
Each tuple is deconstructed and passed verbatim to ``<socket>.setsockopt``.
1256+
1257+
.. versionadded:: 3.12
1258+
12521259
.. attribute:: family
12531260

12541261
*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
@@ -3767,6 +3767,29 @@ def test_connect() -> Literal[True]:
37673767
assert raw_response_list == [True, True]
37683768

37693769

3770+
async def test_tcp_connector_setsockopts(
3771+
loop: asyncio.AbstractEventLoop, start_connection: mock.AsyncMock
3772+
) -> None:
3773+
"""Check that sockopts get passed to socket"""
3774+
conn = aiohttp.TCPConnector(
3775+
tcp_sockopts=[(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 2)]
3776+
)
3777+
3778+
with mock.patch.object(
3779+
conn._loop, "create_connection", autospec=True, spec_set=True
3780+
) as create_connection:
3781+
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
3782+
start_connection.return_value = s
3783+
create_connection.return_value = mock.Mock(), mock.Mock()
3784+
3785+
req = ClientRequest("GET", URL("https://127.0.0.1:443"), loop=loop)
3786+
3787+
with closing(await conn.connect(req, [], ClientTimeout())):
3788+
assert s.getsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT) == 2
3789+
3790+
await conn.close()
3791+
3792+
37703793
def test_default_ssl_context_creation_without_ssl() -> None:
37713794
"""Verify _make_ssl_context does not raise when ssl is not available."""
37723795
with mock.patch.object(connector_module, "ssl", None):

0 commit comments

Comments
 (0)