From 511d07564d3ca306190389e2e72917f4c4745753 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 18 Jul 2026 08:47:29 +0300 Subject: [PATCH 1/6] docs(planning): spec free-threaded (3.14t) compatibility Full-lane design change + a decision recording free-threading as a compatibility guarantee (runs on 3.14t, GIL off) rather than a parallelism redesign, and why 3.14t is the only target (deps ship cp314t wheels, no cp313t). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-07-18.01-free-threaded-support.md | 99 +++++++++++++++++++ ...026-07-18-free-threading-is-compat-only.md | 54 ++++++++++ 2 files changed, 153 insertions(+) create mode 100644 planning/changes/2026-07-18.01-free-threaded-support.md create mode 100644 planning/decisions/2026-07-18-free-threading-is-compat-only.md diff --git a/planning/changes/2026-07-18.01-free-threaded-support.md b/planning/changes/2026-07-18.01-free-threaded-support.md new file mode 100644 index 0000000..9061d2a --- /dev/null +++ b/planning/changes/2026-07-18.01-free-threaded-support.md @@ -0,0 +1,99 @@ +--- +summary: Guarantee free-threaded (3.14t) Python compatibility via a dedicated CI job, a "Free Threading :: 2 - Beta" classifier, and a docs note — no runtime change. +--- + +# Design: Free-threaded Python (3.14t) compatibility + +## Summary + +`faststream-outbox` is pure-Python asyncio with no `threading`, locks, or +C-extension code of its own, so it is *expected* to run on a free-threaded +(no-GIL) CPython — but nothing proves or advertises it. This change adds a +**compatibility guarantee**, not a behavior change: a dedicated CI job that runs +the full suite on **3.14t** with the GIL provably disabled, a `Free Threading :: +2 - Beta` trove classifier, and a short docs note. No source, no dependency +floors, no runtime semantics change. + +## Motivation + +CPython 3.14 free-threaded builds are on the GA track and users increasingly run +`python3.14t`. The package's own code has zero GIL-dependent surface (single +event loop, N worker *tasks*, not OS threads), so it should Just Work — but +"should" is not a guarantee, and a regression would be invisible. Two concrete +facts make the guarantee cheap and worth locking in now: + +- All three compiled deps in the graph — `asyncpg`, `sqlalchemy`, + `pydantic-core` (via faststream/fastapi) — ship `cp314t` free-threaded wheels + as of asyncpg 0.31 / sqlalchemy 2.0.51 / pydantic-core 2.47. So the **full** + integration suite (asyncpg against Postgres) can run under 3.14t today. +- Without a CI gate, a future dep bump that drops or lacks a `cp314t` wheel would + silently re-enable the GIL, and we would ship a false "free-threaded" claim. + +## Design + +**CI** — a new `freethreaded` job in `.github/workflows/_checks.yml`, mirroring +the `pytest` job (same Postgres 17 service block) but standalone (not in the +coverage matrix, no coverage upload): + +```yaml + freethreaded: + runs-on: ubuntu-latest + services: + postgres: { ... same as pytest ... } + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.2.0 + - run: uv python install 3.14t + - run: uv python pin 3.14t + - run: uv sync --all-extras --no-install-project + # Fail loudly if any imported extension silently re-enabled the GIL: + - run: uv run --no-sync python -c "import sys; assert not sys._is_gil_enabled()" + - run: uv run --no-sync pytest . --no-cov + env: { POSTGRES_DSN: postgresql+asyncpg://outbox:outbox@127.0.0.1:5432/outbox, ... } +``` + +The GIL-off assertion is load-bearing: it turns a silent GIL re-enable (the most +likely failure mode) into a red build. `--no-cov` overrides the repo's +`--cov-fail-under=100` addopts — the free-threaded run is a compat/GIL-off smoke, +and the four GIL builds already enforce 100% on identical source; running +coverage under free-threaded `sys.monitoring` only adds flakiness for zero new +signal. + +**Packaging** — add `"Programming Language :: Python :: Free Threading :: +2 - Beta"` to `pyproject.toml` classifiers. Conservative first claim; bump to +`3 - Stable` after real-world soak. No dependency-floor change: free-threaded +users' resolver picks the `cp314t` wheels automatically; GIL users are +unaffected. + +**Docs** — a short "Free-threaded Python" note in +`docs/introduction/installation.md`: supported and CI-proven on 3.14t with the +GIL disabled; it does **not** add cross-core parallelism (the subscriber is a +single event loop by design). + +Why 3.14t only, and why compat-only rather than a parallelism redesign → +[decision: free-threading-is-compat-only](../decisions/2026-07-18-free-threading-is-compat-only.md). + +## Non-goals + +- **No parallelism redesign.** The subscriber stays single-event-loop; this is + not a multi-thread/multi-loop rearchitecture. See the linked decision. +- **No 3.13t target** — the compiled deps ship no `cp313t` wheels. +- **No dependency-floor bumps** — wheel resolution handles it per-interpreter. + +## Testing + +- CI `freethreaded` job green: full suite passes on 3.14t and the + `sys._is_gil_enabled()` assertion holds. +- Fast local smoke (no Postgres): + `uv run --python 3.14t --no-sync pytest tests/test_unit.py tests/test_fake.py --no-cov`. +- `just check-planning` passes. + +## Risk + +- **Future dep drops/lacks a `cp314t` wheel** (medium likelihood, medium + impact): resolution fails or the GIL silently re-enables. Mitigated by the + GIL-off assertion + `--all-extras` install exercising the full graph. +- **`sys.monitoring` coverage flakiness** on free-threaded (cf. the py3.11 + trace-loss class): avoided entirely by `--no-cov` on this job. +- **3.14t availability in setup-uv** (low): uv already resolves and installs + free-threaded interpreters. diff --git a/planning/decisions/2026-07-18-free-threading-is-compat-only.md b/planning/decisions/2026-07-18-free-threading-is-compat-only.md new file mode 100644 index 0000000..98c8840 --- /dev/null +++ b/planning/decisions/2026-07-18-free-threading-is-compat-only.md @@ -0,0 +1,54 @@ +--- +status: accepted +summary: Free-threaded support is a compatibility guarantee (runs on 3.14t, GIL off), not a parallelism redesign; target 3.14t only. +supersedes: null +superseded_by: null +--- + +# Free-threading is a compatibility guarantee, not a parallelism redesign + +**Decision:** "Free-threaded support" means proving and advertising that +`faststream-outbox` runs correctly on a free-threaded CPython (3.14t) with the +GIL disabled — it does **not** mean rearchitecting the subscriber to use multiple +CPU cores. Target 3.14t only, not 3.13t. + +## Context + +Free-threaded CPython removes the GIL, so threaded CPU-bound Python can use +multiple cores. The question raised: should `faststream-outbox` "support nogil"? +Two readings were on the table: + +1. **Compatibility** — guarantee the package imports and runs green under a + free-threaded interpreter, and say so. +2. **Exploit parallelism** — redesign the subscriber so its worker loops run + across OS threads / multiple event loops to use multiple cores. + +## Decision & rationale + +We take reading (1) and explicitly reject (2) for now. + +- The package is pure-Python asyncio: one event loop, N worker *tasks* (not OS + threads), zero `threading`/lock/C-extension code. Free-threading changes none + of its runtime semantics; the win from (1) is a proven guarantee, achieved with + a CI job + classifier + docs and no source change. +- Reading (2) is a substantial rearchitecture of a deliberately single-loop + design (the two-loop subscriber, lease-token invariant, drain-on-stop all + assume one loop). Its payoff is also questionable: outbox throughput is + dominated by Postgres I/O and row-lease contention, not by in-process CPU, so + multi-core in-process fan-out is unlikely to be the bottleneck. Scaling today is + "run more subscriber processes," which already uses more cores. (2) carries + real invariant-breaking risk for an unproven gain — declined. +- **3.14t only, not 3.13t:** the compiled deps (`asyncpg`, `sqlalchemy`, + `pydantic-core`) ship `cp314t` wheels but no `cp313t` wheels, so 3.13t cannot + install the full graph. No point targeting an interpreter the dependencies + cannot support. + +Details of the shipped compat work → change +[2026-07-18.01-free-threaded-support](../changes/2026-07-18.01-free-threaded-support.md). + +## Revisit trigger + +Reopen (2) if a profiled workload shows the subscriber is **in-process CPU-bound** +(not Postgres/lease-bound) and running more processes is not an acceptable scale +lever. Reopen the 3.13t question if the compiled deps start publishing `cp313t` +wheels *and* 3.13t is still within the support window. From 2ee2aabc2e0cfe2fdf35cf13201bf05aaab5b418 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 18 Jul 2026 10:33:53 +0300 Subject: [PATCH 2/6] docs(planning): scope free-threaded guarantee around SQLAlchemy cyext Local proof on 3.14t showed SQLAlchemy's Cython extensions re-enable the GIL (they ship cp314t wheels but don't declare Py_MOD_GIL_NOT_USED). Scope the guarantee to DISABLE_SQLALCHEMY_CEXT_RUNTIME=1 (SQLAlchemy's sanctioned switch); record a decision with a revisit trigger. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-07-18.01-free-threaded-support.md | 60 ++++++++++++------- ...026-07-18-sqlalchemy-cyext-disables-gil.md | 57 ++++++++++++++++++ 2 files changed, 96 insertions(+), 21 deletions(-) create mode 100644 planning/decisions/2026-07-18-sqlalchemy-cyext-disables-gil.md diff --git a/planning/changes/2026-07-18.01-free-threaded-support.md b/planning/changes/2026-07-18.01-free-threaded-support.md index 9061d2a..b531934 100644 --- a/planning/changes/2026-07-18.01-free-threaded-support.md +++ b/planning/changes/2026-07-18.01-free-threaded-support.md @@ -1,5 +1,5 @@ --- -summary: Guarantee free-threaded (3.14t) Python compatibility via a dedicated CI job, a "Free Threading :: 2 - Beta" classifier, and a docs note — no runtime change. +summary: Guarantee free-threaded (3.14t) Python compatibility via a dedicated CI job (SQLAlchemy C-accel disabled so the GIL stays off), a "Free Threading :: 2 - Beta" classifier, and a docs note — no runtime change. --- # Design: Free-threaded Python (3.14t) compatibility @@ -11,8 +11,11 @@ C-extension code of its own, so it is *expected* to run on a free-threaded (no-GIL) CPython — but nothing proves or advertises it. This change adds a **compatibility guarantee**, not a behavior change: a dedicated CI job that runs the full suite on **3.14t** with the GIL provably disabled, a `Free Threading :: -2 - Beta` trove classifier, and a short docs note. No source, no dependency -floors, no runtime semantics change. +2 - Beta` trove classifier, and a short docs note. Keeping the GIL genuinely +disabled requires disabling SQLAlchemy's Cython extensions +(`DISABLE_SQLALCHEMY_CEXT_RUNTIME=1`, its own sanctioned switch) — see the +[SQLAlchemy-cyext decision](../decisions/2026-07-18-sqlalchemy-cyext-disables-gil.md). +No source, no dependency floors, no runtime semantics change. ## Motivation @@ -20,24 +23,31 @@ CPython 3.14 free-threaded builds are on the GA track and users increasingly run `python3.14t`. The package's own code has zero GIL-dependent surface (single event loop, N worker *tasks*, not OS threads), so it should Just Work — but "should" is not a guarantee, and a regression would be invisible. Two concrete -facts make the guarantee cheap and worth locking in now: - -- All three compiled deps in the graph — `asyncpg`, `sqlalchemy`, - `pydantic-core` (via faststream/fastapi) — ship `cp314t` free-threaded wheels - as of asyncpg 0.31 / sqlalchemy 2.0.51 / pydantic-core 2.47. So the **full** - integration suite (asyncpg against Postgres) can run under 3.14t today. -- Without a CI gate, a future dep bump that drops or lacks a `cp314t` wheel would - silently re-enable the GIL, and we would ship a false "free-threaded" claim. +facts shape the design: + +- The compiled deps — `asyncpg`, `pydantic-core`, `sqlalchemy` — install + `cp314t` free-threaded wheels, so the **full** integration suite (asyncpg + against Postgres) runs under 3.14t today. `asyncpg` and `pydantic-core` + declare free-thread safety; **SQLAlchemy's Cython extensions do not**, so + importing `sqlalchemy` silently re-enables the GIL process-wide (a `cp314t` + wheel is necessary but not sufficient). We scope around this with SQLAlchemy's + documented `DISABLE_SQLALCHEMY_CEXT_RUNTIME` switch (pure-Python fallback). +- Without a CI gate, a future dep bump that drops or lacks a `cp314t` wheel — or + another extension that fails to declare free-thread safety — would silently + re-enable the GIL, and we would ship a false "free-threaded" claim. ## Design **CI** — a new `freethreaded` job in `.github/workflows/_checks.yml`, mirroring the `pytest` job (same Postgres 17 service block) but standalone (not in the -coverage matrix, no coverage upload): +coverage matrix, no coverage upload), with `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` +set job-wide: ```yaml freethreaded: runs-on: ubuntu-latest + env: + DISABLE_SQLALCHEMY_CEXT_RUNTIME: 1 # SQLAlchemy cyext re-enables the GIL; use its pure-Python fallback services: postgres: { ... same as pytest ... } steps: @@ -47,7 +57,7 @@ coverage matrix, no coverage upload): - run: uv python pin 3.14t - run: uv sync --all-extras --no-install-project # Fail loudly if any imported extension silently re-enabled the GIL: - - run: uv run --no-sync python -c "import sys; assert not sys._is_gil_enabled()" + - run: uv run --no-sync python -c "import sys, asyncpg, sqlalchemy, pydantic_core, faststream, faststream_outbox; assert not sys._is_gil_enabled()" - run: uv run --no-sync pytest . --no-cov env: { POSTGRES_DSN: postgresql+asyncpg://outbox:outbox@127.0.0.1:5432/outbox, ... } ``` @@ -66,12 +76,17 @@ users' resolver picks the `cp314t` wheels automatically; GIL users are unaffected. **Docs** — a short "Free-threaded Python" note in -`docs/introduction/installation.md`: supported and CI-proven on 3.14t with the -GIL disabled; it does **not** add cross-core parallelism (the subscriber is a -single event loop by design). +`docs/introduction/installation.md`: supported and CI-proven on 3.14t; the +library itself is GIL-independent; **to keep the GIL genuinely disabled, set +`DISABLE_SQLALCHEMY_CEXT_RUNTIME=1`** (otherwise SQLAlchemy's C extensions +re-enable it process-wide — correctness is unaffected either way). It does +**not** add cross-core parallelism (the subscriber is a single event loop by +design). Why 3.14t only, and why compat-only rather than a parallelism redesign → [decision: free-threading-is-compat-only](../decisions/2026-07-18-free-threading-is-compat-only.md). +Why SQLAlchemy's C extensions are disabled → +[decision: sqlalchemy-cyext-disables-gil](../decisions/2026-07-18-sqlalchemy-cyext-disables-gil.md). ## Non-goals @@ -79,20 +94,23 @@ Why 3.14t only, and why compat-only rather than a parallelism redesign → not a multi-thread/multi-loop rearchitecture. See the linked decision. - **No 3.13t target** — the compiled deps ship no `cp313t` wheels. - **No dependency-floor bumps** — wheel resolution handles it per-interpreter. +- **No attempt to force SQLAlchemy's C extensions to run GIL-free** — that is + upstream's to declare; we disable them and revisit when they do. ## Testing - CI `freethreaded` job green: full suite passes on 3.14t and the - `sys._is_gil_enabled()` assertion holds. + `sys._is_gil_enabled()` assertion holds (with `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1`). - Fast local smoke (no Postgres): - `uv run --python 3.14t --no-sync pytest tests/test_unit.py tests/test_fake.py --no-cov`. + `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1 uv run --python 3.14t --no-sync pytest tests/test_unit.py tests/test_fake.py --no-cov`. - `just check-planning` passes. ## Risk -- **Future dep drops/lacks a `cp314t` wheel** (medium likelihood, medium - impact): resolution fails or the GIL silently re-enables. Mitigated by the - GIL-off assertion + `--all-extras` install exercising the full graph. +- **SQLAlchemy (or another dep) re-enables the GIL** (confirmed for SQLAlchemy's + cyext; medium likelihood for future deps): mitigated by + `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` today and the GIL-off assertion, which + fails the job loudly on any new offender. - **`sys.monitoring` coverage flakiness** on free-threaded (cf. the py3.11 trace-loss class): avoided entirely by `--no-cov` on this job. - **3.14t availability in setup-uv** (low): uv already resolves and installs diff --git a/planning/decisions/2026-07-18-sqlalchemy-cyext-disables-gil.md b/planning/decisions/2026-07-18-sqlalchemy-cyext-disables-gil.md new file mode 100644 index 0000000..6e26519 --- /dev/null +++ b/planning/decisions/2026-07-18-sqlalchemy-cyext-disables-gil.md @@ -0,0 +1,57 @@ +--- +status: accepted +summary: The free-threaded CI job sets DISABLE_SQLALCHEMY_CEXT_RUNTIME=1 because SQLAlchemy's Cython extensions re-enable the GIL on 3.14t; docs tell users to do the same for a genuinely GIL-free process. +supersedes: null +superseded_by: null +--- + +# SQLAlchemy C extensions are disabled to keep the GIL off on 3.14t + +**Decision:** Scope the free-threaded (3.14t) guarantee to "runs with the GIL +genuinely disabled *when SQLAlchemy's Cython extensions are off*." The +`freethreaded` CI job sets `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` (SQLAlchemy's own +sanctioned switch) and asserts `sys._is_gil_enabled() is False`; the docs tell +users to set the same env var for a truly GIL-free process. + +## Context + +Adding the free-threaded compat guarantee (change +[2026-07-18.01](../changes/2026-07-18.01-free-threaded-support.md)), the local +proof on 3.14t found that `import sqlalchemy` **silently re-enables the GIL**. +SQLAlchemy 2.0.50 ships `cp314t` wheels, but its Cython extensions +(`sqlalchemy.cyextension.*`) do not declare free-thread safety (no +`Py_MOD_GIL_NOT_USED` slot), so CPython force-re-enables the GIL process-wide on +import. `asyncpg` and `pydantic-core` are unaffected — SQLAlchemy is the sole +offender. A `cp314t` wheel is therefore necessary but not sufficient for a +GIL-free run. + +Options weighed: (1) set `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` and keep the +GIL-off assertion; (2) drop the assertion and make no GIL claim (compat only); +(3) defer the whole change until SQLAlchemy ships a free-threading-safe +cyextension. + +## Decision & rationale + +Take (1). The library runs *correctly* on 3.14t regardless of the GIL, but a +genuinely-disabled GIL matters for a user who runs other threaded code in the +same process — SQLAlchemy's cyext would otherwise re-enable the GIL +process-wide and kill their parallelism. `DISABLE_SQLALCHEMY_CEXT_RUNTIME` is +SQLAlchemy's documented, supported switch (pure-Python fallback of the same +behavior, only slower), not a hack, and it is fully under our control — so +there is no "wait for upstream" gap that would justify (3). Dropping the +assertion (2) discards the regression guard that caught this in the first +place. The cost of (1) is one CI env var plus a one-line docs caveat, and it is +trivially reversible when upstream fixes the extensions. + +Rejected (3) — defer: the guarantee is already true today (the suite passes on +3.14t), the workaround is sanctioned and stable, the upstream timeline is +open-ended, and withholding classifier/docs leaves 3.14t adopters with no +signal. Deferring banks nothing and is strictly worse for users than shipping +with the documented caveat. + +## Revisit trigger + +Drop `DISABLE_SQLALCHEMY_CEXT_RUNTIME=1` (from CI and the docs caveat) once +SQLAlchemy ships Cython extensions that declare `Py_MOD_GIL_NOT_USED` and the +GIL stays disabled on 3.14t with C-accel on. Re-run the change's Step-3 GIL +assertion without the env var to confirm before removing it. From 00ba464515bb5e657e8ed9af32e8594eaae52c86 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 18 Jul 2026 10:37:38 +0300 Subject: [PATCH 3/6] ci: run the suite on free-threaded Python 3.14t Adds a standalone `freethreaded` job that installs 3.14t, runs the full suite (--no-cov), and asserts sys._is_gil_enabled() is False so a dependency that silently re-enables the GIL fails the build. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/_checks.yml | 37 +++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/.github/workflows/_checks.yml b/.github/workflows/_checks.yml index 12a3f71..7cf9b88 100644 --- a/.github/workflows/_checks.yml +++ b/.github/workflows/_checks.yml @@ -64,3 +64,40 @@ jobs: PYTHONDONTWRITEBYTECODE: 1 PYTHONUNBUFFERED: 1 POSTGRES_DSN: postgresql+asyncpg://outbox:outbox@127.0.0.1:5432/outbox + + 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 + env: + POSTGRES_USER: outbox + POSTGRES_PASSWORD: outbox + POSTGRES_DB: outbox + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U outbox" + --health-interval 10s + --health-timeout 5s + --health-retries 5 + steps: + - uses: actions/checkout@v6 + - uses: astral-sh/setup-uv@v8.2.0 + with: + enable-cache: true + cache-dependency-glob: "**/pyproject.toml" + - run: uv python install 3.14t + - run: uv python pin 3.14t + - run: uv sync --all-extras --no-install-project + - name: Assert the GIL is actually disabled + run: uv run --no-sync python -c "import sys, asyncpg, sqlalchemy, pydantic_core, faststream, faststream_outbox; assert not sys._is_gil_enabled(), 'GIL re-enabled - a dependency lacks free-threaded support'" + - run: uv run --no-sync pytest . --no-cov + env: + PYTHONDONTWRITEBYTECODE: 1 + PYTHONUNBUFFERED: 1 + POSTGRES_DSN: postgresql+asyncpg://outbox:outbox@127.0.0.1:5432/outbox From ca89e6020ac0e638b85fa150c22f5f5571f1c0a2 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 18 Jul 2026 10:43:46 +0300 Subject: [PATCH 4/6] chore(packaging): claim Free Threading :: 2 - Beta classifier Advertises the CI-proven 3.14t free-threaded compatibility on PyPI. Conservative first claim; bump to 3 - Stable after real-world soak. Co-Authored-By: Claude Opus 4.8 (1M context) --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index a453cea..0ef69cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,6 +16,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: Free Threading :: 2 - Beta", "Typing :: Typed", "Topic :: Software Development :: Libraries", ] From 64e9e24a98aa2761796556a58cac0ca081fc4097 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 18 Jul 2026 10:47:22 +0300 Subject: [PATCH 5/6] docs(installation): document free-threaded (3.14t) support States what is guaranteed (runs on 3.14t, GIL off) and what is not (no extra in-process parallelism; the subscriber stays single-loop). Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/introduction/installation.md | 32 +++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/docs/introduction/installation.md b/docs/introduction/installation.md index 5409ad7..8d4eda3 100644 --- a/docs/introduction/installation.md +++ b/docs/introduction/installation.md @@ -29,6 +29,38 @@ you're starting fresh. - A running Postgres instance accessible via SQLAlchemy `AsyncEngine` +## Free-threaded Python + +`faststream-outbox` supports free-threaded (no-GIL) CPython and is tested on +**3.14t** in CI, with the GIL asserted disabled. The package is pure-Python +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`" + + 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. + + The same caveat 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 + correctly. + +What this does **not** change: the subscriber runs a single event loop by +design, so free-threading does not add cross-core parallelism within one +process. To use more cores, run more subscriber processes — the same scaling +lever as on a GIL build. + +Free-threaded wheels for the compiled dependencies currently exist for 3.14t +only (there are no `cp313t` wheels), so 3.13t is not a supported target. + ## Postgres If you don't have a Postgres instance, you can start one with Docker: From 5fdf7c8e33b296bbba7fd54ef2b2a47298cf4e27 Mon Sep 17 00:00:00 2001 From: Artur Shiriev Date: Sat, 18 Jul 2026 10:56:38 +0300 Subject: [PATCH 6/6] docs: phrase the free-threaded guarantee as two distinct CI facts The suite's pytest step runs GIL-on (test tooling imports aiokafka at collection, which re-enables the GIL); only the standalone assertion step proves the outbox's runtime dependency graph keeps the GIL disabled. Word the docs/spec to reflect both facts instead of implying the suite runs GIL-off. Co-Authored-By: Claude Opus 4.8 (1M context) --- docs/introduction/installation.md | 6 ++++-- planning/changes/2026-07-18.01-free-threaded-support.md | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/introduction/installation.md b/docs/introduction/installation.md index 8d4eda3..f053e6f 100644 --- a/docs/introduction/installation.md +++ b/docs/introduction/installation.md @@ -31,8 +31,10 @@ ## Free-threaded Python -`faststream-outbox` supports free-threaded (no-GIL) CPython and is tested on -**3.14t** in CI, with the GIL asserted disabled. The package is pure-Python +`faststream-outbox` supports free-threaded (no-GIL) CPython. CI runs the full +test suite on the **3.14t** interpreter, and a separate CI step asserts that +importing the outbox and its runtime dependencies keeps the GIL disabled. The +package is pure-Python 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. diff --git a/planning/changes/2026-07-18.01-free-threaded-support.md b/planning/changes/2026-07-18.01-free-threaded-support.md index b531934..443f305 100644 --- a/planning/changes/2026-07-18.01-free-threaded-support.md +++ b/planning/changes/2026-07-18.01-free-threaded-support.md @@ -10,7 +10,7 @@ summary: Guarantee free-threaded (3.14t) Python compatibility via a dedicated CI C-extension code of its own, so it is *expected* to run on a free-threaded (no-GIL) CPython — but nothing proves or advertises it. This change adds a **compatibility guarantee**, not a behavior change: a dedicated CI job that runs -the full suite on **3.14t** with the GIL provably disabled, a `Free Threading :: +the full suite on **3.14t** and, in a separate step, asserts the outbox's runtime dependency graph keeps the GIL disabled, a `Free Threading :: 2 - Beta` trove classifier, and a short docs note. Keeping the GIL genuinely disabled requires disabling SQLAlchemy's Cython extensions (`DISABLE_SQLALCHEMY_CEXT_RUNTIME=1`, its own sanctioned switch) — see the