|
| 1 | +--- |
| 2 | +summary: Certified httpware under free-threaded CPython (PEP 703) — a `pytest-freethreaded` CI job on 3.14t, real-thread stress tests for RetryBudget, CircuitBreaker, Bulkhead, and the httpx2 shared-pool boundary, plus a deterministic event-loop-guard cross-loop test, a committed contention benchmark (nogil ~1.9x slower for the single-shared-lock workload — correctness, not speedup), and the `Free Threading :: 2 - Beta` classifier shipped last. 3.13t deferred on the msgspec cp313t wheel gap. |
| 3 | +--- |
| 4 | + |
| 5 | +# Design: Free-threading (nogil) support |
| 6 | + |
| 7 | +## Summary |
| 8 | + |
| 9 | +Certify that httpware runs correctly under free-threaded CPython (PEP 703), |
| 10 | +backed by evidence rather than a bare classifier. Adds a free-threaded CI job |
| 11 | +(3.14t, full suite + all extras), parallel stress tests for the Lock-based |
| 12 | +resilience components and the shared httpx2 connection pool, a committed |
| 13 | +contention benchmark with a recorded baseline, and — last, once everything is |
| 14 | +green — the `Programming Language :: Python :: Free Threading` classifier. |
| 15 | +Verify-first: the public claim trails the proof. |
| 16 | + |
| 17 | +## Motivation |
| 18 | + |
| 19 | +httpware is a concurrency library: its resilience suite (`RetryBudget`, |
| 20 | +`CircuitBreaker`, `Bulkhead`) is built on `threading.Lock`/`Semaphore` and |
| 21 | +documents thread-safety invariants, and a shared client across threads is a |
| 22 | +documented usage pattern. Free-threaded CPython (free-threaded builds available |
| 23 | +since 3.13, officially supported in 3.14) removes the GIL that currently masks |
| 24 | +any latent races. The library already ships 3.13/3.14 classifiers and tests |
| 25 | +those *GIL* builds, but nothing exercises true thread parallelism. |
| 26 | + |
| 27 | +Two empirical findings (2026-07-18) de-risk the work: |
| 28 | + |
| 29 | +- **httpx2 2.5.0 holds under free-threading.** 32 threads sharing one client + |
| 30 | + connection pool, 3 × 12,800 requests on 3.14t with the GIL disabled, each |
| 31 | + response verified against its request: zero cross-talk, zero crashes. |
| 32 | +- **Extras wheel matrix on free-threaded interpreters.** pydantic ✓ (3.13t + |
| 33 | + 3.14t), msgspec ✓ 3.14t but ✗ no cp313t wheel (0.18–0.21.1), otel pure-Python |
| 34 | + ✓. **3.14t is the only interpreter with complete extras coverage from |
| 35 | + prebuilt wheels;** all three keep the GIL disabled after import. |
| 36 | + |
| 37 | +## Design |
| 38 | + |
| 39 | +Five workstreams, landed verify-first (classifier last): |
| 40 | + |
| 41 | +1. **CI enablement.** Add one free-threaded job on `3.14t` to |
| 42 | + `.github/workflows/_checks.yml`, running the full suite + all extras (uv |
| 43 | + installs `3.14t`; extras resolve from prebuilt free-threaded wheels). The |
| 44 | + existing GIL matrix (3.11–3.14) is unchanged. 3.13t-free-threaded is deferred |
| 45 | + until msgspec ships a cp313t wheel (record in `deferred.md`). |
| 46 | +2. **Free-threaded audit + stress tests.** Audit httpware's own lock usage under |
| 47 | + parallelism, then add stress tests (real threads via `ThreadPoolExecutor`, |
| 48 | + shared instances, high iteration counts, invariant assertions) for |
| 49 | + `RetryBudget` token accounting, `CircuitBreaker` transitions, `Bulkhead` |
| 50 | + semaphore counting, and the event-loop guard's cross-loop rejection. TDD per |
| 51 | + component. **Specific target:** `_event_loop_guard.py`'s `# pragma: no cover` |
| 52 | + double-checked-locking arm is annotated "single-threaded tests can't |
| 53 | + trigger" — free-threading makes it reachable, so the pragma is re-examined |
| 54 | + (covered → removed, or reworded). |
| 55 | +3. **httpx2 boundary test.** Promote the shared-pool cross-talk stress harness |
| 56 | + into the suite (marked) as living regression evidence for the one dependency |
| 57 | + httpware can't self-certify. |
| 58 | +4. **Benchmarks.** A committed `benchmarks/` script measuring contended |
| 59 | + throughput (GIL vs free-threaded) on the shared Lock components — does nogil |
| 60 | + help, or does lock contention eat the gain? Baseline numbers recorded under |
| 61 | + `planning/audits/`. |
| 62 | +5. **Certify + promote.** Add the `Free Threading` classifier; promote into |
| 63 | + `architecture/resilience.md` (thread-safety prose), `architecture/testing.md` |
| 64 | + (stress-test convention + marker), `architecture/overview.md`; release notes. |
| 65 | + |
| 66 | +Stress tests carry a `stress` pytest marker so the free-threaded job runs them; |
| 67 | +they exercise concurrency under the GIL too but only *prove* safety on 3.14t. |
| 68 | + |
| 69 | +## Non-goals |
| 70 | + |
| 71 | +- **3.13t support** — blocked on a msgspec cp313t wheel; deferred, not designed out. |
| 72 | +- **Certifying httpx2 itself** — out of our control; we ship evidence, optionally upstream later. |
| 73 | +- **CI perf regression gates** — benchmarks are characterization (committed script + recorded baseline), not a gate; shared-runner perf is too noisy. |
| 74 | +- **Reworking the Lock-based design** — the audit validates it; it does not rewrite it. |
| 75 | + |
| 76 | +## Testing |
| 77 | + |
| 78 | +- Full suite green under `uv run --python 3.14t` locally and in the new CI job, |
| 79 | + GIL disabled (the `pytest-freethreaded` job asserts `sys._is_gil_enabled() is |
| 80 | + False` in a shell step before running the suite). |
| 81 | +- `pytest -m stress` on 3.14t: RetryBudget / breaker / bulkhead invariants hold |
| 82 | + across many-thread runs, and the httpx2 boundary test shows no cross-talk; a |
| 83 | + separate deterministic test covers the event-loop guard's cross-loop rejection. |
| 84 | +- `just lint-ci` and the planning validator unchanged and green. |
| 85 | + |
| 86 | +## Risk |
| 87 | + |
| 88 | +- **msgspec 3.13t wheel gap (med likelihood / low impact).** Scopes free-threaded |
| 89 | + CI to 3.14t only. Mitigation: documented deferral; revisit on wheel availability. |
| 90 | +- **Flaky stress tests (med / med).** Probabilistic race detection. Mitigation: |
| 91 | + invariant assertions (not interleaving-dependent), tuned iteration counts, the |
| 92 | + `stress` marker isolates them from the default run. |
| 93 | +- **Audit surfaces a real race (low / high).** The 2026-06-14 deep audit already |
| 94 | + reasoned the loop-guard fast path is benign; a real race would be a genuine bug |
| 95 | + caught *before* shipping the claim — which is the point of verify-first. |
0 commit comments