Skip to content
Merged
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
4 changes: 0 additions & 4 deletions .github/workflows/_checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 3 additions & 4 deletions docs/adr/0002-free-threading-is-compat-only.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
16 changes: 7 additions & 9 deletions docs/introduction/installation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 6 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading