Problem
Thread safety is a documented property of the breaker: a single threading.Lock
guards the two await-free critical sections in interlock/_engine.py:117 and is
released before the protected callable runs. Nothing in CI exercises that claim.
- The matrix in
.github/workflows/ci.yml runs 3.11–3.14, all GIL-enabled.
- The suite is parallelised by process (
pytest-xdist, -n auto), never by
thread, so no test drives one breaker from several threads at once.
- Free-threaded CPython is officially supported as of 3.14, and it is exactly
where a lock-scope assumption that happens to be protected by the GIL turns
into a real race: the sliding-window mutation, the snapshot() copy
(interlock/_engine.py:344), the auto-transition timer arm/cancel pair and
the coordinator lane hand-off.
A library placed on every dependency call is the last place a latent data race
should be discovered by a user.
Proposal
Two independent changes:
- Add
3.14t to the CI matrix. astral-sh/setup-uv resolves free-threaded
interpreters from the t suffix, so this is a matrix entry plus whatever
fallout it exposes. Keep it non-blocking (continue-on-error) only until it
is green — the point is to make it required.
- Add thread-concurrency tests with
pytest-run-parallel (dev dependency,
--parallel-threads=N), scoped to the paths where concurrent mutation is
possible rather than the whole suite: test_breaker.py, test_registry.py,
test_engine.py, test_coordination.py, test_observability.py.
Acceptance criteria
- CI runs the suite on a free-threaded 3.14 interpreter and the job is required.
- A marked subset runs each test from multiple threads and asserts the
invariants that must survive contention: window counts add up, snapshot()
never returns a torn view, HALF_OPEN probe caps are never exceeded, the
registry hands out exactly one breaker per name.
- The core stays dependency-free;
pytest-run-parallel is dev-only.
- Any race the new jobs expose is fixed or filed with a reproducer before the
job is made required.
- Document in
CONTRIBUTING.md how to run the free-threaded and threaded
variants locally.
Non-goals
- ThreadSanitizer or other native tooling.
- Making free-threaded builds a supported distribution target (no wheels, no
classifier changes) — this is about verifying an existing correctness claim.
- Performance comparisons between GIL and free-threaded runs; CodSpeed keeps
measuring the default interpreter.
Problem
Thread safety is a documented property of the breaker: a single
threading.Lockguards the two await-free critical sections in
interlock/_engine.py:117and isreleased before the protected callable runs. Nothing in CI exercises that claim.
.github/workflows/ci.ymlruns 3.11–3.14, all GIL-enabled.pytest-xdist,-n auto), never bythread, so no test drives one breaker from several threads at once.
where a lock-scope assumption that happens to be protected by the GIL turns
into a real race: the sliding-window mutation, the
snapshot()copy(
interlock/_engine.py:344), the auto-transition timer arm/cancel pair andthe coordinator lane hand-off.
A library placed on every dependency call is the last place a latent data race
should be discovered by a user.
Proposal
Two independent changes:
3.14tto the CI matrix.astral-sh/setup-uvresolves free-threadedinterpreters from the
tsuffix, so this is a matrix entry plus whateverfallout it exposes. Keep it non-blocking (
continue-on-error) only until itis green — the point is to make it required.
pytest-run-parallel(dev dependency,--parallel-threads=N), scoped to the paths where concurrent mutation ispossible rather than the whole suite:
test_breaker.py,test_registry.py,test_engine.py,test_coordination.py,test_observability.py.Acceptance criteria
invariants that must survive contention: window counts add up,
snapshot()never returns a torn view, HALF_OPEN probe caps are never exceeded, the
registry hands out exactly one breaker per name.
pytest-run-parallelis dev-only.job is made required.
CONTRIBUTING.mdhow to run the free-threaded and threadedvariants locally.
Non-goals
classifier changes) — this is about verifying an existing correctness claim.
measuring the default interpreter.