156 lines
6.5 KiB
Markdown
156 lines
6.5 KiB
Markdown
|
|
---
|
||
|
|
model: claude-opus-4-7[1m]
|
||
|
|
service: claude
|
||
|
|
session: subints-phase-b-hardening-and-fork-block
|
||
|
|
timestamp: 2026-04-22T20:07:23Z
|
||
|
|
git_ref: 797f57c
|
||
|
|
scope: code
|
||
|
|
substantive: true
|
||
|
|
raw_file: 20260422T200723Z_797f57c_prompt_io.raw.md
|
||
|
|
---
|
||
|
|
|
||
|
|
## Prompt
|
||
|
|
|
||
|
|
Session-spanning work on the Phase B `subint` spawn-backend.
|
||
|
|
Three distinct sub-phases in one log:
|
||
|
|
|
||
|
|
1. **Py3.13 gate tightening** — diagnose a reproducible hang
|
||
|
|
of subint spawn flow under py3.13 (works on py3.14), trace
|
||
|
|
to a private `_interpreters` module vintage issue, tighten
|
||
|
|
our feature gate from "`_interpreters` present" to "public
|
||
|
|
`concurrent.interpreters` present" (i.e. py3.14+).
|
||
|
|
2. **Test-harness hardening** — add `pytest-timeout` dep, put
|
||
|
|
`@pytest.mark.timeout(30, method='thread')` on the
|
||
|
|
three known-hanging subint tests cataloged in
|
||
|
|
`ai/conc-anal/subint_sigint_starvation_issue.md`. Separately,
|
||
|
|
code-review the user's in-flight `skipon_spawn_backend`
|
||
|
|
marker implementation; find four bugs; refactor to use
|
||
|
|
`item.iter_markers()`.
|
||
|
|
3. **`subint_fork` prototype → CPython-block finding** — draft
|
||
|
|
a WIP `subint_fork_proc` backend using a sub-interpreter as
|
||
|
|
a launchpad for `os.fork()` (to sidestep trio#1614). User
|
||
|
|
tests on py3.14, hits
|
||
|
|
`Fatal Python error: _PyInterpreterState_DeleteExceptMain:
|
||
|
|
not main interpreter`. Walk CPython sources (local clone at
|
||
|
|
`~/repos/cpython/`) to pinpoint the refusal
|
||
|
|
(`Modules/posixmodule.c:728` → `Python/pystate.c:1040`).
|
||
|
|
Revert implementation to a `NotImplementedError` stub in a
|
||
|
|
new `_subint_fork.py` submodule, document the finding in a
|
||
|
|
third `conc-anal/` doc with an upstream-report draft for
|
||
|
|
the CPython issue tracker. Finally, discuss user's proposed
|
||
|
|
workaround architecture (main-interp worker-thread
|
||
|
|
forkserver) and draft a standalone smoke-test script for
|
||
|
|
feasibility validation.
|
||
|
|
|
||
|
|
## Response summary
|
||
|
|
|
||
|
|
All three sub-phases landed concrete artifacts:
|
||
|
|
|
||
|
|
**Sub-phase 1** — `_subint.py` + `_spawn.py` gates + error
|
||
|
|
messages updated to require py3.14+ via the public
|
||
|
|
`concurrent.interpreters` module presence check. Module
|
||
|
|
docstring revised to explain the empirical reason
|
||
|
|
(py3.13's private `_interpreters` vintage wedges under
|
||
|
|
multi-trio-task usage even though minimal standalone
|
||
|
|
reproducers work fine there). Test-module
|
||
|
|
`pytest.importorskip` likewise switched.
|
||
|
|
|
||
|
|
**Sub-phase 2** — `pytest-timeout>=2.3` added to `testing`
|
||
|
|
dep group. `@pytest.mark.timeout(30, method='thread')`
|
||
|
|
applied on:
|
||
|
|
- `tests/discovery/test_registrar.py::test_stale_entry_is_deleted`
|
||
|
|
- `tests/test_cancellation.py::test_cancel_while_childs_child_in_sync_sleep`
|
||
|
|
- `tests/test_cancellation.py::test_multierror_fast_nursery`
|
||
|
|
- `tests/test_subint_cancellation.py::test_subint_non_checkpointing_child`
|
||
|
|
|
||
|
|
`method='thread'` documented inline as load-bearing — the
|
||
|
|
GIL-starvation path that drops `SIGINT` would equally drop
|
||
|
|
`SIGALRM`, so only a watchdog-thread timeout can reliably
|
||
|
|
escape.
|
||
|
|
|
||
|
|
`skipon_spawn_backend` plugin refactored into a single
|
||
|
|
`iter_markers`-driven loop in `pytest_collection_modifyitems`
|
||
|
|
(~30 LOC replacing ~30 LOC of nested conditionals). Four
|
||
|
|
bugs dissolved: wrong `.get()` key, module-level `pytestmark`
|
||
|
|
suppressing per-test marks, unhandled `pytestmark = [list]`
|
||
|
|
form, `pytest.Makr` typo. Marker help text updated to
|
||
|
|
document the variadic backend-list + `reason=` kwarg
|
||
|
|
surface.
|
||
|
|
|
||
|
|
**Sub-phase 3** — Prototype drafted (then reverted):
|
||
|
|
|
||
|
|
- `tractor/spawn/_subint_fork.py` — new dedicated submodule
|
||
|
|
housing the `subint_fork_proc` stub. Module docstring +
|
||
|
|
fn docstring explain the attempt, the CPython-level
|
||
|
|
block, and the reason for keeping the stub in-tree
|
||
|
|
(documentation of the attempt + starting point if CPython
|
||
|
|
ever lifts the restriction).
|
||
|
|
- `tractor/spawn/_spawn.py` — `'subint_fork'` registered as a
|
||
|
|
`SpawnMethodKey` literal + in `_methods`, so
|
||
|
|
`--spawn-backend=subint_fork` routes to a clean
|
||
|
|
`NotImplementedError` pointing at the analysis doc rather
|
||
|
|
than an "invalid backend" error.
|
||
|
|
- `ai/conc-anal/subint_fork_blocked_by_cpython_post_fork_issue.md` —
|
||
|
|
third sibling conc-anal doc. Full annotated CPython
|
||
|
|
source walkthrough from user-visible
|
||
|
|
`Fatal Python error` → `Modules/posixmodule.c:728
|
||
|
|
PyOS_AfterFork_Child()` → `Python/pystate.c:1040
|
||
|
|
_PyInterpreterState_DeleteExceptMain()` gate. Includes a
|
||
|
|
copy-paste-ready upstream-report draft for the CPython
|
||
|
|
issue tracker with a two-tier ask (ideally "make it work",
|
||
|
|
minimally "cleaner error than `Fatal Python error`
|
||
|
|
aborting the child").
|
||
|
|
- `ai/conc-anal/subint_fork_from_main_thread_smoketest.py` —
|
||
|
|
standalone zero-tractor-import CPython-level smoke test
|
||
|
|
for the user's proposed workaround architecture
|
||
|
|
(forkserver on a main-interp worker thread). Four
|
||
|
|
argparse-driven scenarios: `control_subint_thread_fork`
|
||
|
|
(reproduces the known-broken case as a test-harness
|
||
|
|
sanity), `main_thread_fork` (baseline), `worker_thread_fork`
|
||
|
|
(architectural assertion), `full_architecture`
|
||
|
|
(end-to-end trio-in-subint in forked child). User will
|
||
|
|
run on py3.14 next.
|
||
|
|
|
||
|
|
## Files changed
|
||
|
|
|
||
|
|
See `git log 26fb820..HEAD --stat` for the canonical list.
|
||
|
|
New files this session:
|
||
|
|
- `tractor/spawn/_subint_fork.py`
|
||
|
|
- `ai/conc-anal/subint_fork_blocked_by_cpython_post_fork_issue.md`
|
||
|
|
- `ai/conc-anal/subint_fork_from_main_thread_smoketest.py`
|
||
|
|
|
||
|
|
Modified (diff pointers in raw log):
|
||
|
|
- `tractor/spawn/_subint.py` (py3.14 gate)
|
||
|
|
- `tractor/spawn/_spawn.py` (`subint_fork` registration)
|
||
|
|
- `tractor/_testing/pytest.py` (`skipon_spawn_backend` refactor)
|
||
|
|
- `pyproject.toml` (`pytest-timeout` dep)
|
||
|
|
- `tests/discovery/test_registrar.py`,
|
||
|
|
`tests/test_cancellation.py`,
|
||
|
|
`tests/test_subint_cancellation.py` (timeout marks,
|
||
|
|
cross-refs to conc-anal docs)
|
||
|
|
|
||
|
|
## Human edits
|
||
|
|
|
||
|
|
Several back-and-forth iterations with user-driven
|
||
|
|
adjustments during the session:
|
||
|
|
|
||
|
|
- User corrected my initial mis-classification of
|
||
|
|
`test_cancel_while_childs_child_in_sync_sleep[subint-False]`
|
||
|
|
as Ctrl-C-able — second strace showed `EAGAIN`, putting
|
||
|
|
it squarely in class A (GIL-starvation). Re-analysis
|
||
|
|
preserved in the raw log.
|
||
|
|
- User independently fixed the `.get(reason)` → `.get('reason', reason)`
|
||
|
|
bug in the marker plugin before my review; preserved their
|
||
|
|
fix.
|
||
|
|
- User suggested moving the `subint_fork_proc` stub from
|
||
|
|
the bottom of `_subint.py` into its own
|
||
|
|
`_subint_fork.py` submodule — applied.
|
||
|
|
- User asked to keep the forkserver-architecture
|
||
|
|
discussion as background for the smoke-test rather than
|
||
|
|
committing to a tractor-side refactor until the smoke
|
||
|
|
test validates the CPython-level assumptions.
|
||
|
|
|
||
|
|
Commit messages in this range (b025c982 … 797f57c) were
|
||
|
|
drafted via `/commit-msg` + `rewrap.py --width 67`; user
|
||
|
|
landed them with the usual review.
|