aiohttp icon indicating copy to clipboard operation
aiohttp copied to clipboard

Support free-threaded Python 3.13 build

Open ngoldbaum opened this issue 1 year ago • 20 comments

Is your feature request related to a problem?

Python 3.13 will be available as an experimental free-threaded build that has the GIL disabled. It should hopefully be possible to build aiohttp on the free-threaded build with an updated Cython. I just did so (with some makefile hacking so the build uses a prerelease cython):

Python 3.13.0rc1 experimental free-threading build (main, Aug 14 2024, 13:20:46) [Clang 15.0.0 (clang-1500.3.9.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import aiohttp
<frozen importlib._bootstrap>:488: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'aiohttp._helpers', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.

Describe the solution you'd like

I don't know what thread safety issues exist in aiohttp, but assuming any C/C++ code is thread safe and the cython code is thread safe, you can declare the modules support free-threading by following https://py-free-threading.github.io/porting/.

Describe alternatives you've considered

Not supporting it?

Related component

Server, Client

Additional context

No response

Code of Conduct

  • [X] I agree to follow the aio-libs Code of Conduct

ngoldbaum avatar Aug 20 '24 15:08 ngoldbaum

If you'd like to help, I think the first thing would be to add a new test run to the CI, so we have some test coverage over this new approach. But, I'd assume the Cython code is likely thread-safe as it's just used for parsers.

Probably need the current 3.13 tests passing first though, which will hopefully happen next week after uvloop publish a 3.13 wheel.

Dreamsorcerer avatar Aug 20 '24 15:08 Dreamsorcerer

I think that getting our in-org hosted deps to run CI / make builds might be another blocker. I think that aiohttp's CI should make use of those wheels rather than compiling them.

webknjaz avatar Aug 21 '24 13:08 webknjaz

3.13 is fully tested and passing now, if anyone wants to look at adding the free-threading testing.

Dreamsorcerer avatar Sep 27 '24 02:09 Dreamsorcerer

@Yushu2606 what's your point? That doesn't seem related to the discussion here.

webknjaz avatar Oct 18 '24 12:10 webknjaz

@Yushu2606 you probably aren't building with a nightly cython. Right now there isn't a tagged cython version or release that supports free-threading.

ngoldbaum avatar Oct 18 '24 13:10 ngoldbaum

Please add cython version or release that supports free-threading.

patrizok avatar Nov 13 '24 14:11 patrizok

Is this forgotten?

mshakery avatar Mar 07 '25 21:03 mshakery

No, plenty of work has been done on aiohttp dependencies, so looks like it's shaping up for a release in the near future. Plenty of the ecosystem is still not ready for free-threaded, so it's not been a trivial change (e.g. needing to use an alpha release of Cython).

Dreamsorcerer avatar Mar 07 '25 23:03 Dreamsorcerer

i'm interested, too. is there a list of free-threaded blockers that need to be addressed?

electroglyph avatar Apr 02 '25 05:04 electroglyph

welp, i just built it for free-threaded on Windows. here's how I did it, i'm sure there will be opinions =) there's surely a less hacky way to go with the macros...

i've only tested the http client, which works, but i get this msg, any pointers on how to remove this warning would be appreciated:

<frozen importlib._bootstrap>:488: RuntimeWarning: The global interpreter lock (GIL) has been enabled to load module 'aiohttp._http_writer', which has not declared that it can run safely without the GIL. To override this behavior and keep the GIL disabled (at your own risk), run with PYTHON_GIL=0 or -Xgil=0.

here's what i changed:

setup.py:

reason: cython's maze of #defs and #undefs

macros = [("GRAALVM_PYTHON", "1"), ("Py_GIL_DISABLED", "1")]
extensions = [
    Extension("aiohttp._websocket.mask", ["aiohttp/_websocket/mask.c"], define_macros=macros),
    Extension(
        "aiohttp._http_parser",
        [
            "aiohttp/_http_parser.c",
            "aiohttp/_find_header.c",
            "vendor/llhttp/build/c/llhttp.c",
            "vendor/llhttp/src/native/api.c",
            "vendor/llhttp/src/native/http.c",
        ],
        define_macros=[("LLHTTP_STRICT_MODE", 0),("GRAALVM_PYTHON", "1"), ("Py_GIL_DISABLED", "1")],
        include_dirs=["vendor/llhttp/build"],
    ),
    Extension("aiohttp._http_writer", ["aiohttp/_http_writer.c"], define_macros=macros),
    Extension("aiohttp._websocket.reader_c", ["aiohttp/_websocket/reader_c.c"], define_macros=macros),
]

_http_parser.c, line 30742: reason: aiohttp/_http_parser.c(30744): error C2039: 'ob_refcnt': is not a member of '_object'

#if !CYTHON_USE_TP_FINALIZE
    assert(Py_REFCNT(self) > 0);
    // if (likely(--self->ob_refcnt == 0)) {
    //     return;
    // }
    {
        Py_ssize_t refcnt = Py_REFCNT(self);
        _Py_NewReference(self);
        __Pyx_SET_REFCNT(self, refcnt);
    }

websocket\reader_c.c line 18485: reason: aiohttp/_websocket/reader_c.c(18487): error C2039: 'ob_refcnt': is not a member of '_object'

#if !CYTHON_USE_TP_FINALIZE
    assert(Py_REFCNT(self) > 0);
    // if (likely(--self->ob_refcnt == 0)) {
    //     return;
    // }
    {
        Py_ssize_t refcnt = Py_REFCNT(self);
        _Py_NewReference(self);
        __Pyx_SET_REFCNT(self, refcnt);
    }

electroglyph avatar Apr 02 '25 06:04 electroglyph

Well, the warning is because it doesn't support free-threading. Fixing the warning is done by supporting free-threading, i.e. what this issue tracks. I think multidict and frozenlist have free-threading now, and yarl I believe is ready to merge and release. That'll probably be all the dependencies sorted, then it's just aiohttp itself that needs a similar update, so seems like it's getting close now.

Dreamsorcerer avatar Apr 02 '25 13:04 Dreamsorcerer

Isn't there some optional DNS/speedup stuff on top of the mandatory deps?

webknjaz avatar Apr 02 '25 14:04 webknjaz

I would assume an optional dependency is not a blocker to us. Maybe pycares will need an update? But, people can use it without aiodns until that becomes supported.

Dreamsorcerer avatar Apr 02 '25 15:04 Dreamsorcerer

It looks like pycares uses CFFI, so that might require using our fork of CFFI to get that working. See https://github.com/python-cffi/cffi/pull/143#issuecomment-2581049170. I'll open an issue over at the pycares repo.

ngoldbaum avatar Apr 02 '25 15:04 ngoldbaum

Well, the warning is because it doesn't support free-threading. Fixing the warning is done by supporting free-threading, i.e. what this issue tracks.

right, which was what i was asking about. in pybind11 it's a simple one line code change to disable GIL for an entire module. do we have something simple like that available to us?

i just looked it up, and it seems the answer is no for now. setting PYTHON_GIL=0 is still the method listed in cython docs.

electroglyph avatar Apr 03 '25 00:04 electroglyph

is still the method listed in cython docs

There is a way to declare free-threaded support in a cython module:

https://py-free-threading.github.io/porting-extensions/#__tabbed_1_3

You need to use a nightly Cython for now. Hopefully we’ll see a Cython 3.1 release soon! Or at least a beta…

That said, there’s not much point in doing that here until aiohttp’s dependencies do it first.

ngoldbaum avatar Apr 03 '25 00:04 ngoldbaum

i've barely tested because i only need the http client, but i'm using it in my free-threaded project right now. i'm new around here, but it's at least usable for some people right this moment. i have multidict and yarl installed in my 3.13t venv and i didn't do anything special...(except maybe have a build environment....no idea if i built them or not)

edit...multidict has a published 3.13t wheel, but it looks like yarl doesn't, which means i must have successfully built it on windows. and i have absolutely no idea if my code has actually tried using it yet...

electroglyph avatar Apr 03 '25 00:04 electroglyph

@webknjaz @Dreamsorcerer @asvetlov HF Datasets depends on the package. Dataset is used in model training and quantization. At this moment, the entire HF train/quant eco system is collasping on 3.13t support due to aiohttp unable to compile under free-threading env. Thanks!

@electroglyph @ngoldbaum I am desperate for a solution so I can get aiothttp compiled as my GPTQModel project depends on pypi: datasets and datasets massively embed/uses aiothttp in all it's code it's not trivial to find a replacement. Thanks for any pointers.

Qubitium avatar May 02 '25 09:05 Qubitium

@Qubitium feel free to contribute supporting this runtime.

webknjaz avatar May 02 '25 11:05 webknjaz

@Qubitium feel free to contribute supporting this runtime.

@webknjaz I am asking for help. I have contributed to many oss projects. I can't fix everyone's compat issues and bugs. If you can tell me which specific parts that needs fixing, starting with basic compile, I am willing to spend some time fixing it.

Qubitium avatar May 02 '25 17:05 Qubitium

We need list of all broken deps and checkboxes, it will be the best on current stage.

stalkerg avatar Jun 21 '25 04:06 stalkerg

I think all required dependencies are done now. Not sure I've seen a PR to aiohttp yet though, so probably just aiohttp remaining now.

I'm still not familiar with exactly what changes are needed, but maybe looking at the other PRs can help, e.g.: https://github.com/aio-libs/multidict/pull/1015/files https://github.com/aio-libs/yarl/pull/1456/files https://github.com/aio-libs/frozenlist/pull/618/files

pycares (aiodns) is also still missing support, but that is an optional dependency.

Dreamsorcerer avatar Jun 21 '25 11:06 Dreamsorcerer

I tried setting up aiohttp and running the tests on the free-threaded build today and ran into some issues, mostly around setting up test dependencies. Here are some notes:

A number of test dependencies in turn depend on CFFI, which does not yet support the free-threaded build. I have a PR open to CFFI to add support (https://github.com/python-cffi/cffi/pull/178) and hopefully that will be fixed soon, allowing myself and others to add support in reverse dependencies of CFFI.

For now to get things working you'll need to install CFFI from that PR branch and then manually build test dependencies of aiohttp that depend on CFFI with build isolation disabled. Additionally, you'll need to build python-zstandard from the PR branch that adds support: https://github.com/indygreg/python-zstandard/pulls. It might make sense to use the backports.zstd package instead of zstandard: https://pypi.org/project/backports.zstd/ due to maintenance issues in python-zstandard.

For uvloop, see https://github.com/MagicStack/uvloop/issues/642#issuecomment-3084563094.

With all that, I was able to run the tests. I had to force-disable the GIL and hack the tests to not crash on warnings due to a warning coming from pytest-xdist about --rsyncdir. After that I saw two failures:

___________________________________________ test_testcase_no_app ___________________________________________
[gw7] darwin -- Python 3.13.5 /Users/goldbaum/.pyenv/versions/3.13.5t/bin/python3.13
[XPASS(strict)] https://github.com/pytest-dev/pytest/issues/13546
------------------------------------------- Captured stdout call -------------------------------------------
=========================================== test session starts ============================================
platform darwin -- Python 3.13.5, pytest-7.4.0, pluggy-1.6.0
codspeed: 4.0.0 (disabled, mode: walltime, callgraph: not supported, timer_resolution: 41.7ns)
rootdir: /private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw7/test_testcase_no_app0
plugins: xdist-3.7.0, codspeed-4.0.0, timeout-2.4.0, cov-4.1.0, mock-3.14.1, run-parallel-0.4.5.dev0, hypothesis-6.135.32
collected 1 item
Collected 0 items to run in parallel

test_testcase_no_app.py E                                                                            [100%]

================================================== ERRORS ==================================================
_______________________________ ERROR at setup of InvalidTestCase.test_noop ________________________________

cls = <class '_pytest.runner.CallInfo'>
func = <function call_runtest_hook.<locals>.<lambda> at 0x500283a5780>, when = 'setup'
reraise = (<class '_pytest.outcomes.Exit'>, <class 'KeyboardInterrupt'>)

    @classmethod
    def from_call(
        cls,
        func: "Callable[[], TResult]",
        when: "Literal['collect', 'setup', 'call', 'teardown']",
        reraise: Optional[
            Union[Type[BaseException], Tuple[Type[BaseException], ...]]
        ] = None,
    ) -> "CallInfo[TResult]":
        """Call func, wrapping the result in a CallInfo.

        :param func:
            The function to call. Called without arguments.
        :param when:
            The phase in which the function is called.
        :param reraise:
            Exception or exceptions that shall propagate if raised by the
            function, instead of being wrapped in the CallInfo.
        """
        excinfo = None
        start = timing.time()
        precise_start = timing.perf_counter()
        try:
>           result: Optional[TResult] = func()

/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:341:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:262: in <lambda>
    lambda: ihook(item=item, **kwds), when=when, reraise=reraise
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_hooks.py:512: in __call__
    return self._hookexec(self.name, self._hookimpls.copy(), kwargs, firstresult)
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_manager.py:120: in _hookexec
    return self._inner_hookexec(hook_name, methods, kwargs, firstresult)
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_manager.py:475: in traced_hookexec
    return outcome.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_manager.py:472: in <lambda>
    lambda: oldcall(hook_name, hook_impls, caller_kwargs, firstresult)
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:53: in run_old_style_hookwrapper
    return result.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:38: in run_old_style_hookwrapper
    res = yield
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:53: in run_old_style_hookwrapper
    return result.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:38: in run_old_style_hookwrapper
    res = yield
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:53: in run_old_style_hookwrapper
    return result.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:38: in run_old_style_hookwrapper
    res = yield
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:53: in run_old_style_hookwrapper
    return result.get_result()
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/pluggy/_callers.py:38: in run_old_style_hookwrapper
    res = yield
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:157: in pytest_runtest_setup
    item.session._setupstate.setup(item)
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:497: in setup
    raise exc
/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/runner.py:494: in setup
    col.setup()
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <TestCaseFunction test_noop>

    def setup(self) -> None:
        # A bound method to be called during teardown() if set (see 'runtest()').
        self._explicit_tearDown: Optional[Callable[[], None]] = None
        assert self.parent is not None
>       self._testcase = self.parent.obj(self.name)  # type: ignore[attr-defined]
E       TypeError: Can't instantiate abstract class InvalidTestCase without an implementation for abstract method 'get_application'

/Users/goldbaum/.pyenv/versions/3.13.5t/lib/python3.13t/site-packages/_pytest/unittest.py:201: TypeError
========================================= short test summary info ==========================================
ERROR test_testcase_no_app.py::InvalidTestCase::test_noop - TypeError: Can't instantiate abstract class I...
============================================= 1 error in 0.18s =============================================
________________________________ test_response_with_bodypart_named[pyloop] _________________________________
[gw3] darwin -- Python 3.13.5 /Users/goldbaum/.pyenv/versions/3.13.5t/bin/python3.13

aiohttp_client = <function aiohttp_client.<locals>.go at 0x52e2d99b360>
tmp_path = PosixPath('/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0')

    async def test_response_with_bodypart_named(
        aiohttp_client: AiohttpClient, tmp_path: pathlib.Path
    ) -> None:
        async def handler(request: web.Request) -> web.Response:
            reader = await request.multipart()
            part = await reader.next()
            return web.Response(body=part)

        app = web.Application(client_max_size=2)
        app.router.add_post("/", handler)
        client = await aiohttp_client(app)

        f = tmp_path / "foobar.txt"
        f.write_text("test", encoding="utf8")
>       with f.open("rb") as fd:
E       ValueError: I/O operation on closed file.

aiohttp_client = <function aiohttp_client.<locals>.go at 0x52e2d99b360>
app        = <Application 0x52e28d7e280>
body       = b'test'
client     = <aiohttp.test_utils.TestClient object at 0x52e29281430>
data       = {'file': <_io.BufferedReader name='/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0/foobar.txt'>}
f          = PosixPath('/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0/foobar.txt')
fd         = <_io.BufferedReader name='/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0/foobar.txt'>
handler    = <function test_response_with_bodypart_named.<locals>.handler at 0x52e2d99c120>
resp       = <ClientResponse(http://127.0.0.1:55063/) [200 OK]>
<CIMultiDictProxy('Content-Type': 'application/octet-stream', 'Cont...Transfer-Encoding': 'chunked', 'Date': 'Thu, 17 Jul 2025 16:02:44 GMT', 'Server': 'Python/3.13 aiohttp/4.0.0a2.dev0')>

tmp_path   = PosixPath('/private/var/folders/nk/yds4mlh97kg9qdq745g715rw0000gn/T/pytest-of-goldbaum/pytest-1/popen-gw3/test_response_with_bodypart_na0')

tests/test_web_functional.py:1833: ValueError

I'm not sure if the maintainers are open to adding complexity to the test setup to avoid issues with test dependencies on the free-threaded build. If not, we probably need to wait until CFFI is unblocked at least.

ngoldbaum avatar Jul 17 '25 16:07 ngoldbaum

[XPASS(strict)] https://github.com/pytest-dev/pytest/issues/13546

You're running a much older version of pytest.

  with f.open("rb") as fd:

E ValueError: I/O operation on closed file.

This one looks weird. How can it be a closed file when we're attempting to open it?

Dreamsorcerer avatar Jul 17 '25 16:07 Dreamsorcerer

You're running a much older version of pytest.

Thanks! This is because of the hacked-together, manually constructed environment.

This one looks weird. How can it be a closed file when we're attempting to open it?

On a second look, I agree. Maybe there's something tricky going on in the pathlib implementation along the lines of https://github.com/python/cpython/issues/136248, which seems similar. I'll try to reproduce this outside of aiohttp.

ngoldbaum avatar Jul 17 '25 16:07 ngoldbaum

I just re-did this exercise with a Python 3.14t interpreter. This required even more hacking of test dependencies, since a few of the dependencies in the test requirements file are pinned to versions that don't support 3.14.

With 3.14t, I no longer see the pathlib error I saw before. It's possible the fix for the issue I saw has been fixed in CPython main for a while and the fix was never backported to 3.13 - I'll try to pin that down so we can make sure there's a backport.

With 3.14t I see the following test failures and errors in the aiohttp tests:

ERROR tests/test_proxy_functional.py::test_uvloop_secure_https_proxy - AttributeError: 'NoneType' object has no attribute 'new_event_loop'
FAILED tests/test_helpers.py::test_when_timeout_smaller_second[pyloop] - assert 0.005357708083465695 == 0 ± 0.001
FAILED tests/test_cookiejar.py::test_pickle_format - AssertionError: assert defaultdict(<...'fifteenth'>}) == defaultdict(<...'fifteenth'>})
FAILED tests/test_circular_imports.py::test_no_warnings[aiohttp._http_parser] - subprocess.CalledProcessError: Command '('/Users/goldbaum/.pyenv/versions/3.14.0rc1t/bin/python3.14', '-W', 'error', '-W',...
FAILED tests/test_circular_imports.py::test_no_warnings[aiohttp._http_writer] - subprocess.CalledProcessError: Command '('/Users/goldbaum/.pyenv/versions/3.14.0rc1t/bin/python3.14', '-W', 'error', '-W',...
FAILED tests/test_circular_imports.py::test_no_warnings[aiohttp._websocket.mask] - subprocess.CalledProcessError: Command '('/Users/goldbaum/.pyenv/versions/3.14.0rc1t/bin/python3.14', '-W', 'error', '-W',...

I think these are probably all 3.14 compatibility issues? I would need to look closer.

ngoldbaum avatar Jul 23 '25 20:07 ngoldbaum

Possibly. With 3.14 in the release candidate stage, we should be able to start preparing 3.14 releases now.

Dreamsorcerer avatar Jul 24 '25 12:07 Dreamsorcerer

I've added 3.14 back to the testing matrix. Looks like initial blockers are isal and pydantic-core. I think pydantic is already resolved, but Dependabot seems to think it's not upgradeable, so may need a manual version bump.

Dreamsorcerer avatar Jul 24 '25 15:07 Dreamsorcerer

When I tried yesterday the incompatible constraint on pydantic-core was coming in from python-on-whales

ngoldbaum avatar Jul 24 '25 15:07 ngoldbaum

I've added 3.14 back to the testing matrix. Looks like initial blockers are isal and pydantic-core. I think pydantic is already resolved, but Dependabot seems to think it's not upgradeable, so may need a manual version bump.

I have updated my PR at https://github.com/aio-libs/aiohttp/pull/10872 to avoid installing isal and python-on-whales on 3.14 as they are test are only dependencies and skipped tests if not present, with that most tests are now passing except one in cookie jar which was changed in 3.14.

I think this is a good starting point to merge that than waiting for all such non-required dependencies to ship 3.14 wheels.

cc @Dreamsorcerer

kumaraditya303 avatar Jul 26 '25 08:07 kumaraditya303