feat(concurrency): free-threaded CPython support — memory-ordering fixes, gil_used=false, CI lane (LAB-511) - #265
feat(concurrency): free-threaded CPython support — memory-ordering fixes, gil_used=false, CI lane (LAB-511)#26527Bslash6 wants to merge 2 commits into
Conversation
…xes, gil_used=false, CI lane (LAB-511) Make cachekit-py provably race-free under free-threaded CPython and gate regressions in CI: - decorators/session.py: the lock-free fast path and in-lock double-check now gate on every published field (_session_start_ms included). Assignment order only guaranteed visibility order under the GIL; a GIL-free reader observing pid+id before start_ms hit the 'should never happen' RuntimeError and silently dropped session headers (the LAB-506 telemetry loss, resurfacing GIL-free). Regression tests pin the mid-publish state deterministically and fail pre-fix. - reliability/metrics_collection.py: AsyncMetricsCollector.flush polled Queue.empty(), which flips at dequeue — before processing finishes. Waits on unfinished_tasks now. Was a routine flake on the free-threaded lane. - rust/src/lib.rs: #[pymodule(gil_used = false)] — the PyO3 0.28+ default made explicit, justified by the LAB-511 audit (AtomicU64 nonce, Mutex metrics, &self-only pyclasses, Send+Sync compiler-enforced). - CI: new test-freethreaded job runs unit+critical on 3.14t, asserts the GIL stays disabled after importing cachekit (hiredis excluded — no Py_mod_gil declaration; redis-py falls back to pure-Python parser). - pyproject: dependency-groups split into test (free-threading-compatible core toolchain) + dev (includes test; adds the extras without free-threaded wheels: orjson, numpy, pandas, pyarrow). - tests: importorskip guards so the core suites honestly run without the [data]/[json] extras; full audit table in docs/free-threading.md. Free-threaded wheels/classifiers explicitly deferred: orjson (build rejects free-threaded), hiredis (no Py_mod_gil), numpy/pandas/pyarrow coverage.
…p encryption/protocol suites (LAB-511) Panel findings applied: - tests/conftest.py: autouse session-scoped fixture fails the run if the GIL got re-enabled on a free-threaded build — runs in EVERY xdist worker and both suites, covering lazily-imported extensions the single-process CI pre-flight and one-worker in-suite check missed (bug-hunter MAJ). - test_encryption_security_invariants.py / test_serializer_protocol.py: module-level importorskip narrowed to the 2+1 tests that actually need orjson/pyarrow — the encryption invariants and protocol-compliance suites now run on the free-threaded lane (craftsman MAJ x2; +35 tests). - README/docs: support claim narrowed to 3.14t — the only build the lane runs (craftsman MIN). Rejected: cutting test_session_headers_present_mid_publish as duplicate — it pins the get_session_headers fallback branch; the end-to-end test pins the info.session_id branch. Distinct paths, both stay.
|
Warning Review limit reachedNext included review available in 28 minutes. View limit detailsLimit details: You’ve used all 6 included reviews currently available. Your 40 included PR review attempts over the past 7 days set your current allowance at 6 reviews per hour. Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available. Review configuration: ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (28)
Comment |
Codecov Report❌ Patch coverage is
📢 Thoughts on this report? Let us know! |
Closes LAB-511.
Makes cachekit-py provably race-free under free-threaded CPython, with a CI lane that gates regressions. No wire/format change; crypto surfaces untouched.
The defect (LAB-506 panel, comment b80e0145)
decorators/session.py::_ensure_session_initializedpublished three module globals and relied on assignment order for its lock-free fast path — a guarantee only the GIL provides. A GIL-free reader observingpid+idbefore_session_start_mssailed intoget_session_start_ms()'s "should never happen"RuntimeError, whichbackend.pycatches and turns into silently dropped session headers — the exact telemetry loss LAB-506 eliminated, resurfacing GIL-free. The fast path and in-lock double-check now gate on every published field. Regression tests pin the mid-publish state deterministically (fail pre-fix, pass post-fix) and an 8-thread hammer races first-touch init for real on the free-threaded lane.What the free-threaded lane surfaced
Running the suites on 3.14t found a second real bug:
AsyncMetricsCollector.flush()polledQueue.empty(), which flips at dequeue — before processing finishes. Routine flake under true concurrency, invisible under the GIL. Now waits onunfinished_tasks(zeroed bytask_done()after processing).Changes
decorators/session.pyfull-field gate; regression tests intest_saas_observability.py::TestMidPublishMemoryOrdering+test_free_threading.py.docs/free-threading.md(stats registry + fork handlers,_FunctionStats, session identity, header cache, L1/L2 SWR single-flight,ObjectCache,_cached_keys, metrics singleton, Rust extension).#[pymodule(gil_used = false)](the 0.28+ default made explicit); justified by audit:&self-only pyclasses,AtomicU64nonce,Mutexmetrics,Send + Synccompiler-enforced.test-freethreadedjob:uv sync --python 3.14t --no-default-groups --group test --no-install-package hiredis(hiredis has noPy_mod_gildeclaration; redis-py falls back to its pure-Python parser), asserts the GIL stays disabled after importing cachekit, runs unit + critical. Wired intoci-success. An autouse session fixture re-asserts GIL state at teardown in every xdist worker.test(free-threading-compatible core toolchain) split out ofdev(which includes it viainclude-group);devresolves identically to before.pytest.importorskipso the core suites honestly run without the[data]/[json]extras; narrowed to per-test scope where a module-level skip would have dropped extra-free coverage (encryption invariants, protocol compliance).docs/free-threading.md(support status, audit table, deferral), README thread-safety section.Deferred (per acceptance criteria, explicitly)
Free-threaded wheels / declared support blocked on upstream: orjson (build script rejects free-threaded interpreters, no FT wheels through 3.12.0), hiredis (no
Py_mod_gil), numpy/pandas/pyarrow ([data]extra coverage incomplete). When they clear: add-i python3.14tto thebuild-wheelsmatrix.Verification