Skip to content

Commit 9777213

Browse files
TingDaoKyilei
andauthored
Support free-thread build (#725)
Co-authored-by: Yilei Yang <hi@mangoumbrella.com>
1 parent 323d104 commit 9777213

16 files changed

Lines changed: 558 additions & 269 deletions

.github/workflows/ci.yml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ jobs:
5656
- cp311-cp311
5757
- cp312-cp312
5858
- cp313-cp313
59+
- cp313-cp313t
60+
- cp314-cp314
61+
- cp314-cp314t
5962
permissions:
6063
id-token: write # This is required for requesting the JWT
6164
steps:
@@ -81,6 +84,9 @@ jobs:
8184
- cp311-cp311
8285
- cp312-cp312
8386
- cp313-cp313
87+
- cp313-cp313t
88+
- cp314-cp314
89+
- cp314-cp314t
8490
permissions:
8591
id-token: write # This is required for requesting the JWT
8692
steps:
@@ -108,6 +114,7 @@ jobs:
108114
- cp311-cp311
109115
- cp312-cp312
110116
- cp313-cp313
117+
- cp313-cp313t
111118
permissions:
112119
id-token: write # This is required for requesting the JWT
113120
steps:
@@ -133,6 +140,7 @@ jobs:
133140
- cp311-cp311
134141
- cp312-cp312
135142
- cp313-cp313
143+
- cp313-cp313t
136144
permissions:
137145
id-token: write # This is required for requesting the JWT
138146
steps:

awscrt/aio/http.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from io import BytesIO
2323
from concurrent.futures import Future
2424
from typing import List, Tuple, Optional, Callable, AsyncIterator
25+
import threading
2526

2627

2728
class AIOHttpClientConnectionUnified(HttpClientConnectionBase):
@@ -328,7 +329,8 @@ class AIOHttpClientStreamUnified(HttpClientStreamBase):
328329
'_completion_future',
329330
'_stream_completed',
330331
'_status_code',
331-
'_loop')
332+
'_loop',
333+
'_deque_lock')
332334

