diff --git a/.github/workflows/_checks.yml b/.github/workflows/_checks.yml index 60021cc..2a98dda 100644 --- a/.github/workflows/_checks.yml +++ b/.github/workflows/_checks.yml @@ -136,10 +136,6 @@ jobs: freethreaded: runs-on: ubuntu-latest - env: - # SQLAlchemy's cyext ships cp314t wheels but doesn't declare free-thread - # safety, so importing it re-enables the GIL; use its pure-Python fallback. - DISABLE_SQLALCHEMY_CEXT_RUNTIME: 1 services: postgres: image: postgres:17 diff --git a/docs/adr/0002-free-threading-is-compat-only.md b/docs/adr/0002-free-threading-is-compat-only.md index 868851f..99e3dbc 100644 --- a/docs/adr/0002-free-threading-is-compat-only.md +++ b/docs/adr/0002-free-threading-is-compat-only.md @@ -7,8 +7,7 @@ free-threading changes none of its runtime semantics and the guarantee costs a C `Free Threading :: 2 - Beta` classifier, and a docs note rather than a source change. Exploiting the parallelism was rejected because the two-loop subscriber, the lease-token invariant, and drain-on-stop all assume one loop, throughput is dominated by Postgres I/O, and scaling today means -running more subscriber processes. The guarantee is bounded by SQLAlchemy: its Cython extensions do -not declare `Py_MOD_GIL_NOT_USED` and re-enable the GIL process-wide on import, so -`DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` is load-bearing until upstream fixes that -([#160](https://github.com/modern-python/faststream-outbox/issues/160)). 3.13t is not a target +running more subscriber processes. The guarantee was bounded by SQLAlchemy until 2.0.54, whose +cyextensions declare `Py_MOD_GIL_NOT_USED`; below it, importing SQLAlchemy re-enabled the GIL +process-wide, which is why 2.0.54 is the floor on 3.14. 3.13t is not a target because the compiled dependencies ship `cp314t` wheels only. diff --git a/docs/introduction/installation.md b/docs/introduction/installation.md index f053e6f..60aa19e 100644 --- a/docs/introduction/installation.md +++ b/docs/introduction/installation.md @@ -39,17 +39,15 @@ asyncio, so nothing in it depends on the GIL; installing on a `python3.14t` interpreter resolves the free-threaded wheels of the compiled dependencies (`asyncpg`, `sqlalchemy`, `pydantic-core`) automatically. -!!! note "Keep the GIL disabled: `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1`" +!!! note "A dependency that has not declared free-thread safety re-enables the GIL" - SQLAlchemy's Cython extensions ship free-threaded wheels but do not yet - declare themselves free-thread-safe, so importing SQLAlchemy re-enables the - GIL process-wide. Your outbox code still runs correctly either way, but if - you want the GIL to stay disabled — for example because other parts of your - process use threads for parallelism — set `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` - (SQLAlchemy's own switch; it falls back to pure-Python implementations). This - is what CI runs, and it is what lets the GIL stay off. + CPython re-enables the GIL process-wide when it imports an extension module + that has not declared itself free-thread-safe. The outbox's own runtime + dependencies are clear of this: SQLAlchemy declares safety as of 2.0.54, + which is the floor on 3.14 for that reason, and `asyncpg` and + `pydantic-core` never had the problem. - The same caveat applies to any foreign-broker client you install for the + It still applies to any foreign-broker client you install for the [relay feature](../usage/relay.md): if it hasn't declared free-thread safety (for example `aiokafka`), importing it re-enables the GIL. That is the client library's limitation, not the outbox's — your outbox code still runs diff --git a/pyproject.toml b/pyproject.toml index e76a658..4201234 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,12 @@ dependencies = [ # because 2.0 is fine on 3.11/3.12 and there is no reason to raise the floor for older # interpreters. "sqlalchemy[asyncio]>=2.0; python_version < '3.13'", - "sqlalchemy[asyncio]>=2.0.31; python_version >= '3.13'", + "sqlalchemy[asyncio]>=2.0.31; python_version == '3.13'", + # 2.0.54 is the first release whose cyextensions declare Py_MOD_GIL_NOT_USED. Below it, + # importing SQLAlchemy re-enables the GIL process-wide on a free-threaded interpreter. There is + # no marker for a free-threaded build, so ordinary 3.14 carries the same floor for a reason that + # only affects 3.14t. + "sqlalchemy[asyncio]>=2.0.54; python_version >= '3.14'", # Not imported here: faststream pulls pydantic through fast-depends[pydantic], which allows it # back to 1.x. FastStream's serializer calls model_dump, so publishing a model raises # AttributeError on pydantic 1 — v2 is a real requirement of ours, not a preference. The two