Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGES/12744.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Added ``aiofastnet`` package to ``speedups`` extra. aiofastnet provides faster alternatives to the standard loop functions, which are used to run server or establish connections. If you experience any issues that you think might be related to this change, you can try to disable ``aiofastnet`` by uninstalling aiofastnet package.

-- by :user:`tarasko`.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -367,6 +367,7 @@ Sunit Deshpande
Sviatoslav Bulbakha
Sviatoslav Sydorenko
Taha Jahangir
Taras Kozlov
Taras Voinarovskyi
Terence Honles
Thanos Lefteris
Expand Down
34 changes: 31 additions & 3 deletions aiohttp/connector.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import functools
import importlib
import random
import socket
import sys
Expand All @@ -18,6 +19,13 @@
from aiohappyeyeballs import AddrInfoType, SocketFactoryType
from multidict import CIMultiDict

aiofastnet: Any
try:
aiofastnet = importlib.import_module("aiofastnet")
except ImportError:
aiofastnet = None


from . import hdrs, helpers
from .abc import AbstractResolver, ResolveResult
from .client_exceptions import (
Expand Down Expand Up @@ -96,6 +104,24 @@
from .tracing import Trace


async def create_connection(
loop: asyncio.AbstractEventLoop, *args: Any, **kwargs: Any,
) -> tuple[asyncio.Transport, ResponseHandler]:
if aiofastnet is not None:
return await aiofastnet.create_connection(loop, *args, **kwargs) # type: ignore[no-any-return]
else:
return await loop.create_connection(*args, **kwargs)


async def start_tls(
loop: asyncio.AbstractEventLoop, *args: Any, **kwargs: Any
) -> asyncio.BaseTransport | None:
if aiofastnet is not None:
return await aiofastnet.start_tls(loop, *args, **kwargs) # type: ignore[no-any-return]
else:
return await loop.start_tls(*args, **kwargs)


class Connection:
"""Represents a single connection."""

Expand Down Expand Up @@ -1259,7 +1285,7 @@ async def _wrap_create_connection(
and sys.version_info >= (3, 11)
):
kwargs["ssl_shutdown_timeout"] = self._ssl_shutdown_timeout
return await self._loop.create_connection(*args, **kwargs, sock=sock)
return await create_connection(self._loop, *args, **kwargs, sock=sock)
except cert_errors as exc:
raise ClientConnectorCertificateError(req.connection_key, exc) from exc
except ssl_errors as exc:
Expand Down Expand Up @@ -1340,7 +1366,8 @@ async def _start_tls_connection(
try:
# ssl_shutdown_timeout is only available in Python 3.11+
if sys.version_info >= (3, 11) and self._ssl_shutdown_timeout:
tls_transport = await self._loop.start_tls(
tls_transport = await start_tls(
self._loop,
underlying_transport,
tls_proto,
sslcontext,
Expand All @@ -1349,7 +1376,8 @@ async def _start_tls_connection(
ssl_shutdown_timeout=self._ssl_shutdown_timeout,
)
else:
tls_transport = await self._loop.start_tls(
tls_transport = await start_tls(
self._loop,
underlying_transport,
tls_proto,
sslcontext,
Expand Down
19 changes: 18 additions & 1 deletion aiohttp/web_fileresponse.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import asyncio
import importlib
import io
import os
import pathlib
Expand All @@ -11,6 +12,13 @@
from types import MappingProxyType
from typing import IO, TYPE_CHECKING, Any, Final, Optional

aiofastnet: Any
try:
aiofastnet = importlib.import_module("aiofastnet")
except ImportError:
aiofastnet = None


from . import hdrs
from .abc import AbstractStreamWriter
from .helpers import DEFAULT_CHUNK_SIZE, ETAG_ANY, ETag, must_be_empty_body
Expand All @@ -34,6 +42,15 @@
_T_OnChunkSent = Optional[Callable[[bytes], Awaitable[None]]]


async def sendfile(
loop: asyncio.AbstractEventLoop, *args: Any, **kwargs: Any
) -> int:
if aiofastnet is not None:
return await aiofastnet.sendfile(loop, *args, **kwargs) # type: ignore[no-any-return]
else:
return await loop.sendfile(*args, **kwargs)


NOSENDFILE: Final[bool] = bool(os.environ.get("AIOHTTP_NOSENDFILE"))

CONTENT_TYPES: Final[MimeTypes] = MimeTypes()
Expand Down Expand Up @@ -132,7 +149,7 @@ async def _sendfile(
raise ConnectionResetError("Connection lost")

try:
await loop.sendfile(transport, fobj, offset, count)
await sendfile(loop, transport, fobj, offset, count)
except NotImplementedError:
return await self._sendfile_fallback(writer, fobj, offset, count)

Expand Down
24 changes: 21 additions & 3 deletions aiohttp/web_runner.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import asyncio
import importlib
import signal
import socket
from abc import ABC, abstractmethod
from typing import Any, Generic, TypeVar

from yarl import URL

aiofastnet: Any
try:
aiofastnet = importlib.import_module("aiofastnet")
except ImportError:
aiofastnet = None

from .abc import AbstractAccessLogger, AbstractStreamWriter
from .http_parser import RawRequestMessage
from .streams import StreamReader
Expand All @@ -21,6 +28,16 @@
except ImportError: # pragma: no cover
SSLContext = object # type: ignore[misc,assignment]


async def create_server(
loop: asyncio.AbstractEventLoop, *args: Any, **kwargs: Any
) -> asyncio.Server:
if aiofastnet is not None:
return await aiofastnet.create_server(loop, *args, **kwargs) # type: ignore[no-any-return]
else:
return await loop.create_server(*args, **kwargs)


__all__ = (
"BaseSite",
"TCPSite",
Expand Down Expand Up @@ -130,7 +147,8 @@ async def start(self) -> None:
loop = asyncio.get_running_loop()
server = self._runner.server
assert server is not None
self._server = await loop.create_server(
self._server = await create_server(
loop,
server,
self._host,
self._port,
Expand Down Expand Up @@ -244,8 +262,8 @@ async def start(self) -> None:
loop = asyncio.get_running_loop()
server = self._runner.server
assert server is not None
self._server = await loop.create_server(
server, sock=self._sock, ssl=self._ssl_context, backlog=self._backlog
self._server = await create_server(
loop, server, sock=self._sock, ssl=self._ssl_context, backlog=self._backlog
)


Expand Down
46 changes: 46 additions & 0 deletions docs/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,52 @@ enable compression in NGINX (you are deploying aiohttp behind reverse
proxy, right?).


How do I enable Kernel TLS, and should I do it?
-----------------------------------------------

Kernel TLS (KTLS) allows aiohttp to move encryption and decryption of
TLS traffic from user space to the kernel. It was added to the Linux kernel in
4.13, but full support for TLS 1.3 and modern ciphers is available only
since 5.19.

KTLS will be beneficial if you run an HTTPS server that often returns
:class:`~aiohttp.web.FileResponse` objects or you have a high-end NIC that can
offload TLS encryption. For ordinary
dynamic responses, small files, or deployments behind a TLS-terminating reverse
proxy, it is unlikely to help and may actually slightly degrade performance.

KTLS is supported through the ``aiofastnet`` package, which is installed as
part of the ``speedups`` extra.

To enable KTLS, you have to do and check the following:

* Make sure the Linux ``tls`` kernel module is loaded::

sudo modprobe tls

* Make sure the ``ssl.OP_ENABLE_KTLS`` option is enabled in ``SSLContext``
(available since Python 3.12)::

sslcontext.options |= ssl.OP_ENABLE_KTLS

* Make sure Python is using OpenSSL 3.0 or newer. OpenSSL should have been
built on a machine whose Linux headers are new enough. OpenSSL needs Linux
headers at least 4.13.0 to build the transmit path; older headers make it
skip KTLS support. Typically, Python is using the system OpenSSL on Linux,
but some times distributions ship their own OpenSSL. The following commands
will help identify the OpenSSL version and which ``libssl`` and ``libcrypto``
are being used by the ``ssl`` module::

python -c "import ssl; print(ssl.OPENSSL_VERSION)"
ldd "$(python -c 'import _ssl; print(_ssl.__file__)')"


If ``ssl.OP_ENABLE_KTLS`` was requested in ``sslcontext``, but ``aiofastnet``
could not enable KTLS, it will log a warning suggesting the possible reason.

After enabling it, run your own benchmarks and verify that KTLS actually
speeds things up in your case.

How do I manage a ClientSession within a web server?
----------------------------------------------------

Expand Down
4 changes: 4 additions & 0 deletions docs/spelling_wordlist.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ ABI
addons
aiodns
aioes
aiofastnet
aiohttp
aiohttpdemo
aiohttp’s
Expand Down Expand Up @@ -183,6 +184,7 @@ keepalive
keepalived
keepalives
keepaliving
KTLS
kib
KiB
kwarg
Expand Down Expand Up @@ -227,6 +229,7 @@ namedtuple
nameservers
namespace
netrc
NIC
nginx
Nginx
Nikolay
Expand All @@ -236,6 +239,7 @@ nowait
OAuth
Online
optimizations
OpenSSL
orjson
os
outcoming
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ dynamic = [
[project.optional-dependencies]
speedups = [
"aiodns >= 3.3.0; sys_platform != 'android' and sys_platform != 'ios'",
"aiofastnet >= 0.9.0; platform_python_implementation == 'CPython' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'aarch64')",
"Brotli >= 1.2; platform_python_implementation == 'CPython' and sys_platform != 'android' and sys_platform != 'ios'",
"brotlicffi >= 1.2; platform_python_implementation != 'CPython'",
"backports.zstd; platform_python_implementation == 'CPython' and python_version < '3.14' and sys_platform != 'android' and sys_platform != 'ios'",
Expand Down
1 change: 1 addition & 0 deletions requirements/lint.in
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
aiodns
aiofastnet
backports.zstd; implementation_name == "cpython" and python_version < "3.14"
blockbuster
freezegun
Expand Down
1 change: 1 addition & 0 deletions requirements/runtime-deps.in
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Extracted from `pyproject.toml` via `make sync-direct-runtime-deps`

aiodns >= 3.3.0; sys_platform != 'android' and sys_platform != 'ios'
aiofastnet >= 0.9.0; platform_python_implementation == 'CPython' and (platform_machine == 'x86_64' or platform_machine == 'AMD64' or platform_machine == 'aarch64')
aiohappyeyeballs >= 2.5.0
aiosignal >= 1.4.0
async-timeout >= 4.0, < 6.0 ; python_version < '3.11'
Expand Down
4 changes: 4 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,10 @@ def blockbuster(request: pytest.FixtureRequest) -> Iterator[None]:
# synchronization in async code.
# Allow lock.acquire calls to prevent these false positives
bb.functions["threading.Lock.acquire"].deactivate()

# aiofastnet is using sendfile on a non-blocking socket.
# blockbuster triggers anyway. Seems like a false positive
bb.functions["os.sendfile"].deactivate()
yield


Expand Down
Loading
Loading