333335
def __init__(self,
334336
connection: AIOHttpClientConnection,
@@ -347,8 +349,8 @@ def __init__(self,
347349
raise TypeError("loop must be an instance of asyncio.AbstractEventLoop")
348350
self._loop = loop
349351

350-
# deque is thread-safe for appending and popping, so that we don't need
351-
# locks to handle the callbacks from the C thread
352+
# Lock to protect check-then-act sequences on deques for thread safety in free-threaded Python
353+
self._deque_lock = threading.Lock()
352354
self._chunk_futures = deque()
353355
self._received_chunks = deque()
354356
self._stream_completed = False
@@ -373,12 +375,16 @@ def _on_response(self, status_code: int, name_value_pairs: List[Tuple[str, str]]
373375
self._response_headers_future.set_result(name_value_pairs)
374376

375377
def _on_body(self, chunk: bytes) -> None:
376-
"""Process body chunk on the correct event loop thread."""
377-
if self._chunk_futures:
378-
future = self._chunk_futures.popleft()
379-
future.set_result(chunk)
380-
else:
381-
self._received_chunks.append(chunk)
378+
"""Process body chunk - called from C thread."""
379+
with self._deque_lock:
380+
if self._chunk_futures:
381+
future = self._chunk_futures.popleft()
382+
else:
383+
self._received_chunks.append(chunk)
384+
return
385+
386+
# Set result outside lock (Future is thread-safe)
387+
future.set_result(chunk)
382388

383389
def _on_complete(self, error_code: int) -> None:
384390
"""Set the completion status of the stream."""
@@ -387,10 +393,14 @@ def _on_complete(self, error_code: int) -> None:
387393
else:
388394
self._completion_future.set_exception(awscrt.exceptions.from_code(error_code))
389395

390-
# Resolve all pending chunk futures with an empty string to indicate end of stream
391-
while self._chunk_futures:
392-
future = self._chunk_futures.popleft()
393-
future.set_result("")
396+
# Resolve all pending chunk futures with lock protection
397+
with self._deque_lock:
398+
pending_futures = list(self._chunk_futures)
399+
self._chunk_futures.clear()
400+
401+
# Set results outside lock (Future is thread-safe)
402+
for future in pending_futures:
403+
future.set_result(b"")
394404

395405
async def _set_request_body_generator(self, body_iterator: AsyncIterator[bytes]):
396406
...
@@ -418,14 +428,17 @@ async def get_next_response_chunk(self) -> bytes:
418428
bytes: The next chunk of data from the response body.
419429
Returns empty bytes when the stream is completed and no more chunks are left.
420430
"""
421-
if self._received_chunks:
422-
return self._received_chunks.popleft()
423-
elif self._completion_future.done():
424-
return b""
425-
else:
426-
future = Future()
427-
self._chunk_futures.append(future)
428-
return await asyncio.wrap_future(future, loop=self._loop)
431+
with self._deque_lock:
432+
if self._received_chunks:
433+
return self._received_chunks.popleft()
434+
elif self._completion_future.done():
435+
return b""
436+
else:
437+
future = Future()
438+
self._chunk_futures.append(future)
439+
440+
# Await outside lock
441+
return await asyncio.wrap_future(future, loop=self._loop)
429442

430443
async def wait_for_completion(self) -> int:
431444
"""Wait asynchronously for the stream to complete.

continuous-delivery/build-wheels-manylinux2014-aarch64.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ auditwheel repair --plat manylinux2014_aarch64 dist/awscrt-*cp311*.whl
2222
/opt/python/cp313-cp313/bin/python -m build
2323
auditwheel repair --plat manylinux2014_aarch64 dist/awscrt-*cp313*.whl
2424

25+
# The free-threaded build does not currently support the Limited C API or the stable ABI. Built them separately
26+
/opt/python/cp313-cp313t/bin/python -m build
27+
auditwheel repair --plat manylinux2014_aarch64 dist/awscrt-*cp313t*.whl
28+
/opt/python/cp314-cp314t/bin/python -m build
29+
auditwheel repair --plat manylinux2014_aarch64 dist/awscrt-*cp314t*.whl
30+
2531
rm dist/*.whl
2632
cp -rv wheelhouse/* dist/
2733

continuous-delivery/build-wheels-manylinux2014-x86_64.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ auditwheel repair --plat manylinux2014_x86_64 dist/awscrt-*cp311*.whl
2222
/opt/python/cp313-cp313/bin/python -m build
2323
auditwheel repair --plat manylinux2014_x86_64 dist/awscrt-*cp313*.whl
2424

25+
# The free-threaded build does not currently support the Limited C API or the stable ABI. Built them separately
26+
/opt/python/cp313-cp313t/bin/python -m build
27+
auditwheel repair --plat manylinux2014_x86_64 dist/awscrt-*cp313t*.whl
28+
/opt/python/cp314-cp314t/bin/python -m build
29+
auditwheel repair --plat manylinux2014_x86_64 dist/awscrt-*cp314t*.whl
30+
2531
rm dist/*.whl
2632
cp -rv wheelhouse/* dist/
2733

continuous-delivery/build-wheels-musllinux-1-1-aarch64.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ auditwheel repair --plat musllinux_1_1_aarch64 dist/awscrt-*cp311*.whl
2222
/opt/python/cp313-cp313/bin/python -m build
2323
auditwheel repair --plat musllinux_1_1_aarch64 dist/awscrt-*cp313*.whl
2424

25+
# The free-threaded build does not currently support the Limited C API or the stable ABI. Built them separately
26+
/opt/python/cp313-cp313t/bin/python -m build
27+
auditwheel repair --plat musllinux_1_1_aarch64 dist/awscrt-*cp313t*.whl
28+
# Musllinux-1-1 is EOL without python>3.13
29+
2530
rm dist/*.whl
2631
cp -rv wheelhouse/* dist/
2732

continuous-delivery/build-wheels-musllinux-1-1-x86_64.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,11 @@ auditwheel repair --plat musllinux_1_1_x86_64 dist/awscrt-*cp311*.whl
2222
/opt/python/cp313-cp313/bin/python -m build
2323
auditwheel repair --plat musllinux_1_1_x86_64 dist/awscrt-*cp313*.whl
2424

25+
# The free-threaded build does not currently support the Limited C API or the stable ABI. Built them separately
26+
/opt/python/cp313-cp313t/bin/python -m build
27+
auditwheel repair --plat musllinux_1_1_x86_64 dist/awscrt-*cp313t*.whl
28+
# Musllinux-1-1 is EOL without python>3.13
29+
2530
rm dist/*.whl
2631
cp -rv wheelhouse/* dist/
2732

continuous-delivery/build-wheels-osx.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,4 +15,8 @@ set -ex
1515
# We are using the Python 3.13 stable ABI from Python 3.13 onwards because of deprecated functions.
1616
/Library/Frameworks/Python.framework/Versions/3.13/bin/python3 -m build
1717

18+
# The free-threaded build does not currently support the Limited C API or the stable ABI. Built them separately
19+
/Library/Frameworks/PythonT.framework/Versions/3.13/bin/python3.13t -m build
20+
/Library/Frameworks/PythonT.framework/Versions/3.14/bin/python3.14t -m build
21+
1822
#now you just need to run twine (that's in a different script)

continuous-delivery/build-wheels-win32.bat

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,10 @@
1111
:: We are using the 3.13 stable ABI from 3.13 onwards because of deprecated functions.
1212
"C:\Program Files (x86)\Python313-32\python.exe" -m build || goto error
1313

14+
:: The free-threaded build does not currently support the Limited C API or the stable ABI. Built them separately
15+
"C:\Program Files (x86)\Python313-32\python3.13t.exe" -m build || goto error
16+
"C:\Program Files (x86)\Python314-32\python3.14t.exe" -m build || goto error
17+
1418
goto :EOF
1519

1620
:error

continuous-delivery/build-wheels-win64.bat

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
:: We are using the 3.13 stable ABI from 3.13 onwards because of deprecated functions.
1111
"C:\Program Files\Python313\python.exe" -m build || goto error
1212

13+
:: The free-threaded build does not currently support the Limited C API or the stable ABI. Built them separately
14+
"C:\Program Files\Python-313\python3.13t.exe" -m build || goto error
15+
"C:\Program Files\Python314\python3.14t.exe" -m build || goto error
16+
1317
goto :EOF
1418

1519
:error

setup.py

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@
2525
# sysconfig.get_config_var('MACOSX_DEPLOYMENT_TARGET').
2626
MACOS_DEPLOYMENT_TARGET_MIN = "10.15"
2727

28+
# True if this is a free-threaded Python build.
29+
FREE_THREADED_BUILD = sysconfig.get_config_var("Py_GIL_DISABLED") == 1
30+
2831
# This is the minimum version of the Windows SDK needed for schannel.h with SCH_CREDENTIALS and
2932
# TLS_PARAMETERS. These are required to build Windows Binaries with TLS 1.3 support.
3033
WINDOWS_SDK_VERSION_TLS1_3_SUPPORT = "10.0.17763.0"
@@ -428,7 +431,10 @@ class bdist_wheel_abi3(bdist_wheel):
428431
def get_tag(self):
429432
python, abi, plat = super().get_tag()
430433
# on CPython, our wheels are abi3 and compatible back to 3.11
431-
if python.startswith("cp") and sys.version_info >= (3, 13):
434+
if FREE_THREADED_BUILD:
435+
# free-threaded builds don't use limited API, so skip abi3 tag
436+
return python, abi, plat
437+
elif python.startswith("cp") and sys.version_info >= (3, 13):
432438
# 3.13 deprecates PyWeakref_GetObject(), adds alternative
433439
return "cp313", "abi3", plat
434440
elif python.startswith("cp") and sys.version_info >= (3, 11):
@@ -532,7 +538,11 @@ def awscrt_ext():
532538
extra_link_args += ['-Wl,--fatal-warnings']
533539

534540
# prefer building with stable ABI, so a wheel can work with multiple major versions
535-
if sys.version_info >= (3, 13):
541+
if FREE_THREADED_BUILD and sys.version_info[:2] <= (3, 14):
542+
# 3.14 free threaded (aka no gil) does not support limited api.
543+
# disable it for now. 3.15 promises to support limited api + free threading combo
544+
py_limited_api = False
545+
elif sys.version_info >= (3, 13):
536546
# 3.13 deprecates PyWeakref_GetObject(), adds alternative
537547
define_macros.append(('Py_LIMITED_API', '0x030D0000'))
538548
py_limited_api = True

0 commit comments

Comments
 (0)