Compare commits
5 Commits
0be872ff97
...
28e6269a35
| Author | SHA1 | Date |
|---|---|---|
|
|
28e6269a35 | |
|
|
67fa97dbaf | |
|
|
545142933e | |
|
|
5067917b9f | |
|
|
346878219c |
|
|
@ -0,0 +1,632 @@
|
||||||
|
---
|
||||||
|
name: run-tests
|
||||||
|
description: >
|
||||||
|
Run tractor test suite (or subsets). Use when the user wants
|
||||||
|
to run tests, verify changes, or check for regressions.
|
||||||
|
argument-hint: "[test-path-or-pattern] [--opts]"
|
||||||
|
allowed-tools:
|
||||||
|
- Bash(python -m pytest *)
|
||||||
|
- Bash(python -c *)
|
||||||
|
- Bash(python --version *)
|
||||||
|
- Bash(UV_PROJECT_ENVIRONMENT=py* uv run python *)
|
||||||
|
- Bash(UV_PROJECT_ENVIRONMENT=py* uv run pytest *)
|
||||||
|
- Bash(UV_PROJECT_ENVIRONMENT=py* uv sync *)
|
||||||
|
- Bash(UV_PROJECT_ENVIRONMENT=py* uv pip show *)
|
||||||
|
- Bash(git rev-parse *)
|
||||||
|
- Bash(ls *)
|
||||||
|
- Bash(cat *)
|
||||||
|
- Bash(jq * .pytest_cache/*)
|
||||||
|
# process inspection + SIGINT-first cleanup ladder (see
|
||||||
|
# the zombie-actor pre-flight / teardown steps below).
|
||||||
|
- Bash(ss *)
|
||||||
|
- Bash(pgrep *)
|
||||||
|
- Bash(pkill *)
|
||||||
|
- Bash(sleep *)
|
||||||
|
- Bash(rm -f /tmp/registry@*.sock)
|
||||||
|
- Read
|
||||||
|
- Grep
|
||||||
|
- Glob
|
||||||
|
- Task
|
||||||
|
- AskUserQuestion
|
||||||
|
---
|
||||||
|
|
||||||
|
Run the `tractor` test suite using `pytest`. Follow this
|
||||||
|
process:
|
||||||
|
|
||||||
|
## 1. Parse user intent
|
||||||
|
|
||||||
|
From the user's message and any arguments, determine:
|
||||||
|
|
||||||
|
- **scope**: full suite, specific file(s), specific
|
||||||
|
test(s), or a keyword pattern (`-k`).
|
||||||
|
- **transport**: which IPC transport protocol to test
|
||||||
|
against (default: `tcp`, also: `uds`).
|
||||||
|
- **options**: any extra pytest flags the user wants
|
||||||
|
(e.g. `--ll debug`, `--tpdb`, `-x`, `-v`).
|
||||||
|
|
||||||
|
If the user provides a bare path or pattern as argument,
|
||||||
|
treat it as the test target. Examples:
|
||||||
|
|
||||||
|
- `/run-tests` → full suite
|
||||||
|
- `/run-tests test_local.py` → single file
|
||||||
|
- `/run-tests test_registrar -v` → file + verbose
|
||||||
|
- `/run-tests -k cancel` → keyword filter
|
||||||
|
- `/run-tests tests/ipc/ --tpt-proto uds` → subdir + UDS
|
||||||
|
|
||||||
|
## 2. Construct the pytest command
|
||||||
|
|
||||||
|
Base command:
|
||||||
|
```
|
||||||
|
python -m pytest
|
||||||
|
```
|
||||||
|
|
||||||
|
### Default flags (always include unless user overrides):
|
||||||
|
- `-x` (stop on first failure)
|
||||||
|
- `--tb=short` (concise tracebacks)
|
||||||
|
- `--no-header` (reduce noise)
|
||||||
|
|
||||||
|
### Path resolution:
|
||||||
|
- If the user gives a bare filename like `test_local.py`,
|
||||||
|
resolve it under `tests/`.
|
||||||
|
- If the user gives a subdirectory like `ipc/`, resolve
|
||||||
|
under `tests/ipc/`.
|
||||||
|
- Glob if needed: `tests/**/test_*<pattern>*.py`
|
||||||
|
|
||||||
|
### Key pytest options for this project:
|
||||||
|
|
||||||
|
| Flag | Purpose |
|
||||||
|
|---|---|
|
||||||
|
| `--ll <level>` | Set tractor log level (e.g. `debug`, `info`, `runtime`) |
|
||||||
|
| `--tpdb` / `--debug-mode` | Enable tractor's multi-proc debugger |
|
||||||
|
| `--tpt-proto <key>` | IPC transport: `tcp` (default) or `uds` |
|
||||||
|
| `--spawn-backend <be>` | Spawn method: `trio` (default), `mp_spawn`, `mp_forkserver` |
|
||||||
|
| `-k <expr>` | pytest keyword filter |
|
||||||
|
| `-v` / `-vv` | Verbosity |
|
||||||
|
| `-s` | No output capture (useful with `--tpdb`) |
|
||||||
|
|
||||||
|
### Common combos:
|
||||||
|
```sh
|
||||||
|
# quick smoke test of core modules
|
||||||
|
python -m pytest tests/test_local.py tests/test_rpc.py -x --tb=short --no-header
|
||||||
|
|
||||||
|
# full suite, stop on first failure
|
||||||
|
python -m pytest tests/ -x --tb=short --no-header
|
||||||
|
|
||||||
|
# specific test with debug
|
||||||
|
python -m pytest tests/discovery/test_registrar.py::test_reg_then_unreg -x -s --tpdb --ll debug
|
||||||
|
|
||||||
|
# run with UDS transport
|
||||||
|
python -m pytest tests/ -x --tb=short --no-header --tpt-proto uds
|
||||||
|
|
||||||
|
# keyword filter
|
||||||
|
python -m pytest tests/ -x --tb=short --no-header -k "cancel and not slow"
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3. Pre-flight: venv detection (MANDATORY)
|
||||||
|
|
||||||
|
**Always verify a `uv` venv is active before running
|
||||||
|
`python` or `pytest`.** This project uses
|
||||||
|
`UV_PROJECT_ENVIRONMENT=py<MINOR>` naming (e.g.
|
||||||
|
`py313`) — never `.venv`.
|
||||||
|
|
||||||
|
### Step 1: detect active venv
|
||||||
|
|
||||||
|
Run this check first:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python -c "
|
||||||
|
import sys, os
|
||||||
|
venv = os.environ.get('VIRTUAL_ENV', '')
|
||||||
|
prefix = sys.prefix
|
||||||
|
print(f'VIRTUAL_ENV={venv}')
|
||||||
|
print(f'sys.prefix={prefix}')
|
||||||
|
print(f'executable={sys.executable}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Step 2: interpret results
|
||||||
|
|
||||||
|
**Case A — venv is active** (`VIRTUAL_ENV` is set
|
||||||
|
and points to a `py<MINOR>/` dir under the project
|
||||||
|
root or worktree):
|
||||||
|
|
||||||
|
Use bare `python` / `python -m pytest` for all
|
||||||
|
commands. This is the normal, fast path.
|
||||||
|
|
||||||
|
**Case B — no venv active** (`VIRTUAL_ENV` is empty
|
||||||
|
or `sys.prefix` points to a system Python):
|
||||||
|
|
||||||
|
Use `AskUserQuestion` to ask the user:
|
||||||
|
|
||||||
|
> "No uv venv is active. Should I activate one
|
||||||
|
> via `UV_PROJECT_ENVIRONMENT=py<MINOR> uv sync`,
|
||||||
|
> or would you prefer to activate your shell venv
|
||||||
|
> first?"
|
||||||
|
|
||||||
|
Options:
|
||||||
|
1. **"Create/sync venv"** — run
|
||||||
|
`UV_PROJECT_ENVIRONMENT=py<MINOR> uv sync` where
|
||||||
|
`<MINOR>` is detected from `python --version`
|
||||||
|
(e.g. `313` for 3.13). Then use
|
||||||
|
`py<MINOR>/bin/python` for all subsequent
|
||||||
|
commands in this session.
|
||||||
|
2. **"I'll activate it myself"** — stop and let the
|
||||||
|
user `source py<MINOR>/bin/activate` or similar.
|
||||||
|
|
||||||
|
**Case C — inside a git worktree** (`git rev-parse
|
||||||
|
--git-common-dir` differs from `--git-dir`):
|
||||||
|
|
||||||
|
Verify Python resolves from the **worktree's own
|
||||||
|
venv**, not the main repo's:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python -c "import tractor; print(tractor.__file__)"
|
||||||
|
```
|
||||||
|
|
||||||
|
If the path points outside the worktree, create a
|
||||||
|
worktree-local venv:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
UV_PROJECT_ENVIRONMENT=py<MINOR> uv sync
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use `py<MINOR>/bin/python` for all commands.
|
||||||
|
|
||||||
|
**Why this matters**: without the correct venv,
|
||||||
|
subprocesses spawned by tractor resolve modules
|
||||||
|
from the wrong editable install, causing spurious
|
||||||
|
`AttributeError` / `ModuleNotFoundError`.
|
||||||
|
|
||||||
|
### Fallback: `uv run`
|
||||||
|
|
||||||
|
If the user can't or won't activate a venv, all
|
||||||
|
`python` and `pytest` commands can be prefixed
|
||||||
|
with `UV_PROJECT_ENVIRONMENT=py<MINOR> uv run`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# instead of: python -m pytest tests/ -x
|
||||||
|
UV_PROJECT_ENVIRONMENT=py313 uv run pytest tests/ -x
|
||||||
|
|
||||||
|
# instead of: python -c 'import tractor'
|
||||||
|
UV_PROJECT_ENVIRONMENT=py313 uv run python -c 'import tractor'
|
||||||
|
```
|
||||||
|
|
||||||
|
`uv run` auto-discovers the project and venv,
|
||||||
|
but is slower than a pre-activated venv due to
|
||||||
|
lock-file resolution on each invocation. Prefer
|
||||||
|
activating the venv when possible.
|
||||||
|
|
||||||
|
### Step 3: import + collection checks
|
||||||
|
|
||||||
|
After venv is confirmed, always run these
|
||||||
|
(especially after refactors or module moves):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. package import smoke check
|
||||||
|
python -c 'import tractor; print(tractor)'
|
||||||
|
|
||||||
|
# 2. verify all tests collect (no import errors)
|
||||||
|
python -m pytest tests/ -x -q --co 2>&1 | tail -5
|
||||||
|
```
|
||||||
|
|
||||||
|
If either fails, fix the import error before running
|
||||||
|
any actual tests.
|
||||||
|
|
||||||
|
### Step 4: zombie-actor / stale-registry check (MANDATORY)
|
||||||
|
|
||||||
|
The tractor runtime's default registry address is
|
||||||
|
**`127.0.0.1:1616`** (TCP) / `/tmp/registry@1616.sock`
|
||||||
|
(UDS). Whenever any prior test run — especially one
|
||||||
|
using a fork-based backend like `subint_forkserver` —
|
||||||
|
leaks a child actor process, that zombie keeps the
|
||||||
|
registry port bound and **every subsequent test
|
||||||
|
session fails to bind**, often presenting as 50+
|
||||||
|
unrelated failures ("all tests broken"!) across
|
||||||
|
backends.
|
||||||
|
|
||||||
|
**This has to be checked before the first run AND
|
||||||
|
after any cancelled/SIGINT'd run** — signal failures
|
||||||
|
in the middle of a test can leave orphan children.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. TCP registry — any listener on :1616? (primary signal)
|
||||||
|
ss -tlnp 2>/dev/null | grep ':1616' || echo 'TCP :1616 free'
|
||||||
|
|
||||||
|
# 2. leftover actor/forkserver procs — scoped to THIS
|
||||||
|
# repo's python path, so we don't false-flag legit
|
||||||
|
# long-running tractor-using apps (e.g. `piker`,
|
||||||
|
# downstream projects that embed tractor).
|
||||||
|
pgrep -af "$(pwd)/py[0-9]*/bin/python.*_actor_child_main|subint-forkserv" \
|
||||||
|
| grep -v 'grep\|pgrep' \
|
||||||
|
|| echo 'no leaked actor procs from this repo'
|
||||||
|
|
||||||
|
# 3. stale UDS registry sockets
|
||||||
|
ls -la /tmp/registry@*.sock 2>/dev/null \
|
||||||
|
|| echo 'no leaked UDS registry sockets'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Interpretation:**
|
||||||
|
|
||||||
|
- **TCP :1616 free AND no stale sockets** → clean,
|
||||||
|
proceed. The actor-procs probe is secondary — false
|
||||||
|
positives are common (piker, any other tractor-
|
||||||
|
embedding app); only cleanup if `:1616` is bound or
|
||||||
|
sockets linger.
|
||||||
|
- **TCP :1616 bound OR stale sockets present** →
|
||||||
|
surface PIDs + cmdlines to the user, offer cleanup:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# 1. GRACEFUL FIRST (tractor is structured concurrent — it
|
||||||
|
# catches SIGINT as an OS-cancel in `_trio_main` and
|
||||||
|
# cascades Portal.cancel_actor via IPC to every descendant.
|
||||||
|
# So always try SIGINT first with a bounded timeout; only
|
||||||
|
# escalate to SIGKILL if graceful cleanup doesn't complete).
|
||||||
|
pkill -INT -f "$(pwd)/py[0-9]*/bin/python.*_actor_child_main|subint-forkserv"
|
||||||
|
|
||||||
|
# 2. bounded wait for graceful teardown (usually sub-second).
|
||||||
|
# Loop until the processes exit, or timeout. Keep the
|
||||||
|
# bound tight — hung/abrupt-killed descendants usually
|
||||||
|
# hang forever, so don't wait more than a few seconds.
|
||||||
|
for i in $(seq 1 10); do
|
||||||
|
pgrep -f "$(pwd)/py[0-9]*/bin/python.*_actor_child_main|subint-forkserv" >/dev/null || break
|
||||||
|
sleep 0.3
|
||||||
|
done
|
||||||
|
|
||||||
|
# 3. ESCALATE TO SIGKILL only if graceful didn't finish.
|
||||||
|
if pgrep -f "$(pwd)/py[0-9]*/bin/python.*_actor_child_main|subint-forkserv" >/dev/null; then
|
||||||
|
echo 'graceful teardown timed out — escalating to SIGKILL'
|
||||||
|
pkill -9 -f "$(pwd)/py[0-9]*/bin/python.*_actor_child_main|subint-forkserv"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. if a test zombie holds :1616 specifically and doesn't
|
||||||
|
# match the above pattern, find its PID the hard way:
|
||||||
|
ss -tlnp 2>/dev/null | grep ':1616' # prints `users:(("<name>",pid=NNNN,...))`
|
||||||
|
# then (same SIGINT-first ladder):
|
||||||
|
# kill -INT <NNNN>; sleep 1; kill -9 <NNNN> 2>/dev/null
|
||||||
|
|
||||||
|
# 5. remove stale UDS sockets
|
||||||
|
rm -f /tmp/registry@*.sock
|
||||||
|
|
||||||
|
# 6. re-verify
|
||||||
|
ss -tlnp 2>/dev/null | grep ':1616' || echo 'TCP :1616 now free'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Never ignore stale registry state.** If you see the
|
||||||
|
"all tests failing" pattern — especially
|
||||||
|
`trio.TooSlowError` / connection refused / address in
|
||||||
|
use on many unrelated tests — check registry **before**
|
||||||
|
spelunking into test code. The failure signature will
|
||||||
|
be identical across backends because they're all
|
||||||
|
fighting for the same port.
|
||||||
|
|
||||||
|
**False-positive warning for step 2:** a plain
|
||||||
|
`pgrep -af '_actor_child_main'` will also match
|
||||||
|
legit long-running tractor-embedding apps (e.g.
|
||||||
|
`piker` at `~/repos/piker/py*/bin/python3 -m
|
||||||
|
tractor._child ...`). Always scope to the current
|
||||||
|
repo's python path, or only use step 1 (`:1616`) as
|
||||||
|
the authoritative signal.
|
||||||
|
|
||||||
|
## 4. Run and report
|
||||||
|
|
||||||
|
- Run the constructed command.
|
||||||
|
- Use a timeout of **600000ms** (10min) for full suite
|
||||||
|
runs, **120000ms** (2min) for single-file runs.
|
||||||
|
- If the suite is large (full `tests/`), consider running
|
||||||
|
in the background and checking output when done.
|
||||||
|
- Use `--lf` (last-failed) to re-run only previously
|
||||||
|
failing tests when iterating on a fix.
|
||||||
|
|
||||||
|
### On failure:
|
||||||
|
- Show the failing test name(s) and short traceback.
|
||||||
|
- If the failure looks related to recent changes, point
|
||||||
|
out the likely cause and suggest a fix.
|
||||||
|
- **Check the known-flaky list** (section 8) before
|
||||||
|
investigating — don't waste time on pre-existing
|
||||||
|
timeout issues.
|
||||||
|
- **NEVER auto-commit fixes.** If you apply a code fix
|
||||||
|
during test iteration, leave it unstaged. Tell the
|
||||||
|
user what changed and suggest they review the
|
||||||
|
worktree state, stage files manually, and use
|
||||||
|
`/commit-msg` (inline or in a separate session) to
|
||||||
|
generate the commit message. The human drives all
|
||||||
|
`git add` and `git commit` operations.
|
||||||
|
|
||||||
|
### On success:
|
||||||
|
- Report the pass/fail/skip counts concisely.
|
||||||
|
|
||||||
|
## 5. Test directory layout (reference)
|
||||||
|
|
||||||
|
```
|
||||||
|
tests/
|
||||||
|
├── conftest.py # root fixtures, daemon, signals
|
||||||
|
├── devx/ # debugger/tooling tests
|
||||||
|
├── ipc/ # transport protocol tests
|
||||||
|
├── msg/ # messaging layer tests
|
||||||
|
├── discovery/ # discovery subsystem tests
|
||||||
|
│ ├── test_multiaddr.py # multiaddr construction
|
||||||
|
│ └── test_registrar.py # registry/discovery protocol
|
||||||
|
├── test_local.py # registrar + local actor basics
|
||||||
|
├── test_rpc.py # RPC error handling
|
||||||
|
├── test_spawning.py # subprocess spawning
|
||||||
|
├── test_multi_program.py # multi-process tree tests
|
||||||
|
├── test_cancellation.py # cancellation semantics
|
||||||
|
├── test_context_stream_semantics.py # ctx streaming
|
||||||
|
├── test_inter_peer_cancellation.py # peer cancel
|
||||||
|
├── test_infected_asyncio.py # trio-in-asyncio
|
||||||
|
└── ...
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. Change-type → test mapping
|
||||||
|
|
||||||
|
After modifying specific modules, run the corresponding
|
||||||
|
test subset first for fast feedback:
|
||||||
|
|
||||||
|
| Changed module(s) | Run these tests first |
|
||||||
|
|---|---|
|
||||||
|
| `runtime/_runtime.py`, `runtime/_state.py` | `test_local.py test_rpc.py test_spawning.py test_root_runtime.py` |
|
||||||
|
| `discovery/` (`_registry`, `_discovery`, `_addr`) | `tests/discovery/ test_multi_program.py test_local.py` |
|
||||||
|
| `_context.py`, `_streaming.py` | `test_context_stream_semantics.py test_advanced_streaming.py` |
|
||||||
|
| `ipc/` (`_chan`, `_server`, `_transport`) | `tests/ipc/ test_2way.py` |
|
||||||
|
| `runtime/_portal.py`, `runtime/_rpc.py` | `test_rpc.py test_cancellation.py` |
|
||||||
|
| `spawn/` (`_spawn`, `_entry`) | `test_spawning.py test_multi_program.py` |
|
||||||
|
| `devx/debug/` | `tests/devx/test_debugger.py` (slow!) |
|
||||||
|
| `to_asyncio.py` | `test_infected_asyncio.py test_root_infect_asyncio.py` |
|
||||||
|
| `msg/` | `tests/msg/` |
|
||||||
|
| `_exceptions.py` | `test_remote_exc_relay.py test_inter_peer_cancellation.py` |
|
||||||
|
| `runtime/_supervise.py` | `test_cancellation.py test_spawning.py` |
|
||||||
|
|
||||||
|
## 7. Quick-check shortcuts
|
||||||
|
|
||||||
|
### After refactors (fastest first-pass):
|
||||||
|
```sh
|
||||||
|
# import + collect check
|
||||||
|
python -c 'import tractor' && python -m pytest tests/ -x -q --co 2>&1 | tail -3
|
||||||
|
|
||||||
|
# core subset (~10s)
|
||||||
|
python -m pytest tests/test_local.py tests/test_rpc.py tests/test_spawning.py tests/discovery/test_registrar.py -x --tb=short --no-header
|
||||||
|
```
|
||||||
|
|
||||||
|
### Inspect last failures (without re-running):
|
||||||
|
|
||||||
|
When the user asks "what failed?", "show failures",
|
||||||
|
or wants to check the last-failed set before
|
||||||
|
re-running — read the pytest cache directly. This
|
||||||
|
is instant and avoids test collection overhead.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
python -c "
|
||||||
|
import json, pathlib, sys
|
||||||
|
p = pathlib.Path('.pytest_cache/v/cache/lastfailed')
|
||||||
|
if not p.exists():
|
||||||
|
print('No lastfailed cache found.'); sys.exit()
|
||||||
|
data = json.loads(p.read_text())
|
||||||
|
# filter to real test node IDs (ignore junk
|
||||||
|
# entries that can accumulate from system paths)
|
||||||
|
tests = sorted(k for k in data if k.startswith('tests/'))
|
||||||
|
if not tests:
|
||||||
|
print('No failures recorded.')
|
||||||
|
else:
|
||||||
|
print(f'{len(tests)} last-failed test(s):')
|
||||||
|
for t in tests:
|
||||||
|
print(f' {t}')
|
||||||
|
"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Why not `--cache-show` or `--co --lf`?**
|
||||||
|
|
||||||
|
- `pytest --cache-show 'cache/lastfailed'` works
|
||||||
|
but dumps raw dict repr including junk entries
|
||||||
|
(stale system paths that leak into the cache).
|
||||||
|
- `pytest --co --lf` actually *collects* tests which
|
||||||
|
triggers import resolution and is slow (~0.5s+).
|
||||||
|
Worse, when cached node IDs don't exactly match
|
||||||
|
current parametrize IDs (e.g. param names changed
|
||||||
|
between runs), pytest falls back to collecting
|
||||||
|
the *entire file*, giving false positives.
|
||||||
|
- Reading the JSON directly is instant, filterable
|
||||||
|
to `tests/`-prefixed entries, and shows exactly
|
||||||
|
what pytest recorded — no interpretation.
|
||||||
|
|
||||||
|
**After inspecting**, re-run the failures:
|
||||||
|
```sh
|
||||||
|
python -m pytest --lf -x --tb=short --no-header
|
||||||
|
```
|
||||||
|
|
||||||
|
### Full suite in background:
|
||||||
|
When core tests pass and you want full coverage while
|
||||||
|
continuing other work, run in background:
|
||||||
|
```sh
|
||||||
|
python -m pytest tests/ -x --tb=short --no-header -q
|
||||||
|
```
|
||||||
|
(use `run_in_background=true` on the Bash tool)
|
||||||
|
|
||||||
|
## 8. Known flaky tests
|
||||||
|
|
||||||
|
These tests have **pre-existing** timing/environment
|
||||||
|
sensitivity. If they fail with `TooSlowError` or
|
||||||
|
pexpect `TIMEOUT`, they are almost certainly NOT caused
|
||||||
|
by your changes — note them and move on.
|
||||||
|
|
||||||
|
| Test | Typical error | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| `devx/test_debugger.py::test_multi_nested_subactors_error_through_nurseries` | pexpect TIMEOUT | Debugger pexpect timing |
|
||||||
|
| `test_cancellation.py::test_cancel_via_SIGINT_other_task` | TooSlowError | Signal handling race |
|
||||||
|
| `test_inter_peer_cancellation.py::test_peer_spawns_and_cancels_service_subactor` | TooSlowError | Async timing (both param variants) |
|
||||||
|
| `test_docs_examples.py::test_example[we_are_processes.py]` | `assert None == 0` | `__main__` missing `__file__` in subproc |
|
||||||
|
|
||||||
|
**Rule of thumb**: if a test fails with `TooSlowError`,
|
||||||
|
`trio.TooSlowError`, or `pexpect.TIMEOUT` and you didn't
|
||||||
|
touch the relevant code path, it's flaky — skip it.
|
||||||
|
|
||||||
|
## 9. The pytest-capture hang pattern (CHECK THIS FIRST)
|
||||||
|
|
||||||
|
**Symptom:** a tractor test hangs indefinitely under
|
||||||
|
default `pytest` but passes instantly when you add
|
||||||
|
`-s` (`--capture=no`).
|
||||||
|
|
||||||
|
**Cause:** tractor subactors (especially under fork-
|
||||||
|
based backends) inherit pytest's stdout/stderr
|
||||||
|
capture pipes via fds 1,2. Under high-volume error
|
||||||
|
logging (e.g. multi-level cancel cascade, nested
|
||||||
|
`run_in_actor` failures, anything triggering
|
||||||
|
`RemoteActorError` + `ExceptionGroup` traceback
|
||||||
|
spew), the **64KB Linux pipe buffer fills** faster
|
||||||
|
than pytest drains it. Subactor writes block → can't
|
||||||
|
finish exit → parent's `waitpid`/pidfd wait blocks →
|
||||||
|
deadlock cascades up the tree.
|
||||||
|
|
||||||
|
**Pre-existing guards in the tractor harness** that
|
||||||
|
encode this same knowledge — grep these FIRST
|
||||||
|
before spelunking:
|
||||||
|
|
||||||
|
- `tests/conftest.py:258-260` (in the `daemon`
|
||||||
|
fixture): `# XXX: too much logging will lock up
|
||||||
|
the subproc (smh)` — downgrades `trace`/`debug`
|
||||||
|
loglevel to `info` to prevent the hang.
|
||||||
|
- `tests/conftest.py:316`: `# can lock up on the
|
||||||
|
_io.BufferedReader and hang..` — noted on the
|
||||||
|
`proc.stderr.read()` post-SIGINT.
|
||||||
|
|
||||||
|
**Debug recipe (in priority order):**
|
||||||
|
|
||||||
|
1. **Try `-s` first.** If the hang disappears with
|
||||||
|
`pytest -s`, you've confirmed it's capture-pipe
|
||||||
|
fill. Skip spelunking.
|
||||||
|
2. **Lower the loglevel.** Default `--ll=error` on
|
||||||
|
this project; if you've bumped it to `debug` /
|
||||||
|
`info`, try dropping back. Each log level
|
||||||
|
multiplies pipe-pressure under fault cascades.
|
||||||
|
3. **If you MUST use default capture + high log
|
||||||
|
volume**, redirect subactor stdout/stderr in the
|
||||||
|
child prelude (e.g.
|
||||||
|
`tractor.spawn._subint_forkserver._child_target`
|
||||||
|
post-`_close_inherited_fds`) to `/dev/null` or a
|
||||||
|
file.
|
||||||
|
|
||||||
|
**Signature tells you it's THIS bug (vs. a real
|
||||||
|
code hang):**
|
||||||
|
|
||||||
|
- Multi-actor test under fork-based backend
|
||||||
|
(`subint_forkserver`, eventually `trio_proc` too
|
||||||
|
under enough log volume).
|
||||||
|
- Multiple `RemoteActorError` / `ExceptionGroup`
|
||||||
|
tracebacks in the error path.
|
||||||
|
- Test passes with `-s` in the 5-10s range, hangs
|
||||||
|
past pytest-timeout (usually 30+ s) without `-s`.
|
||||||
|
- Subactor processes visible via `pgrep -af
|
||||||
|
subint-forkserv` or similar after the hang —
|
||||||
|
they're alive but blocked on `write()` to an
|
||||||
|
inherited stdout fd.
|
||||||
|
|
||||||
|
**Historical reference:** this deadlock cost a
|
||||||
|
multi-session investigation (4 genuine cascade
|
||||||
|
fixes landed along the way) that only surfaced the
|
||||||
|
capture-pipe issue AFTER the deeper fixes let the
|
||||||
|
tree actually tear down enough to produce pipe-
|
||||||
|
filling log volume. Full post-mortem in
|
||||||
|
`ai/conc-anal/subint_forkserver_test_cancellation_leak_issue.md`.
|
||||||
|
Lesson codified here so future-me grep-finds the
|
||||||
|
workaround before digging.
|
||||||
|
|
||||||
|
## 10. Reaping zombie subactors (`tractor-reap`)
|
||||||
|
|
||||||
|
**Symptom:** after a `pytest` run crashes, times out,
|
||||||
|
or is `Ctrl+C`'d, subactor forks (esp. under
|
||||||
|
`subint_forkserver`) can be reparented to `init`
|
||||||
|
(PPid==1) and linger. They hold onto ports, inherit
|
||||||
|
pytest's capture-pipe fds, and flakify later
|
||||||
|
sessions.
|
||||||
|
|
||||||
|
**Two layers of defense:**
|
||||||
|
|
||||||
|
### a) Session-scoped auto-fixture (always on)
|
||||||
|
|
||||||
|
`tractor/_testing/pytest.py::_reap_orphaned_subactors`
|
||||||
|
runs at pytest session teardown. It walks `/proc` for
|
||||||
|
direct descendants of the pytest pid, SIGINTs them,
|
||||||
|
waits up to 3s, then SIGKILLs survivors. SC-polite:
|
||||||
|
gives the subactor runtime a chance to run its trio
|
||||||
|
cancel shield + IPC teardown before escalation.
|
||||||
|
|
||||||
|
This is *autouse* and session-scoped — you don't need
|
||||||
|
to do anything. It just runs.
|
||||||
|
|
||||||
|
### b) `scripts/tractor-reap` CLI (manual reap)
|
||||||
|
|
||||||
|
For the **pytest-died-mid-session** case (Ctrl+C, OOM
|
||||||
|
kill, hung process you had to `kill -9`), the fixture
|
||||||
|
never ran. Reach for the CLI:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# default: orphans (PPid==1, cwd==repo, cmd contains python)
|
||||||
|
scripts/tractor-reap
|
||||||
|
|
||||||
|
# descendant-mode: from a still-live supervisor
|
||||||
|
scripts/tractor-reap --parent <pytest-pid>
|
||||||
|
|
||||||
|
# see what would be reaped, don't signal
|
||||||
|
scripts/tractor-reap -n
|
||||||
|
|
||||||
|
# tune the SIGINT → SIGKILL grace window
|
||||||
|
scripts/tractor-reap --grace 5
|
||||||
|
```
|
||||||
|
|
||||||
|
Exit code: `0` if everyone exited on SIGINT, `1` if
|
||||||
|
SIGKILL had to escalate — so you can chain it in CI
|
||||||
|
health-checks (`scripts/tractor-reap || <alert>`).
|
||||||
|
|
||||||
|
**What it matches** (orphan-mode):
|
||||||
|
- `PPid == 1` (reparented to init → definitely
|
||||||
|
orphaned, not just a currently-running child)
|
||||||
|
- `cwd == <repo-root>` (keeps the sweep scoped; won't
|
||||||
|
touch unrelated init-children elsewhere)
|
||||||
|
- `python` in cmdline
|
||||||
|
|
||||||
|
**What it does not do:** kill anything whose PPid is
|
||||||
|
still a live tractor parent. If the parent is alive
|
||||||
|
it's not an orphan; use `--parent <pid>` if you need
|
||||||
|
to force-reap under a still-live supervisor.
|
||||||
|
|
||||||
|
**When NOT to run it:** while a pytest session is
|
||||||
|
active in another terminal. It's safe (won't touch
|
||||||
|
that session's live children in orphan-mode) but can
|
||||||
|
race if the target session is mid-teardown.
|
||||||
|
|
||||||
|
### c) `--shm` / `--shm-only`: orphan-segment sweep
|
||||||
|
|
||||||
|
Because `tractor.ipc._mp_bs.disable_mantracker()`
|
||||||
|
turns off `mp.resource_tracker` (see
|
||||||
|
`ai/conc-anal/subint_forkserver_mp_shared_memory_issue.md`),
|
||||||
|
a hard-crashing actor can leave `/dev/shm/<key>`
|
||||||
|
segments behind that nothing else GCs.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
# process reap THEN shm sweep
|
||||||
|
scripts/tractor-reap --shm
|
||||||
|
|
||||||
|
# shm sweep only (skip process phase)
|
||||||
|
scripts/tractor-reap --shm-only
|
||||||
|
|
||||||
|
# dry-run: list candidates, don't unlink
|
||||||
|
scripts/tractor-reap --shm -n
|
||||||
|
```
|
||||||
|
|
||||||
|
**Match criteria** (very conservative — this is a
|
||||||
|
shared-system path, can't be wrong):
|
||||||
|
- segment is a regular file under `/dev/shm`,
|
||||||
|
- owned by the **current uid** (`stat.st_uid`),
|
||||||
|
- AND **no live process holds it open** —
|
||||||
|
enumerated by walking every readable
|
||||||
|
`/proc/<pid>/maps` (post-mmap mappings) AND
|
||||||
|
`/proc/<pid>/fd/*` (pre-mmap shm-opened fds).
|
||||||
|
|
||||||
|
The "nobody has it open" check is the
|
||||||
|
kernel-canonical "is this leaked?" test — same
|
||||||
|
answer `lsof /dev/shm/<key>` would give. No
|
||||||
|
reliance on tractor-specific naming, so it works
|
||||||
|
for any tractor app. Critically, it WILL NOT touch
|
||||||
|
segments held by other apps you have running
|
||||||
|
(e.g. `piker`, `lttng-ust-*`, `aja-shm-*` —
|
||||||
|
verified locally with 81 in-use segments correctly
|
||||||
|
preserved).
|
||||||
|
|
@ -1,255 +0,0 @@
|
||||||
# Tractor Test Harness Reference
|
|
||||||
|
|
||||||
This repository-local file supplements the canonical [`/run-tests` skill][1]
|
|
||||||
from the [`ai.skillz` repository][2]. Its deployer links the shared `SKILL.md`
|
|
||||||
into both
|
|
||||||
`.claude/skills/run-tests/` and `.opencode/skills/run-tests/` while preserving
|
|
||||||
this project-owned reference:
|
|
||||||
|
|
||||||
```text
|
|
||||||
bash /path/to/ai.skillz/scripts/deploy.sh run-tests /path/to/tractor --provider all --method symlink
|
|
||||||
```
|
|
||||||
|
|
||||||
Keep shared environment permission, process-signal safety, target selection,
|
|
||||||
failure inspection, and result reporting policy in the deployed `SKILL.md`.
|
|
||||||
|
|
||||||
[1]: https://github.com/baudco/ai.skillz/blob/2d4896ca7e38fe2cb3090cdefc7245be4241a6d2/skills/run-tests/SKILL.md
|
|
||||||
[2]: https://github.com/baudco/ai.skillz
|
|
||||||
|
|
||||||
## Project And Environment
|
|
||||||
|
|
||||||
- Project/import: `tractor`
|
|
||||||
- Test root: `tests/`
|
|
||||||
- Supported Python: `>=3.13,<3.15`
|
|
||||||
- Runner: pytest `>=9.0.3`
|
|
||||||
- Test dependencies: the `dev` group includes the `testing` group
|
|
||||||
- CI uses uv's default `.venv`; the Nix flake uses `py313`.
|
|
||||||
- Run from the repository root so pytest loads `pyproject.toml`.
|
|
||||||
- Do not use `default.nix` as current test-environment authority; it still
|
|
||||||
selects unsupported Python 3.12.
|
|
||||||
|
|
||||||
Environment directory naming is not a harness invariant. Use an already
|
|
||||||
verified active project environment when available. Otherwise, use an
|
|
||||||
existing uv environment without syncing it:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync python -c 'import pathlib, sys, tractor; root = pathlib.Path.cwd().resolve(); mod = pathlib.Path(tractor.__file__).resolve(); print(sys.executable); print(mod); assert mod.is_relative_to(root)'
|
|
||||||
```
|
|
||||||
|
|
||||||
After module moves or collection failures, check collection with:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync pytest --collect-only -q tests/
|
|
||||||
```
|
|
||||||
|
|
||||||
Collection is not a mandatory precursor to every narrow run. Ask before
|
|
||||||
provisioning or changing an environment.
|
|
||||||
|
|
||||||
Before trusting CLI-selected runtime settings, inspect
|
|
||||||
`TRACTOR_SPAWN_METHOD` and `TRACTOR_LOGLEVEL`. They override the spawn method
|
|
||||||
and runtime log level passed by callers, so report active values with test
|
|
||||||
results rather than claiming the CLI flags alone selected the runtime.
|
|
||||||
|
|
||||||
## Pytest Configuration And Commands
|
|
||||||
|
|
||||||
`pyproject.toml` configures:
|
|
||||||
|
|
||||||
- `testpaths = ["tests"]` and `--rootdir=./tests`;
|
|
||||||
- importlib import mode;
|
|
||||||
- the `tractor._testing.pytest` plugin;
|
|
||||||
- xonsh plugin disablement;
|
|
||||||
- `--show-capture=no` and `--capture=fd`.
|
|
||||||
|
|
||||||
Do not silently add `-x`, `--tb=short`, or `--no-header`; those are not
|
|
||||||
project defaults. In a verified active environment, replace `uv run
|
|
||||||
--frozen --no-sync pytest` below with `python -m pytest`.
|
|
||||||
|
|
||||||
```text
|
|
||||||
# Full suite
|
|
||||||
uv run --frozen --no-sync pytest tests/
|
|
||||||
|
|
||||||
# Narrow file
|
|
||||||
uv run --frozen --no-sync pytest tests/test_local.py
|
|
||||||
|
|
||||||
# Exact node
|
|
||||||
uv run --frozen --no-sync pytest tests/discovery/test_registrar.py::test_reg_then_unreg
|
|
||||||
|
|
||||||
# Keyword selection
|
|
||||||
uv run --frozen --no-sync pytest tests/ -k 'cancel and not slow'
|
|
||||||
|
|
||||||
# Previous failures
|
|
||||||
uv run --frozen --no-sync pytest --lf
|
|
||||||
```
|
|
||||||
|
|
||||||
After verifying that the no-sync environment is current, these pytest
|
|
||||||
arguments match the Linux TCP CI row:
|
|
||||||
|
|
||||||
```text
|
|
||||||
CI=1 uv run --frozen --no-sync pytest tests/ -rsx --spawn-backend=trio --tpt-proto=tcp --capture=fd
|
|
||||||
```
|
|
||||||
|
|
||||||
## Plugin Options And Matrices
|
|
||||||
|
|
||||||
Supported spawn backends:
|
|
||||||
|
|
||||||
- `trio` (default)
|
|
||||||
- `mp_spawn`
|
|
||||||
- `mp_forkserver`
|
|
||||||
|
|
||||||
Do not advertise `subint`, `subint_forkserver`, or
|
|
||||||
`main_thread_forkserver` as runnable backends. Supported transports are
|
|
||||||
`tcp` (default) and `uds`. Run one transport per pytest session.
|
|
||||||
`mp_forkserver` and UDS are POSIX-only.
|
|
||||||
|
|
||||||
Other Tractor plugin options include:
|
|
||||||
|
|
||||||
- `--tpdb` / `--debug-mode`
|
|
||||||
- `--ll` / `--loglevel`
|
|
||||||
- `--tl` / `--tractor-loglevel`
|
|
||||||
- `--enable-stackscope`
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync pytest tests/ipc/ --tpt-proto=uds
|
|
||||||
uv run --frozen --no-sync pytest tests/test_spawning.py --spawn-backend=mp_spawn
|
|
||||||
uv run --frozen --no-sync pytest tests/test_spawning.py --spawn-backend=mp_forkserver --capture=sys
|
|
||||||
```
|
|
||||||
|
|
||||||
CI currently exercises Python 3.13 with the `trio` backend: TCP and UDS on
|
|
||||||
Linux and macOS, plus an informational TCP row on Windows whose pytest step
|
|
||||||
uses `continue-on-error`.
|
|
||||||
|
|
||||||
## Registry And Transport Isolation
|
|
||||||
|
|
||||||
Tests requesting the `reg_addr` fixture use addresses randomized per session:
|
|
||||||
an unreserved unprivileged loopback port for TCP or a unique socket name under
|
|
||||||
the platform runtime directory for UDS. A TCP collision remains possible.
|
|
||||||
|
|
||||||
The runtime fallback remains `127.0.0.1:1616` or `registry@1616.sock`.
|
|
||||||
Inspect that fallback only when the selected test intentionally uses runtime
|
|
||||||
defaults or a failure identifies that address. Do not perform a mandatory
|
|
||||||
`:1616` preflight or assume UDS sockets live under `/tmp`.
|
|
||||||
|
|
||||||
## Capture And Hang Diagnosis
|
|
||||||
|
|
||||||
Normal capture is `fd`. Use `--capture=sys` with `mp_forkserver`; some tests
|
|
||||||
switch to `capsys`, but the harness does not enforce that suite-wide.
|
|
||||||
|
|
||||||
For a suspected capture interaction, compare only the exact node:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync pytest <node> --capture=sys
|
|
||||||
uv run --frozen --no-sync pytest <node> -s
|
|
||||||
```
|
|
||||||
|
|
||||||
Treat `-s` as a diagnostic comparison, not a pass-equivalent workaround. Do
|
|
||||||
not use it to reinterpret an ordinary captured pass. Interactive `--tpdb` or
|
|
||||||
`tractor.pause()` sessions are different: they require a real TTY and disabled
|
|
||||||
capture, normally `-s`.
|
|
||||||
|
|
||||||
Do not add a global pytest timeout. `fail_after_w_trace` is Trio-cooperative;
|
|
||||||
`afk_alarm_w_trace` is a POSIX main-thread `SIGALRM` hard backstop and can
|
|
||||||
raise asynchronously. Use the latter only as a last resort, not as a generally
|
|
||||||
Trio-safe timeout replacement.
|
|
||||||
|
|
||||||
For live task-tree diagnosis:
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync python -c 'import stackscope'
|
|
||||||
uv run --frozen --no-sync pytest <node> --enable-stackscope --capture=sys
|
|
||||||
kill -USR1 <pytest-pid>
|
|
||||||
```
|
|
||||||
|
|
||||||
The import check and pytest command must use the same environment. Do not send
|
|
||||||
SIGUSR1 if the import fails or setup warns that stackscope or SIGUSR1 is
|
|
||||||
unavailable: without the installed handler, SIGUSR1 normally terminates the
|
|
||||||
target process. Signal a subactor only after separately confirming that it
|
|
||||||
installed the same handler.
|
|
||||||
|
|
||||||
Stackscope appends dumps to `/tmp/tractor-stackscope-<pid>.log`, including when
|
|
||||||
pytest capture hides terminal output. SIGUSR1 stackscope is unavailable on
|
|
||||||
Windows and degrades to a no-op there.
|
|
||||||
|
|
||||||
When a trace guard actually fires and snapshot capture succeeds, it writes
|
|
||||||
under `$XDG_CACHE_HOME/tractor/hung-dumps/`, falling back beneath
|
|
||||||
`~/.cache/tractor/hung-dumps/`, and prints an end-of-session index. A normal
|
|
||||||
non-timeout run creates no snapshot.
|
|
||||||
|
|
||||||
## Cleanup And `tractor-reap`
|
|
||||||
|
|
||||||
On Linux, normal pytest teardown discovers surviving descendants through
|
|
||||||
`/proc`, sends SIGINT, waits three seconds, then escalates survivors to
|
|
||||||
SIGKILL. It does not sweep shared memory and cannot run if pytest never reaches
|
|
||||||
fixture teardown.
|
|
||||||
|
|
||||||
Process discovery is a no-op off Linux. UDS PID liveness also depends on
|
|
||||||
`/proc`; on macOS, recognized PID-named sockets can therefore be classified as
|
|
||||||
dead without proof. The session-scoped autouse fixture currently passes those
|
|
||||||
candidates directly to `reap_uds()` at teardown. Do not treat its non-Linux
|
|
||||||
classification as proof of orphanhood or run concurrent live Tractor sessions
|
|
||||||
against the same UDS bindspace.
|
|
||||||
|
|
||||||
Use the CLI in inspection-only mode first:
|
|
||||||
|
|
||||||
`--shm` and `--shm-only` are Linux/FreeBSD-only and raise
|
|
||||||
`NotImplementedError` elsewhere. On other platforms, use the UDS-only command.
|
|
||||||
|
|
||||||
```text
|
|
||||||
uv run --frozen --no-sync scripts/tractor-reap -n
|
|
||||||
uv run --frozen --no-sync scripts/tractor-reap --parent <pytest-pid> -n
|
|
||||||
uv run --frozen --no-sync scripts/tractor-reap --shm --uds -n
|
|
||||||
uv run --frozen --no-sync scripts/tractor-reap --uds-only -n
|
|
||||||
```
|
|
||||||
|
|
||||||
Direct `scripts/tractor-reap` execution is acceptable only after verifying its
|
|
||||||
`python3` shebang resolves the intended project environment.
|
|
||||||
|
|
||||||
Review every candidate before requesting a mutating run:
|
|
||||||
|
|
||||||
- default orphan mode is not repository-scoped;
|
|
||||||
- `--parent` trusts the supplied PID and can include non-Tractor children;
|
|
||||||
- `--shm` scans all current-user candidate files, not just Tractor-named
|
|
||||||
files;
|
|
||||||
- `--uds` treats `registry@1616.sock` as removable even if a live default UDS
|
|
||||||
registrar uses it.
|
|
||||||
|
|
||||||
Dry-run output prints only the initially matched root PIDs. A mutating run can
|
|
||||||
recursively expand those roots to additional descendants when `psutil` is
|
|
||||||
available. Inspect the descendant process tree separately; `-n` is not exact
|
|
||||||
signal-set parity and does not by itself authorize signaling unseen children.
|
|
||||||
|
|
||||||
The canonical skill owns signaling and unlinking authorization.
|
|
||||||
|
|
||||||
## Test Layout And Change Mapping
|
|
||||||
|
|
||||||
| Changed area | Run first |
|
|
||||||
|---|---|
|
|
||||||
| `tractor/runtime/_runtime.py`, `_state.py`, `tractor/_root.py` | `tests/test_local.py`, `tests/test_root_runtime.py`, `tests/test_runtime.py`, `tests/test_rpc.py` |
|
|
||||||
| `tractor/runtime/_portal.py`, `_rpc.py` | `tests/test_rpc.py`, `tests/test_cancellation.py` |
|
|
||||||
| `tractor/runtime/_supervise.py` | `tests/test_cancellation.py`, `tests/test_spawning.py` |
|
|
||||||
| `tractor/discovery/` | `tests/discovery/`, `tests/test_local.py` |
|
|
||||||
| `tractor/ipc/` | `tests/ipc/`, `tests/test_2way.py`, `tests/test_shm.py` as relevant |
|
|
||||||
| `tractor/spawn/` | `tests/test_spawning.py`, `tests/discovery/test_multi_program.py`, `tests/test_cancellation.py` |
|
|
||||||
| `tractor/_context.py`, `_streaming.py` | `tests/test_context_stream_semantics.py`, `tests/test_advanced_streaming.py`, `tests/test_legacy_one_way_streaming.py` |
|
|
||||||
| `tractor/to_asyncio.py` | `tests/test_infected_asyncio.py`, `tests/test_root_infect_asyncio.py` |
|
|
||||||
| `tractor/msg/` | `tests/msg/` |
|
|
||||||
| `tractor/devx/` | `tests/devx/`; debugger tests use pexpect and are comparatively slow |
|
|
||||||
| `tractor/_exceptions.py` | `tests/test_remote_exc_relay.py`, `tests/test_reg_err_types.py`, `tests/test_inter_peer_cancellation.py`, `tests/test_cancellation.py`, `tests/msg/` |
|
|
||||||
|
|
||||||
Current subdirectories include `discovery/`, `ipc/`, `msg/`, `devx/`, and
|
|
||||||
`trionics/`. There is no `tests/spawn/` directory.
|
|
||||||
|
|
||||||
## Expected Outcomes
|
|
||||||
|
|
||||||
Do not maintain a blanket known-flaky exemption list. Classify only current
|
|
||||||
explicit skip or xfail marks and exact expected signatures. Notable tracked
|
|
||||||
outcomes include:
|
|
||||||
|
|
||||||
- duplicate-name `n_dups=4` and `n_dups=8` variants in
|
|
||||||
`tests/discovery/test_multi_program.py` are non-strict xfails;
|
|
||||||
- `tests/test_ringbuf.py` is module-skipped;
|
|
||||||
- some documentation examples have explicit macOS-CI skips.
|
|
||||||
|
|
||||||
A generic `TooSlowError` or `pexpect.TIMEOUT` is not enough to classify a
|
|
||||||
failure as pre-existing.
|
|
||||||
|
|
@ -91,10 +91,6 @@ jobs:
|
||||||
name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}'
|
name: '${{ matrix.os }} Python${{ matrix.python-version }} spawn_backend=${{ matrix.spawn_backend }} tpt_proto=${{ matrix.tpt_proto }}'
|
||||||
timeout-minutes: 16
|
timeout-minutes: 16
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
# Windows support is nascent: its full test suite remains
|
|
||||||
# informational, while setup and the `import tractor` smoke below
|
|
||||||
# are hard signals. Promote the test step to required once the
|
|
||||||
# suite is green.
|
|
||||||
|
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
|
|
@ -102,7 +98,6 @@ jobs:
|
||||||
os: [
|
os: [
|
||||||
ubuntu-latest,
|
ubuntu-latest,
|
||||||
macos-latest,
|
macos-latest,
|
||||||
windows-latest,
|
|
||||||
]
|
]
|
||||||
python-version: [
|
python-version: [
|
||||||
'3.13',
|
'3.13',
|
||||||
|
|
@ -123,10 +118,10 @@ jobs:
|
||||||
'tcp',
|
'tcp',
|
||||||
'uds',
|
'uds',
|
||||||
]
|
]
|
||||||
|
# https://github.com/orgs/community/discussions/26253#discussioncomment-3250989
|
||||||
exclude:
|
exclude:
|
||||||
# UDS is POSIX-only; Windows has no `AF_UNIX` so the
|
# don't do UDS run on macOS (for now)
|
||||||
# backend is intentionally unavailable there.
|
- os: macos-latest
|
||||||
- os: windows-latest
|
|
||||||
tpt_proto: 'uds'
|
tpt_proto: 'uds'
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
|
|
@ -155,18 +150,7 @@ jobs:
|
||||||
- name: List deps tree
|
- name: List deps tree
|
||||||
run: uv tree
|
run: uv tree
|
||||||
|
|
||||||
# hard signal for the Windows import-safety fix: `import
|
|
||||||
# tractor` must succeed everywhere, and `HAS_UDS` reflects
|
|
||||||
# platform capability (False on Windows, True on POSIX).
|
|
||||||
- name: 'Smoke: import tractor'
|
|
||||||
run: uv run python -c "import sys; import tractor; from tractor.ipc._uds import HAS_UDS; assert sys.platform != 'win32' or not HAS_UDS; print('import tractor OK | HAS_UDS=', HAS_UDS)"
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
# Actor/PTY scheduling on macOS can fail a different
|
|
||||||
# timing-sensitive node between otherwise-green runs. Retry
|
|
||||||
# only that matrix leg; deterministic failures still fail
|
|
||||||
# after the final attempt.
|
|
||||||
continue-on-error: ${{ matrix.os == 'windows-latest' }}
|
|
||||||
run: >
|
run: >
|
||||||
uv run
|
uv run
|
||||||
pytest
|
pytest
|
||||||
|
|
@ -175,8 +159,6 @@ jobs:
|
||||||
--spawn-backend=${{ matrix.spawn_backend }}
|
--spawn-backend=${{ matrix.spawn_backend }}
|
||||||
--tpt-proto=${{ matrix.tpt_proto }}
|
--tpt-proto=${{ matrix.tpt_proto }}
|
||||||
--capture=fd
|
--capture=fd
|
||||||
--reruns=${{ matrix.os == 'macos-latest' && 2 || 0 }}
|
|
||||||
--reruns-delay=1
|
|
||||||
|
|
||||||
# XXX legacy NOTE XXX
|
# XXX legacy NOTE XXX
|
||||||
#
|
#
|
||||||
|
|
|
||||||
|
|
@ -168,267 +168,3 @@ gh/
|
||||||
|
|
||||||
# LLM conversations that should remain private
|
# LLM conversations that should remain private
|
||||||
docs/conversations/
|
docs/conversations/
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:run-tests
|
|
||||||
/.claude/skills/run-tests/SKILL.md
|
|
||||||
# END ai.skillz: direct:symlink:claude:run-tests
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:run-tests
|
|
||||||
/.opencode/skills/run-tests/SKILL.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:run-tests
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:run-tests
|
|
||||||
/.opencode/commands/run-tests.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:run-tests
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:gish
|
|
||||||
/.claude/skills/gish
|
|
||||||
# END ai.skillz: direct:symlink:claude:gish
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:gish
|
|
||||||
/.opencode/skills/gish
|
|
||||||
# END ai.skillz: direct:symlink:opencode:gish
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:resolve-conflicts
|
|
||||||
/.claude/skills/resolve-conflicts
|
|
||||||
# END ai.skillz: direct:symlink:claude:resolve-conflicts
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:resolve-conflicts
|
|
||||||
/.opencode/skills/resolve-conflicts
|
|
||||||
# END ai.skillz: direct:symlink:opencode:resolve-conflicts
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:git-mgmt
|
|
||||||
/.claude/skills/git-mgmt
|
|
||||||
# END ai.skillz: direct:symlink:claude:git-mgmt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:git-mgmt
|
|
||||||
/.opencode/skills/git-mgmt
|
|
||||||
# END ai.skillz: direct:symlink:opencode:git-mgmt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:open-wkt
|
|
||||||
/wkts/
|
|
||||||
# END ai.skillz: runtime:open-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:open-wkt
|
|
||||||
/.claude/skills/open-wkt
|
|
||||||
# END ai.skillz: direct:symlink:claude:open-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:open-wkt
|
|
||||||
/.opencode/skills/open-wkt
|
|
||||||
# END ai.skillz: direct:symlink:opencode:open-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:close-wkt
|
|
||||||
/.claude/skills/close-wkt
|
|
||||||
# END ai.skillz: direct:symlink:claude:close-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:close-wkt
|
|
||||||
/.opencode/skills/close-wkt
|
|
||||||
# END ai.skillz: direct:symlink:opencode:close-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:code-review
|
|
||||||
.ai/code-review/reports/
|
|
||||||
# END ai.skillz: runtime:code-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:code-review
|
|
||||||
/.claude/skills/code-review
|
|
||||||
# END ai.skillz: direct:symlink:claude:code-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:code-review
|
|
||||||
/.opencode/skills/code-review
|
|
||||||
# END ai.skillz: direct:symlink:opencode:code-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:code-nav-refs
|
|
||||||
/.claude/skills/code-nav-refs
|
|
||||||
# END ai.skillz: direct:symlink:claude:code-nav-refs
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:code-nav-refs
|
|
||||||
/.opencode/skills/code-nav-refs
|
|
||||||
# END ai.skillz: direct:symlink:opencode:code-nav-refs
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:code-review-changes
|
|
||||||
.claude/review_context.md
|
|
||||||
.claude/review_regression.md
|
|
||||||
.claude/review_replies/
|
|
||||||
# END ai.skillz: runtime:code-review-changes
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:code-review-changes
|
|
||||||
/.claude/skills/code-review-changes
|
|
||||||
# END ai.skillz: direct:symlink:claude:code-review-changes
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:code-review-changes
|
|
||||||
/.opencode/skills/code-review-changes
|
|
||||||
# END ai.skillz: direct:symlink:opencode:code-review-changes
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:commit-msg
|
|
||||||
.claude/skills/commit-msg/msgs/
|
|
||||||
.claude/git_commit_msg_LATEST.md
|
|
||||||
# END ai.skillz: runtime:commit-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:commit-msg
|
|
||||||
/.claude/skills/commit-msg/SKILL.md
|
|
||||||
# END ai.skillz: direct:symlink:claude:commit-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:commit-msg
|
|
||||||
/.opencode/skills/commit-msg/SKILL.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:commit-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:commit-plan
|
|
||||||
/.claude/skills/commit-plan
|
|
||||||
# END ai.skillz: direct:symlink:claude:commit-plan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:commit-plan
|
|
||||||
/.opencode/skills/commit-plan
|
|
||||||
# END ai.skillz: direct:symlink:opencode:commit-plan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:dep-supersede-scan
|
|
||||||
/.claude/skills/dep-supersede-scan
|
|
||||||
# END ai.skillz: direct:symlink:claude:dep-supersede-scan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:dep-supersede-scan
|
|
||||||
/.opencode/skills/dep-supersede-scan
|
|
||||||
# END ai.skillz: direct:symlink:opencode:dep-supersede-scan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:harness-perf
|
|
||||||
/.claude/skills/harness-perf
|
|
||||||
# END ai.skillz: direct:symlink:claude:harness-perf
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:harness-perf
|
|
||||||
/.opencode/skills/harness-perf
|
|
||||||
# END ai.skillz: direct:symlink:opencode:harness-perf
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:inter-skill-review
|
|
||||||
/.claude/skills/inter-skill-review
|
|
||||||
# END ai.skillz: direct:symlink:claude:inter-skill-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:inter-skill-review
|
|
||||||
/.opencode/skills/inter-skill-review
|
|
||||||
# END ai.skillz: direct:symlink:opencode:inter-skill-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:opencode-cleaning
|
|
||||||
/.claude/skills/opencode-cleaning
|
|
||||||
# END ai.skillz: direct:symlink:claude:opencode-cleaning
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:opencode-cleaning
|
|
||||||
/.opencode/skills/opencode-cleaning
|
|
||||||
# END ai.skillz: direct:symlink:opencode:opencode-cleaning
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:plan-io
|
|
||||||
/.claude/skills/plan-io
|
|
||||||
# END ai.skillz: direct:symlink:claude:plan-io
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:plan-io
|
|
||||||
/.opencode/skills/plan-io
|
|
||||||
# END ai.skillz: direct:symlink:opencode:plan-io
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:pr-msg
|
|
||||||
.claude/skills/pr-msg/msgs/
|
|
||||||
.claude/skills/pr-msg/pr_msg_LATEST.md
|
|
||||||
# END ai.skillz: runtime:pr-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:pr-msg
|
|
||||||
/.claude/skills/pr-msg/SKILL.md
|
|
||||||
/.claude/skills/pr-msg/references
|
|
||||||
/.claude/skills/pr-msg/scripts
|
|
||||||
# END ai.skillz: direct:symlink:claude:pr-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:pr-msg
|
|
||||||
/.opencode/skills/pr-msg/SKILL.md
|
|
||||||
/.opencode/skills/pr-msg/references
|
|
||||||
/.opencode/skills/pr-msg/scripts
|
|
||||||
# END ai.skillz: direct:symlink:opencode:pr-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:prompt-io
|
|
||||||
/.claude/skills/prompt-io
|
|
||||||
# END ai.skillz: direct:symlink:claude:prompt-io
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:prompt-io
|
|
||||||
/.opencode/skills/prompt-io
|
|
||||||
# END ai.skillz: direct:symlink:opencode:prompt-io
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:py-codestyle
|
|
||||||
/.claude/skills/py-codestyle
|
|
||||||
# END ai.skillz: direct:symlink:claude:py-codestyle
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:py-codestyle
|
|
||||||
/.opencode/skills/py-codestyle
|
|
||||||
# END ai.skillz: direct:symlink:opencode:py-codestyle
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: runtime:taken-export
|
|
||||||
.ai/taken/exports/
|
|
||||||
# END ai.skillz: runtime:taken-export
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:taken-export
|
|
||||||
/.claude/skills/taken-export
|
|
||||||
# END ai.skillz: direct:symlink:claude:taken-export
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:taken-export
|
|
||||||
/.opencode/skills/taken-export
|
|
||||||
# END ai.skillz: direct:symlink:opencode:taken-export
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:claude:yt-url-lookup
|
|
||||||
/.claude/skills/yt-url-lookup
|
|
||||||
# END ai.skillz: direct:symlink:claude:yt-url-lookup
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:yt-url-lookup
|
|
||||||
/.opencode/skills/yt-url-lookup
|
|
||||||
# END ai.skillz: direct:symlink:opencode:yt-url-lookup
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:gish
|
|
||||||
/.opencode/commands/gish.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:gish
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:resolve-conflicts
|
|
||||||
/.opencode/commands/resolve-conflicts.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:resolve-conflicts
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:git-mgmt
|
|
||||||
/.opencode/commands/git-mgmt.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:git-mgmt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:open-wkt
|
|
||||||
/.opencode/commands/open-wkt.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:open-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:close-wkt
|
|
||||||
/.opencode/commands/close-wkt.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:close-wkt
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:code-review
|
|
||||||
/.opencode/commands/code-review.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:code-review
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:code-review-changes
|
|
||||||
/.opencode/commands/code-review-changes.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:code-review-changes
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:commit-msg
|
|
||||||
/.opencode/commands/commit-msg.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:commit-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:commit-plan
|
|
||||||
/.opencode/commands/commit-plan.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:commit-plan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:dep-supersede-scan
|
|
||||||
/.opencode/commands/dep-supersede-scan.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:dep-supersede-scan
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:harness-perf
|
|
||||||
/.opencode/commands/harness-perf.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:harness-perf
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:opencode-cleaning
|
|
||||||
/.opencode/commands/opencode-cleaning.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:opencode-cleaning
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:pr-msg
|
|
||||||
/.opencode/commands/pr-msg.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:pr-msg
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:taken-export
|
|
||||||
/.opencode/commands/taken-export.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:taken-export
|
|
||||||
|
|
||||||
# BEGIN ai.skillz: direct:symlink:opencode:command:yt-url-lookup
|
|
||||||
/.opencode/commands/yt-url-lookup.md
|
|
||||||
# END ai.skillz: direct:symlink:opencode:command:yt-url-lookup
|
|
||||||
|
|
|
||||||
|
|
@ -1,463 +0,0 @@
|
||||||
# `_ria_nursery` removal plan (issue #477 follow-up)
|
|
||||||
|
|
||||||
Goal: drop the secondary "run-in-actor" spawn nursery (and
|
|
||||||
friends) from `ActorNursery`/spawn internals, now that
|
|
||||||
`tractor.to_actor.run()` delivers one-shot semantics purely on
|
|
||||||
the daemon-spawn + portal primitives.
|
|
||||||
|
|
||||||
## Verified machinery map (2026-07-02, wkt @ a34aaf98)
|
|
||||||
|
|
||||||
The entire mechanism is 4 files:
|
|
||||||
|
|
||||||
- `runtime/_supervise.py`
|
|
||||||
- `ActorNursery.__init__(.., ria_nursery, ..)` stores
|
|
||||||
`._ria_nursery` (:202, :238); sole read is
|
|
||||||
`run_in_actor()` passing `nursery=self._ria_nursery`
|
|
||||||
(:442) into `start_actor()`'s `nursery:
|
|
||||||
trio.Nursery|None` escape-hatch param (:305, :367).
|
|
||||||
- `._cancel_after_result_on_exit: set` (:244) marks ria
|
|
||||||
portals (:457).
|
|
||||||
- `_open_and_supervise_one_cancels_all_nursery()` nests
|
|
||||||
`da_nursery` (:609) around `ria_nursery` (:622); the
|
|
||||||
`finally:` at the ria->da boundary (:747-766) raises
|
|
||||||
collected `errors` (single exc or BEG).
|
|
||||||
- `runtime/_portal.py`
|
|
||||||
- `._expect_result_ctx` (:112) set by `_submit_for_result()`
|
|
||||||
(:142, sole caller `run_in_actor()`); consumed by
|
|
||||||
`wait_for_result()` (:167) + deprecated `result()` (:220).
|
|
||||||
The `None` branch (:184-196) returns the `NoResult`
|
|
||||||
sentinel (`_exceptions.py:1164`).
|
|
||||||
- `spawn/_spawn.py`
|
|
||||||
- `exhaust_portal()` (:129): awaits
|
|
||||||
`portal.wait_for_result()`, CATCHES+RETURNS any exc
|
|
||||||
(never raises).
|
|
||||||
- `cancel_on_completion()` (:177): `exhaust_portal()` ->
|
|
||||||
on exc-result stash `errors[uid] = result` (:203) ->
|
|
||||||
ALWAYS `portal.cancel_actor()` (:218).
|
|
||||||
- `spawn/_trio.py` (:195-222) + `spawn/_mp.py` (:187-213),
|
|
||||||
identical shape: after shielded
|
|
||||||
`await an._join_procs.wait()`, open a per-child local
|
|
||||||
nursery; IFF `portal in an._cancel_after_result_on_exit`
|
|
||||||
start `cancel_on_completion` alongside `soft_kill()`; when
|
|
||||||
`soft_kill` returns first, `nursery.cancel_scope.cancel()`
|
|
||||||
reaps the result-waiter.
|
|
||||||
|
|
||||||
## The load-bearing semantic (already-deferred errors)
|
|
||||||
|
|
||||||
Remote ria-child errors NEVER raise into `ria_nursery`:
|
|
||||||
|
|
||||||
1. reaper tasks only START after `_join_procs.set()` (block
|
|
||||||
exit or the inner error handler),
|
|
||||||
2. `exhaust_portal` swallows the exc into a return value,
|
|
||||||
3. `cancel_on_completion` stashes it in `errors` + cancels
|
|
||||||
that child,
|
|
||||||
4. the ria->da `finally:` re-raises collected `errors` (and
|
|
||||||
`an.cancel()`s any daemon stragglers).
|
|
||||||
|
|
||||||
So mid-block there is NO error propagation from ria children
|
|
||||||
(unless user code explicitly `await portal.wait_for_result()`s)
|
|
||||||
— the two-nursery nesting only sequences "reap ria results
|
|
||||||
BEFORE blocking on daemon join". A single-nursery impl only
|
|
||||||
needs to preserve that sequencing, not any ASAP-cancel
|
|
||||||
behavior.
|
|
||||||
|
|
||||||
## Target design
|
|
||||||
|
|
||||||
### step A: single-nursery `run_in_actor()` (mechanical)
|
|
||||||
|
|
||||||
- `run_in_actor()` spawns via the DEFAULT (`_da_nursery`)
|
|
||||||
path — drop `nursery=self._ria_nursery`.
|
|
||||||
- rename `._cancel_after_result_on_exit` ->
|
|
||||||
`._ria_portals: dict[portal, Actor]` (need the subactor ref
|
|
||||||
for `cancel_on_completion`).
|
|
||||||
- move reaper start-up OUT of the backends into
|
|
||||||
`_open_and_supervise...`: immediately after EACH
|
|
||||||
`an._join_procs.set()` call-site (happy path :642, inner
|
|
||||||
error handler :661), start one
|
|
||||||
`cancel_on_completion(portal, subactor, errors)` task per
|
|
||||||
ria portal into `da_nursery`, then (happy path only)
|
|
||||||
`await` their completion BEFORE falling out of the
|
|
||||||
`try:`/`finally:` that raises `errors` — e.g. gather in a
|
|
||||||
dedicated inner `trio.open_nursery()` block replacing
|
|
||||||
today's `ria_nursery` join point.
|
|
||||||
- delete the membership branch + local reaper nursery from
|
|
||||||
`_trio.py`/`_mp.py` (keep the `soft_kill()` call; the
|
|
||||||
per-child local nursery collapses to just `soft_kill`).
|
|
||||||
- `_trio.py:310` `_children.pop()` etc. unchanged.
|
|
||||||
|
|
||||||
### step B: delete the plumbing
|
|
||||||
|
|
||||||
- `_open_and_supervise...`: drop the inner
|
|
||||||
`ria_nursery` + merge its `except BaseException` classify
|
|
||||||
logic into ONE handler on the (now single) nursery scope;
|
|
||||||
`ActorNursery.__init__` loses the `ria_nursery` param.
|
|
||||||
- `start_actor()` loses the `nursery:` escape-hatch param
|
|
||||||
(the :302-304 TODO).
|
|
||||||
- backends: no more `_cancel_after_result_on_exit` refs.
|
|
||||||
|
|
||||||
### step C: (separate PRs) deprecate + migrate + excise
|
|
||||||
|
|
||||||
- migrate in-repo `.run_in_actor()` usage to
|
|
||||||
`to_actor.run()`: tests 46 hits/9 files (test_cancellation
|
|
||||||
15, test_infected_asyncio 10, test_spawning 8, registrar 3,
|
|
||||||
adv_streaming 4, pubsub 2, rpc 1, runtime 1), examples 28
|
|
||||||
hits/13 files (debugging/* dominate), docs 20 hits/8 rst
|
|
||||||
files. NOTE: many sites also use deprecated
|
|
||||||
`Portal.result()`/`wait_for_result()` — these die with
|
|
||||||
`_expect_result_ctx`, so migration must land FIRST.
|
|
||||||
- add `DeprecationWarning` to `run_in_actor()` (+
|
|
||||||
`_submit_for_result`/`wait_for_result`).
|
|
||||||
- final excision: `run_in_actor()`, `_submit_for_result`,
|
|
||||||
`_expect_result_ctx`, `wait_for_result`/`result`,
|
|
||||||
`exhaust_portal`, `cancel_on_completion`, `NoResult`.
|
|
||||||
|
|
||||||
## Risk register
|
|
||||||
|
|
||||||
1. hard-killed ria child: today the backend-local
|
|
||||||
`nursery.cancel_scope.cancel()` discards a still-parked
|
|
||||||
reaper when the proc dies first; a da_nursery-hosted
|
|
||||||
reaper instead sees the transport break ->
|
|
||||||
`exhaust_portal` returns a `TransportClosed`-ish exc ->
|
|
||||||
NEW entry in `errors` that today gets discarded. Guard:
|
|
||||||
reap-gather block must cancel remaining reapers once all
|
|
||||||
ria procs are dead, or filter transport-death excs for
|
|
||||||
already-`cancel_called` children.
|
|
||||||
2. error-path ordering: inner handler today sets
|
|
||||||
`_join_procs` THEN `an.cancel()`; reapers race the
|
|
||||||
cancel-RPC. Keep that ordering when moving reaper spawn.
|
|
||||||
3. debugger interplay: `maybe_wait_for_debugger()` calls
|
|
||||||
(:654, :730) must stay BEFORE any reap/cancel issuance.
|
|
||||||
4. `errors` double-entry: local body error (:646) + child's
|
|
||||||
relayed exc (via reaper) can both land for the same
|
|
||||||
scenario -> BEG shape changes vs today? (today has the
|
|
||||||
same dual-write sites; keep behavior identical.)
|
|
||||||
5. mp backend parity: mirror every `_trio.py` edit in
|
|
||||||
`_mp.py` (identical block).
|
|
||||||
|
|
||||||
## Step-A first-probe findings (2026-07-02, WIP in tree)
|
|
||||||
|
|
||||||
Step A is IMPLEMENTED (uncommitted):
|
|
||||||
`run_in_actor()` spawns via da_nursery; new
|
|
||||||
`_supervise._reap_ria_portals()` helper; reap awaited after
|
|
||||||
happy-path `_join_procs.set()`; error-path runs reap
|
|
||||||
CONCURRENT with `an.cancel()` in the shielded block;
|
|
||||||
backends stripped of the membership branch + per-child
|
|
||||||
reaper nursery (+ dead imports).
|
|
||||||
|
|
||||||
Probe history (trio backend):
|
|
||||||
- `tests/test_to_actor.py` + `tests/test_spawning.py`:
|
|
||||||
20/20 PASS — incl. all `run_in_actor()` result
|
|
||||||
round-trips + `test_remote_error` (single erroring child,
|
|
||||||
body re-raise -> inner error path).
|
|
||||||
- FIRST attempt ran the error-path reap CONCURRENT with
|
|
||||||
`an.cancel()` (mimicking the old backend-side race):
|
|
||||||
`test_cancellation.py::test_multierror` (2 erroring ria
|
|
||||||
children, body re-raises one) DEADLOCKED. Root cause per
|
|
||||||
the sequencing fix below: reap + cancel must NOT race at
|
|
||||||
this layer (suspected `._children` pop-during-iteration
|
|
||||||
and/or double-cancel RPC wedge; not fully root-caused
|
|
||||||
since the fix removes the race wholesale).
|
|
||||||
- FIX (2nd attempt, current impl): error path SEQUENCES:
|
|
||||||
(1) snapshot ria `(portal, subactor)` pairs (backend
|
|
||||||
`finally`s pop `._children` as procs reap), (2)
|
|
||||||
`await an.cancel()`, (3) bounded reap over the snapshot.
|
|
||||||
Bound was first 3s -> blew the `fail_after` deadline in
|
|
||||||
`test_cancel_while_childs_child_in_sync_sleep` (hard-
|
|
||||||
killed grandchild never relays => reaper parks the full
|
|
||||||
bound). Tightened to 0.5s: anything collectable is
|
|
||||||
already queued in the local ctx (relayed BEFORE the
|
|
||||||
cancel); a parked reaper self-cleans (`trio.Cancelled`
|
|
||||||
results are never stashed).
|
|
||||||
- RESULT: `tests/test_cancellation.py` FULLY GREEN
|
|
||||||
(20 passed, 1 xfailed, 77s); full-suite gate run kicked
|
|
||||||
off same session (see final report/next session).
|
|
||||||
|
|
||||||
Remaining risk: on slow CI a relayed-but-undelivered error
|
|
||||||
racing the 0.5s bound could drop an `errors` entry
|
|
||||||
(BEG-shape flake); if observed, scale the bound via the
|
|
||||||
`cpu_perf_headroom()`-style approach or peek
|
|
||||||
`Portal._final_result_msg`/ctx queue state instead of
|
|
||||||
time-bounding.
|
|
||||||
|
|
||||||
## Step-B outcome (2026-07-02, done in tree)
|
|
||||||
|
|
||||||
Step A landed as `5cd190c5` (code) + `99310269` (docs).
|
|
||||||
Step B implemented on top (uncommitted):
|
|
||||||
|
|
||||||
- `._ria_nursery` is GONE — the inner
|
|
||||||
`async with (collapse_eg(), trio.open_nursery() as
|
|
||||||
ria_nursery)` layer in
|
|
||||||
`_open_and_supervise_one_cancels_all_nursery` is deleted;
|
|
||||||
`da_nursery` is now the single nursery for ALL subactors.
|
|
||||||
- `ActorNursery.__init__` drops the `ria_nursery` param +
|
|
||||||
the `self._ria_nursery` attr; `start_actor()` drops its
|
|
||||||
`nursery=` escape-hatch param (uses `self._da_nursery`
|
|
||||||
directly).
|
|
||||||
- `._cancel_after_result_on_exit` STAYS — it's the
|
|
||||||
ria-child discriminator for `_reap_ria_portals()`.
|
|
||||||
|
|
||||||
Deliberately NOT done (deferred to its own higher-risk PR,
|
|
||||||
flagged with a TODO at the outer `except`): merging the two
|
|
||||||
error handlers into one. Rationale — collapsing the empty
|
|
||||||
nursery is provably behavior-preserving (a zero-task
|
|
||||||
`trio.open_nursery()` only adds a checkpoint), whereas the
|
|
||||||
inner `except BaseException` (swallow-into-`errors`) and
|
|
||||||
outer `except (...)` (re-raise, safety-net for the inner
|
|
||||||
handler's own non-shielded awaits) have DIFFERENT
|
|
||||||
semantics; merging changes error/cancel propagation and
|
|
||||||
wants isolated review + its own gate. Both handlers are
|
|
||||||
kept, now nested directly under the single nursery.
|
|
||||||
|
|
||||||
Why the collapse is safe: post-step-A NOTHING spawns into
|
|
||||||
`ria_nursery` (its only reader, `run_in_actor`'s
|
|
||||||
`nursery=self._ria_nursery`, was removed in A; the stored
|
|
||||||
attr was never read again). So the layer was pure dead
|
|
||||||
weight.
|
|
||||||
|
|
||||||
Gate (trio backend, all 0-failure):
|
|
||||||
- targeted set (`test_cancellation test_spawning test_local
|
|
||||||
test_rpc test_to_actor`) = 49 passed, 1 xfailed.
|
|
||||||
- tail set (`test_reg_err_types remote_exc_relay
|
|
||||||
resource_cache ringbuf root_infect_asyncio root_runtime
|
|
||||||
runtime shm task_broadcasting trioisms trionics/`) = 63
|
|
||||||
passed, 1 skipped, 5 xfailed.
|
|
||||||
- full-suite head ~73% (subdirs + `test_2way`..`test_pubsub`)
|
|
||||||
= 303 passed before the known-flaky `test_dynamic_pub_sub`
|
|
||||||
TooSlowError stall (pre-existing; same hang in the step-A
|
|
||||||
full run). Suite ran slow this session (~13min vs 555s
|
|
||||||
cold, likely thermal from back-to-back runs), never
|
|
||||||
completing within an 800s bound — but split across the
|
|
||||||
above three runs EVERY module passed under step B.
|
|
||||||
|
|
||||||
## Step-B2 outcome (2026-07-02, done in tree)
|
|
||||||
|
|
||||||
Step B committed as `9201a2ed` (code) + `d2e812fb` (docs), then
|
|
||||||
branched to `drop_ria_nursery`. Step B2 (the deferred
|
|
||||||
handler-merge) implemented on top (uncommitted):
|
|
||||||
|
|
||||||
- the two nested handlers in
|
|
||||||
`_open_and_supervise_one_cancels_all_nursery` collapse to
|
|
||||||
ONE `except BaseException as _scope_err` + the existing
|
|
||||||
`finally`. The `outer_err`/`inner_err` locals go away.
|
|
||||||
|
|
||||||
Why it's safe (trace, not hope): the OLD inner handler records
|
|
||||||
`errors[actor.aid.uid]` as its FIRST statement (before any
|
|
||||||
await). So whenever an error path runs, `errors` is non-empty.
|
|
||||||
The OLD outer handler was only reachable via leakage from the
|
|
||||||
inner handler (it catches `BaseException`, so nothing from the
|
|
||||||
`yield` scope bypasses it) — and by then `errors` is already
|
|
||||||
populated, so the `finally`'s `raise` from `errors` ALWAYS
|
|
||||||
superseded the outer handler's own `raise`. i.e. the outer
|
|
||||||
`raise` was dead. The outer handler's other effects
|
|
||||||
(`_scope_error`, a 2nd debugger-wait, child-cancel) are
|
|
||||||
redundant with the merged handler + `finally`. So one handler
|
|
||||||
+ `finally` is observably equivalent.
|
|
||||||
|
|
||||||
Residual nuance (accepted): in the rare "`trio.Cancelled`
|
|
||||||
delivered during the non-shielded `maybe_wait_for_debugger`"
|
|
||||||
path, the merged form may leave `_cancel_called` False (cancel
|
|
||||||
happens after the wait), so `open_nursery`'s tb-hiding guard
|
|
||||||
(`not cancel_called and _scope_error`) can show a tb it
|
|
||||||
previously hid. More informative, not less; no test asserts on
|
|
||||||
it.
|
|
||||||
|
|
||||||
Gate (box ran ~2.7x slow this session, load-induced
|
|
||||||
`TooSlowError` flakiness on timing tests — NOT code; see
|
|
||||||
[[env_cpu_throttle_masquerades_as_regression]]):
|
|
||||||
- baseline (pre-B2 tip `9201a2ed`) full suite
|
|
||||||
(`-k 'not dynamic_pub_sub'`) = 300 passed + 1
|
|
||||||
`test_ext_types_over_ipc` `TooSlowError` that passes 6/6 in
|
|
||||||
isolation (4.89s).
|
|
||||||
- B2 error/cancel gate (`test_cancellation remote_exc_relay
|
|
||||||
inter_peer_cancellation advanced_faults oob_cancellation
|
|
||||||
to_actor spawning local rpc`) = 71 passed, 1 xfailed
|
|
||||||
(125s).
|
|
||||||
- B2 full-suite run: see `b2_full.log` (result appended on
|
|
||||||
completion). RECOMMEND a clean full-suite run on a
|
|
||||||
normal-speed box before this merges.
|
|
||||||
|
|
||||||
## Regression + fix: ria-reap hang (2026-07-02)
|
|
||||||
|
|
||||||
Human hit a full-suite hang on
|
|
||||||
`test_infected_asyncio.py::test_tractor_cancels_aio`. Bisected:
|
|
||||||
passes at pre-ria `a34aaf98` (0.59s), hangs at B2 `e617b498`
|
|
||||||
(90s+). Root-caused to the STEP-A reaper hoist (`5cd190c5`),
|
|
||||||
NOT B2 (`_reap_ria_portals` is byte-identical A->B2).
|
|
||||||
|
|
||||||
Bug: the test does `run_in_actor(asyncio_actor)` then a USER
|
|
||||||
`portal.cancel_actor()` and exits the block cleanly -> the
|
|
||||||
happy path's `await _reap_ria_portals()`, which waits UNBOUNDED
|
|
||||||
on `cancel_on_completion -> wait_for_result()`. The child was
|
|
||||||
cancelled out-of-band so no final result is relayed -> parked
|
|
||||||
forever. The OLD spawn-backend reaper was raced against
|
|
||||||
`soft_kill()` (per-child nursery `cancel_scope.cancel()` on
|
|
||||||
subproc death); the hoist dropped that race.
|
|
||||||
|
|
||||||
Fix: `_reap_ria_portals()` runs each `cancel_on_completion()`
|
|
||||||
in a local nursery alongside a `proc.poll()` death-watch that
|
|
||||||
cancels the parked reaper once the subproc exits — restoring
|
|
||||||
the old race, backend-agnostic (guarded by
|
|
||||||
`hasattr(proc, 'poll')` for a future `subint` handle).
|
|
||||||
|
|
||||||
Why POLL (`proc.poll()`) not the event-driven `wait_func`:
|
|
||||||
the mp waiter (`_spawn.proc_waiter`) does
|
|
||||||
`wait_readable(proc.sentinel)`, and `soft_kill()` is ALREADY
|
|
||||||
awaiting that same fd concurrently in the daemon nursery — a
|
|
||||||
2nd `wait_readable` on one fd raises `trio.BusyResourceError`.
|
|
||||||
(`trio.Process.wait()` IS multi-waiter-safe, but mp has no
|
|
||||||
async equivalent.) `proc.poll()` — the same liveness check
|
|
||||||
`soft_kill` itself falls back to — is the conflict-free common
|
|
||||||
denominator. Verified: poll-fix passes on BOTH trio and
|
|
||||||
mp_spawn.
|
|
||||||
|
|
||||||
Also added a per-test anti-hang guard: wrapped
|
|
||||||
`test_tractor_cancels_aio`'s `main()` in
|
|
||||||
`with trio.fail_after(9 * cpu_perf_headroom())` — the blessed
|
|
||||||
pattern (`pytest-timeout`'s global cap is intentionally off;
|
|
||||||
breaks trio under fork backends, see `pyproject` NOTE). So a
|
|
||||||
future recurrence FAILS FAST instead of hanging the suite.
|
|
||||||
(Several other tests in the file are still guardless —
|
|
||||||
`test_aio_simple_error`, `test_trio_error_cancels_intertask_chan`,
|
|
||||||
`test_aio_errors_and_channel_propagates_and_closes` — candidate
|
|
||||||
follow-up sweep.)
|
|
||||||
|
|
||||||
Lesson: the B2 focused gate OMITTED `test_infected_asyncio`
|
|
||||||
(and the full runs were clipped/slow), so the step-A hang
|
|
||||||
slipped through. Any future ria-touching change MUST gate
|
|
||||||
`test_infected_asyncio` explicitly.
|
|
||||||
|
|
||||||
Gate: `test_tractor_cancels_aio` green (trio 1.53s, mp 3.98s);
|
|
||||||
fix gate (`test_infected_asyncio test_cancellation test_to_actor
|
|
||||||
test_spawning`) = 74 passed, 3 xfailed, 0 failures.
|
|
||||||
|
|
||||||
## PAUSED (2026-07-02): re-assess the reaper's SCOPE
|
|
||||||
|
|
||||||
User's insight (compelling — likely the real root cause of
|
|
||||||
the hang, not just the missing proc-death race):
|
|
||||||
|
|
||||||
> the "hoisting" of 5cd190c5 was just not really done right
|
|
||||||
> — the hoist should have been into the `to_actor` scope,
|
|
||||||
> not `_supervise`.
|
|
||||||
|
|
||||||
The argument: `.run_in_actor()`'s result-waiting/reaping got
|
|
||||||
hoisted into `_supervise._reap_ria_portals` (nursery-machinery
|
|
||||||
scope), which has NO natural cancel-scope to bound a parked
|
|
||||||
`wait_for_result()` — hence the awkward proc-death race +
|
|
||||||
the poll-vs-`proc_waiter` dilemma. If the result-wait instead
|
|
||||||
lived in the `to_actor` one-shot scope
|
|
||||||
(`to_actor._invoke_in_subactor()`), it would sit right next to
|
|
||||||
the caller's `an` + a local `trio` task-nursery + cancel-scope
|
|
||||||
(the `trio.to_thread`-style model #477 actually wants) — so
|
|
||||||
bounding/cancelling the wait is trivial and the hang
|
|
||||||
dissolves from correct scoping rather than a bolt-on race.
|
|
||||||
|
|
||||||
Follow-on to re-evaluate on resume:
|
|
||||||
- should `_reap_ria_portals` exist AT ALL, or should
|
|
||||||
result-waiting move entirely into
|
|
||||||
`to_actor._invoke_in_subactor()`?
|
|
||||||
- reimplement legacy `run_in_actor()` on top of
|
|
||||||
`to_actor.run()` so `_reap_ria_portals` +
|
|
||||||
`_cancel_after_result_on_exit` can be DROPPED from
|
|
||||||
`_supervise` entirely (the true #477 simplification)?
|
|
||||||
- the poll-vs-event decision is MOOT under this re-scoping.
|
|
||||||
|
|
||||||
State at pause: `test_infected_asyncio` anti-hang guard
|
|
||||||
COMMITTED (`d1fb4a1a`, intentionally red w/o the fix — the
|
|
||||||
user's failing-test-first convention). The poll-based reap
|
|
||||||
fix in `_supervise.py` is UNCOMMITTED and likely SUPERSEDED
|
|
||||||
by the re-scoping — do NOT land it as-is.
|
|
||||||
|
|
||||||
## RESOLVED (2026-07-06): migrate everything, remove the API
|
|
||||||
|
|
||||||
The PAUSED re-assessment concluded decisively: rather than
|
|
||||||
re-scope `_reap_ria_portals` (or bolt any hack onto it), the
|
|
||||||
`run_in_actor()` API itself was REMOVED — its non-blocking
|
|
||||||
"result at teardown" semantic predates streaming and confused
|
|
||||||
more than it served. Every in-repo caller was migrated
|
|
||||||
per-file/-group (each its own commit, each gated):
|
|
||||||
|
|
||||||
- tests: `test_infected_asyncio` `test_runtime` `test_rpc`
|
|
||||||
`test_spawning` `test_pubsub` `test_registrar`
|
|
||||||
`test_cancellation` (3 groups) `test_advanced_streaming`.
|
|
||||||
- examples: 4 non-debugging + all 8 `debugging/` REPL scripts
|
|
||||||
(debugger suite byte-identical green, 28p/6s).
|
|
||||||
- docs: 8 rst pages + the `experimental/_pubsub` docstring.
|
|
||||||
|
|
||||||
Migration patterns (the `run_in_actor` shape -> successor):
|
|
||||||
|
|
||||||
- blocking result -> `to_actor.run(fn, an=an, ...)`
|
|
||||||
- fire-&-forget/forever -> bg `to_actor.run()` task in a local
|
|
||||||
`trio` task-nursery (or `start_actor`
|
|
||||||
+ bg `Portal.run()` when a portal
|
|
||||||
handle is needed)
|
|
||||||
- concurrent fan-out -> N bg `to_actor.run()` tasks / or
|
|
||||||
`gather_contexts([p.open_context(..)])`
|
|
||||||
- reap-all-error-collect -> the "collect don't cancel" pattern:
|
|
||||||
each one-shot catches + stashes its
|
|
||||||
`RemoteActorError`, group raised
|
|
||||||
after the task-nursery joins (see
|
|
||||||
`examples/debugging/multi_subactors.py`)
|
|
||||||
- mutual-rendezvous -> peers must OUTLIVE both dialogs:
|
|
||||||
`start_actor()` daemons + concurrent
|
|
||||||
`Portal.run()`s + explicit
|
|
||||||
`an.cancel()` (eager one-shot reap
|
|
||||||
races the slower peer's dial of the
|
|
||||||
winner's dead sockaddr; found via
|
|
||||||
`test_trynamic_trio` flake).
|
|
||||||
|
|
||||||
Semantic deltas (tests loosened accordingly):
|
|
||||||
|
|
||||||
- teardown-reap-all BEG-of-N is GONE: local task-nurseries are
|
|
||||||
cancel-on-first, raced siblings' `Cancelled`s are absorbed,
|
|
||||||
and the runtime's `collapse_eg()` unwraps every single-member
|
|
||||||
group at each actor boundary — a fully-raced nested tree
|
|
||||||
relays a bare (annotated) `RemoteActorError` chain.
|
|
||||||
- `test_multierror_fast_nursery`'s obsolete BEG-of-25 assertion
|
|
||||||
deleted; `test_concurrent_start_error_reaps_all` retains its
|
|
||||||
high-fan-out startup/cancel/reap stress under caller-scoped
|
|
||||||
semantics.
|
|
||||||
- `test_nested_multierrors` re-purposed separately as deep-tree
|
|
||||||
cancel-cascade stress w/ a race-tolerant shape walk.
|
|
||||||
|
|
||||||
Final excision (after zero callers remained): `run_in_actor()`,
|
|
||||||
`._cancel_after_result_on_exit`, `_reap_ria_portals()`,
|
|
||||||
`Portal._submit_for_result/._expect_result_ctx/
|
|
||||||
.wait_for_result()/.result()`, `exhaust_portal()`,
|
|
||||||
`cancel_on_completion()`, `NoResult` — net -402 lines. The
|
|
||||||
reap-hang class (unbounded `wait_for_result` in machinery
|
|
||||||
scope) dissolves structurally: the only result-wait left lives
|
|
||||||
in the caller's task inside its own cancel-scope; the
|
|
||||||
`d1fb4a1a` anti-hang guard test passes by construction. The
|
|
||||||
poll-vs-`proc_waiter` debate is moot as predicted.
|
|
||||||
|
|
||||||
## Follow-up sketch: `to_actor.open_one_shot()` (run-async parity)
|
|
||||||
|
|
||||||
If deferred-result parity is ever wanted, the design that needs
|
|
||||||
NO runtime coupling, NO returned `Portal` and NO cancel-relay
|
|
||||||
`trio.Event` machinery:
|
|
||||||
|
|
||||||
async with to_actor.open_one_shot(
|
|
||||||
fn, an=an, **kws,
|
|
||||||
) as one_shot:
|
|
||||||
... # concurrent caller work
|
|
||||||
val = await one_shot.wait() # optional; errors always
|
|
||||||
# propagate at scope exit
|
|
||||||
|
|
||||||
an `@acm` that opens a private task-nursery, `start_soon`s ONE
|
|
||||||
task running the existing blocking `run()` and stashes the
|
|
||||||
value in a slot + sets a done-`trio.Event` (a memo, not a
|
|
||||||
cancel relay). Cancellation = plain scope-cancel of the acm's
|
|
||||||
nursery (the parked `Portal.run()` unwinds via `Cancelled`, the
|
|
||||||
shielded `cancel_actor()` reap still runs); a child error
|
|
||||||
raises into the acm scope so an un-`wait()`ed one-shot can
|
|
||||||
never silently drop its error. i.e. the old reaper's job is
|
|
||||||
done by scoping, not machinery. ~40 lines, all in
|
|
||||||
`to_actor/_api.py`, zero `_supervise` involvement.
|
|
||||||
|
|
||||||
## Verification gate
|
|
||||||
|
|
||||||
- per-migration-commit module gates on `trio` (+ `mp_spawn`
|
|
||||||
spot-gates incl. `test_infected_asyncio` per the B2 lesson);
|
|
||||||
`tests/devx/test_debugger.py` for the REPL flows.
|
|
||||||
- full suite on `trio` + `mp_spawn` at branch tip + CI matrix
|
|
||||||
via draft PR #484.
|
|
||||||
|
|
@ -1,79 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
session: f6c84722-471a-4458-9a80-e453fea9029f
|
|
||||||
timestamp: 2026-07-02T15:42:55Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260702T154255Z_65bf9df5_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Driver prompt file `ai/prompt-io/prompts/issue_477.md`:
|
|
||||||
|
|
||||||
> attempt to resolve
|
|
||||||
> https://github.com/goodboy/tractor/issues/477
|
|
||||||
> do it with /open-wkt.
|
|
||||||
|
|
||||||
(plus a hard stop-for-human-review deadline of 12:50PM
|
|
||||||
EST the same day)
|
|
||||||
|
|
||||||
Issue #477 asks to factor `ActorNursery.run_in_actor()`
|
|
||||||
(and possibly `Portal.run()`) out of the nursery
|
|
||||||
internals into a new `tractor.to_actor` wrapper
|
|
||||||
subpackage of "higher level one shot" single-remote-task
|
|
||||||
APIs, adopting the `trio.to_thread`/`anyio.to_process`
|
|
||||||
parlance, so that error collection/propagation moves up
|
|
||||||
into the caller's local `trio` scope and the nursery's
|
|
||||||
spawn machinery can eventually drop the
|
|
||||||
`._ria_nursery` coupling.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
First-cut `tractor.to_actor` subpkg delivering the
|
|
||||||
one-shot API composed purely from the existing
|
|
||||||
daemon-spawn + portal primitives (`start_actor()` +
|
|
||||||
`Portal.run()` + `Portal.cancel_actor()`), leaving the
|
|
||||||
legacy `.run_in_actor()` machinery untouched (formal
|
|
||||||
deprecation deferred until in-repo usage migrates):
|
|
||||||
|
|
||||||
- `to_actor.run(fn, **fn_kwargs) -> Any`: spawn a
|
|
||||||
subactor, schedule `fn` as its lone remote task, wait
|
|
||||||
on and return its result, ALWAYS reaping the subactor
|
|
||||||
(shield-safe `finally`). Remote errors raise in the
|
|
||||||
caller's task as boxed `RemoteActorError`s.
|
|
||||||
- placement variants: `portal=` reuses a running actor
|
|
||||||
(no spawn/reap), `an=` spawns from a caller-managed
|
|
||||||
actor-nursery, neither opens a call-scoped private
|
|
||||||
`open_nursery()` (implicitly booting the runtime,
|
|
||||||
configurable via `runtime_kwargs`).
|
|
||||||
- fail-fast validation before any spawn: non-streaming
|
|
||||||
async fn required; `portal=`/`an=` mutually
|
|
||||||
exclusive; `runtime_kwargs` rejected alongside any
|
|
||||||
placement opt.
|
|
||||||
- `run_in_actor()` TODO/docstring now cross-reference
|
|
||||||
the successor API.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/to_actor/__init__.py` — new subpkg,
|
|
||||||
re-exports `run`
|
|
||||||
- `tractor/to_actor/_api.py` — `run()` +
|
|
||||||
`_invoke_in_subactor()` + `_validate_one_shot_fn()`
|
|
||||||
- `tractor/__init__.py` — top-level `to_actor`
|
|
||||||
re-export
|
|
||||||
- `tractor/runtime/_supervise.py` — comment/docstring
|
|
||||||
pointers from `run_in_actor()` to the successor
|
|
||||||
- `tests/test_to_actor.py` — 11-test suite covering
|
|
||||||
all placement variants, error relay, the concurrent
|
|
||||||
worker-pool-ish pattern and arg validation
|
|
||||||
- `examples/parallelism/to_actor_one_shots.py` —
|
|
||||||
runnable demo (auto-collected by
|
|
||||||
`test_docs_examples.py`)
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None yet — pending human review (work paused before the
|
|
||||||
12:50PM EST deadline per the driver prompt).
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-07-02T15:42:55Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
diff_cmd: git diff main..wkt/to_actor_subpkg
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw AI output (diff-ref mode)
|
|
||||||
|
|
||||||
All generated code is committed on the
|
|
||||||
`wkt/to_actor_subpkg` branch; per diff-ref mode each
|
|
||||||
file's verbatim content is reachable via the pointers
|
|
||||||
below rather than duplicated here.
|
|
||||||
|
|
||||||
## Generated files
|
|
||||||
|
|
||||||
> `git diff main..wkt/to_actor_subpkg -- tractor/to_actor/__init__.py`
|
|
||||||
|
|
||||||
New subpackage init: module docstring establishing the
|
|
||||||
`trio.to_thread`/`anyio.to_process` "run it over there"
|
|
||||||
parlance for actors, plus the single public re-export
|
|
||||||
`run as run` from `._api`.
|
|
||||||
|
|
||||||
> `git diff main..wkt/to_actor_subpkg -- tractor/to_actor/_api.py`
|
|
||||||
|
|
||||||
The one-shot invocation impl, composed entirely from the
|
|
||||||
lower level daemon-spawn + portal primitives as
|
|
||||||
prescribed by issue #477:
|
|
||||||
|
|
||||||
- `_validate_one_shot_fn()`: the `Portal.run()`
|
|
||||||
non-streaming-async-fn constraint checked up-front,
|
|
||||||
before any subactor is spawned.
|
|
||||||
- `_invoke_in_subactor()`: `an.start_actor()` ->
|
|
||||||
`Portal.run()` -> always-reap via
|
|
||||||
`Portal.cancel_actor()` in a `finally` (the cancel
|
|
||||||
req's bounded wait is internally shielded so the reap
|
|
||||||
also runs under caller-scope cancellation).
|
|
||||||
- `run()`: the public API. Placement options:
|
|
||||||
`portal=` (reuse a running actor, no spawn/reap),
|
|
||||||
`an=` (spawn from a caller-managed nursery), or
|
|
||||||
neither (private `open_nursery()` scoped to the call,
|
|
||||||
implicitly booting the runtime when needed, tunable
|
|
||||||
via pass-through `runtime_kwargs`). Spawn opts mirror
|
|
||||||
`ActorNursery.start_actor()`; `**fn_kwargs` are
|
|
||||||
relayed to the remote task. Errors raise in the
|
|
||||||
caller's task as boxed `RemoteActorError`s.
|
|
||||||
`runtime_kwargs` alongside any placement opt is a
|
|
||||||
hard `ValueError`, never silently ignored.
|
|
||||||
|
|
||||||
> `git diff main..wkt/to_actor_subpkg -- tractor/__init__.py`
|
|
||||||
|
|
||||||
Top-level `from . import to_actor as to_actor`
|
|
||||||
re-export.
|
|
||||||
|
|
||||||
> `git diff main..wkt/to_actor_subpkg -- tractor/runtime/_supervise.py`
|
|
||||||
|
|
||||||
Comment/docstring-only: the `run_in_actor()` deprecation
|
|
||||||
TODO now points at the implemented `.to_actor.run()`
|
|
||||||
successor (checkbox ticked) and the method docstring
|
|
||||||
gains a NOTE steering users to the new API; remaining
|
|
||||||
TODO items are the `DeprecationWarning` emission +
|
|
||||||
in-repo usage migration.
|
|
||||||
|
|
||||||
> `git diff main..wkt/to_actor_subpkg -- tests/test_to_actor.py`
|
|
||||||
|
|
||||||
11-test suite: private-nursery one-shot, implicit
|
|
||||||
runtime boot via `runtime_kwargs`, remote-error relay to
|
|
||||||
the caller's task (bare + caller-managed nursery),
|
|
||||||
caller-nursery spawn, portal reuse w/o implicit reap,
|
|
||||||
the concurrent worker-pool-ish pattern (local `trio`
|
|
||||||
nursery x shared `an`), and the four validation
|
|
||||||
rejections (sync fn, async-gen fn, `portal`+`an`
|
|
||||||
combo, `runtime_kwargs`+placement combo).
|
|
||||||
|
|
||||||
> `git diff main..wkt/to_actor_subpkg -- examples/parallelism/to_actor_one_shots.py`
|
|
||||||
|
|
||||||
Runnable example (auto-collected by
|
|
||||||
`test_docs_examples.py`): the fully-implicit one-shot
|
|
||||||
plus the concurrent worker-pool-ish prime-check pattern
|
|
||||||
against a shared caller-managed actor-nursery.
|
|
||||||
|
|
||||||
## Test runs (verbatim)
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/test_to_actor.py .......... [100%]
|
|
||||||
============= 10 passed in 4.29s =============
|
|
||||||
```
|
|
||||||
|
|
||||||
Regression subset for touched modules
|
|
||||||
(`test_local.py test_rpc.py test_spawning.py
|
|
||||||
test_cancellation.py`):
|
|
||||||
|
|
||||||
```
|
|
||||||
38 passed, 1 xfailed, 24 warnings in 80.71s (0:01:20)
|
|
||||||
```
|
|
||||||
|
|
||||||
(warnings are pre-existing stdlib `os.fork()`
|
|
||||||
DeprecationWarnings from the mp spawn backends, not
|
|
||||||
introduced by this change)
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
session: b8ec2ffe-1e47-4db5-8bc3-5bebc5feaaf6
|
|
||||||
timestamp: 2026-07-02T15:50:06Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260702T155006Z_65bf9df5_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
From `ai/prompt-io/prompts/issue_473.md`:
|
|
||||||
|
|
||||||
> NOTE: you MUST pause this work at 12:50PM EST (BEFORE
|
|
||||||
> your weekly limit reset) for review by a human!
|
|
||||||
>
|
|
||||||
> resolve GH #473: https://github.com/goodboy/tractor/issues/474
|
|
||||||
> do it with /open-wkt.
|
|
||||||
|
|
||||||
(The prompt's issue-number/URL mismatch was resolved in
|
|
||||||
favor of #473 — file name, session name and prompt text
|
|
||||||
all say #473; the `/474` URL is a typo.)
|
|
||||||
|
|
||||||
GH #473: "Root-cause UDS-on-macOS —
|
|
||||||
`uds_transport_actor_tree` example fails on CI".
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Worked all four #473 next-step checkboxes in wkt
|
|
||||||
`uds_macos_473`:
|
|
||||||
|
|
||||||
1. **stderr surfacing**: `test_docs_examples.py` now
|
|
||||||
raises with the full subproc stderr (+stdout) on ANY
|
|
||||||
non-zero exit. The old last-line-'Error' check could
|
|
||||||
never fire for `tractor` crashes since root-actor
|
|
||||||
stderr always ends with the strict-EG collapse note
|
|
||||||
`( ^^^ this exc was collapsed from a group ^^^ )` —
|
|
||||||
proven against the real PR #460 macOS CI log (bare
|
|
||||||
`assert 1 == 0`, no traceback).
|
|
||||||
2. **root-cause (linux-provable layer)**: macOS-only
|
|
||||||
addr corruption in
|
|
||||||
`MsgpackUDSStream.get_stream_addrs()` — no
|
|
||||||
`SO_PASSCRED`/autobind on darwin means the accept
|
|
||||||
side's `getpeername()` is `''`, and the
|
|
||||||
`(str(), str())` arm took `peername` unconditionally →
|
|
||||||
`Path('')` garbage addrs on every accepted conn.
|
|
||||||
Proven + fixed via linux no-autobind simulation.
|
|
||||||
Possibly not the final macOS crasher (non-fatal on
|
|
||||||
linux-sim); the diagnostic patch guarantees the next
|
|
||||||
macOS CI run shows any remaining layer.
|
|
||||||
3. **CI matrix**: removed the `macos-latest`+`uds`
|
|
||||||
exclude.
|
|
||||||
4. **un-skip**: dropped the macOS+CI skip of the example.
|
|
||||||
|
|
||||||
Also: `start_listener()` bindspace mkdir hardened
|
|
||||||
(`parents=True, exist_ok=True`), example docstring
|
|
||||||
peer-pid mechanism corrected for macOS.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tests/test_docs_examples.py` — surface full stderr on
|
|
||||||
non-zero exit; remove macOS skip of the UDS example
|
|
||||||
- `tractor/ipc/_uds.py` — fix no-autobind
|
|
||||||
`get_stream_addrs()` arm; harden bindspace mkdir;
|
|
||||||
document autobind semantics
|
|
||||||
- `.github/workflows/ci.yml` — add macOS UDS leg (remove
|
|
||||||
matrix exclude)
|
|
||||||
- `examples/uds_transport_actor_tree.py` — platform-
|
|
||||||
correct peer-pid docs
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None yet — pending human review (hard-stop review
|
|
||||||
checkpoint at 12:50PM EST per prompt); commit staging and
|
|
||||||
any edits are the human's.
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-07-02T15:50:06Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
diff_cmd: git diff main..wkt/uds_macos_473
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw output — GH #473 UDS-on-macOS root-cause session
|
|
||||||
|
|
||||||
NOTE: code output is diff-referenced (not copied) per
|
|
||||||
prompt-io diff-ref mode; all generated code is on branch
|
|
||||||
`wkt/uds_macos_473` relative to `main` (65bf9df5).
|
|
||||||
|
|
||||||
## Diagnostic narrative (verbatim)
|
|
||||||
|
|
||||||
Root-cause work proceeded by linux-side simulation since no
|
|
||||||
macOS host is available:
|
|
||||||
|
|
||||||
1. Pulled the actual macOS CI failure log from PR #460 run
|
|
||||||
28343878933 (job 84115107992): the failure is a bare
|
|
||||||
`assert 1 == 0` on `proc.returncode` with NO stderr
|
|
||||||
surfaced — the harness only re-raised when the LAST
|
|
||||||
stderr line contained 'Error'.
|
|
||||||
|
|
||||||
2. Reproduced the stderr-swallow mechanism on linux: any
|
|
||||||
`tractor` root-actor crash ends stderr with the
|
|
||||||
strict-EG collapse note
|
|
||||||
`( ^^^ this exc was collapsed from a group ^^^ )` which
|
|
||||||
never matches 'Error' — so EVERY possible crash was
|
|
||||||
swallowed. (Verified by sabotaging the runtime dir via
|
|
||||||
an over-long `XDG_RUNTIME_DIR` → `OSError: AF_UNIX path
|
|
||||||
too long` → rc=1 + swallowed.)
|
|
||||||
|
|
||||||
3. Found + proved a macOS-only addr-corruption bug in
|
|
||||||
`MsgpackUDSStream.get_stream_addrs()`: the
|
|
||||||
`(str(), str())` match-arm unconditionally took
|
|
||||||
`peername`, but on no-autobind platforms (macOS lacks
|
|
||||||
linux's `SO_PASSCRED`-triggered autobind) the accept
|
|
||||||
side's `getpeername()` is `''` → `Path('')` garbage
|
|
||||||
laddr/raddr on EVERY accepted UDS conn. Simulated on
|
|
||||||
linux by nulling `SO_PASSCRED` (no autobind → same `''`
|
|
||||||
shape): pre-fix the example printed
|
|
||||||
`listener sock file: .`; post-fix it prints the real
|
|
||||||
registry sockpath. Non-fatal on linux-sim (rc=0), so
|
|
||||||
possibly not the final macOS crasher — the diagnostic
|
|
||||||
patch guarantees the next macOS CI run reveals any
|
|
||||||
remaining layer.
|
|
||||||
|
|
||||||
4. Falsified the missing-parent-dir theory:
|
|
||||||
`get_rt_dir()` already `mkdir(parents=True,
|
|
||||||
exist_ok=True)`s at import (and macOS TCP CI passes),
|
|
||||||
so `~/Library/Caches/TemporaryItems` absence cannot be
|
|
||||||
the crasher. Hardened `start_listener()`'s bindspace
|
|
||||||
mkdir anyway (custom `filedir` case + racing actors).
|
|
||||||
|
|
||||||
## Generated changes (diff pointers)
|
|
||||||
|
|
||||||
> `git diff main..wkt/uds_macos_473 -- tests/test_docs_examples.py`
|
|
||||||
|
|
||||||
- always raise with FULL subproc stderr (+stdout) on any
|
|
||||||
non-zero example exit; keep legacy last-line 'Error'
|
|
||||||
check for zero-rc cases; drop the macOS+CI skip of
|
|
||||||
`uds_transport_actor_tree.py` (GH #473 next-step).
|
|
||||||
|
|
||||||
> `git diff main..wkt/uds_macos_473 -- tractor/ipc/_uds.py`
|
|
||||||
|
|
||||||
- `get_stream_addrs()`: document the autobind semantics
|
|
||||||
(bytes = linux abstract-ns autobind artifact), add
|
|
||||||
no-autobind `(str, str)` arm picking the non-empty name
|
|
||||||
(`peername` connect-side, `sockname` accept-side) with
|
|
||||||
an empty-pair `ValueError` guard.
|
|
||||||
- `start_listener()`: `bs.mkdir(parents=True,
|
|
||||||
exist_ok=True)`.
|
|
||||||
|
|
||||||
> `git diff main..wkt/uds_macos_473 -- .github/workflows/ci.yml`
|
|
||||||
|
|
||||||
- remove the `macos-latest`+`uds` matrix exclude so
|
|
||||||
UDS-on-macOS is exercised by CI (GH #473 next-step).
|
|
||||||
|
|
||||||
> `git diff main..wkt/uds_macos_473 -- examples/uds_transport_actor_tree.py`
|
|
||||||
|
|
||||||
- docs nit: peer-pid mechanism is `SO_PEERCRED` on linux,
|
|
||||||
`LOCAL_PEERPID` on macOS.
|
|
||||||
|
|
||||||
## Verification (verbatim summary)
|
|
||||||
|
|
||||||
- macOS-shape sim (no autobind) + fix: example rc=0 with
|
|
||||||
correct listener sockpath.
|
|
||||||
- native linux post-fix: example rc=0, autobind arms
|
|
||||||
unchanged.
|
|
||||||
- sabotage smoke-test: diagnostic patch surfaces the full
|
|
||||||
EG traceback incl. collapse-note line.
|
|
||||||
- `pytest tests/ipc/ tests/test_2way.py tests/discovery/
|
|
||||||
--tpt-proto uds`: 61 passed, 1 `TooSlowError` flake
|
|
||||||
(test_simple_rpc) that passes solo in 2.2s — attributed
|
|
||||||
to a concurrent full-suite run (other session) loading
|
|
||||||
the host.
|
|
||||||
- full `tests/test_docs_examples.py` run pending at
|
|
||||||
raw-file write time (queued behind the concurrent
|
|
||||||
session's suite).
|
|
||||||
|
|
@ -1,83 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
session: b6b42e23-0454-4d9b-be1c-0a89d65aed1b
|
|
||||||
timestamp: 2026-07-02T15:56:26Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260702T155626Z_65bf9df5_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
From `ai/prompt-io/prompts/issue_470.md`:
|
|
||||||
|
|
||||||
> attempt to resolve
|
|
||||||
> https://github.com/goodboy/tractor/issues/470
|
|
||||||
> do it with /open-wkt, ensure you /run-tests on all
|
|
||||||
> changes
|
|
||||||
|
|
||||||
(plus a hard pause-for-human-review deadline of
|
|
||||||
12:50PM EST.)
|
|
||||||
|
|
||||||
Issue #470 asks to trim the ~0.42s `import tractor`
|
|
||||||
cost — which dominates per-actor spawn latency on the
|
|
||||||
`trio` backend — by lazy-importing heavy/optional deps
|
|
||||||
(`pdbp`, `stackscope`, `multiaddr`, + audit of
|
|
||||||
`colorlog`/`bidict`/`wrapt`/`setproctitle`).
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Profiling showed the issue's dep-list only accounted
|
|
||||||
for ~20ms; the dominant cost (~244ms) was
|
|
||||||
`log.get_logger()`'s `get_caller_mod()` calling
|
|
||||||
`inspect.stack()` at module level in ~39 modules —
|
|
||||||
each call walks every stack frame (deep during nested
|
|
||||||
imports) and scans `sys.modules` per frame via
|
|
||||||
`inspect.getmodule()`.
|
|
||||||
|
|
||||||
Changes, in impact order:
|
|
||||||
|
|
||||||
1. `get_caller_mod()` -> `sys._getframe()` +
|
|
||||||
`f_globals['__name__']` `sys.modules` lookup
|
|
||||||
(~240ms saved).
|
|
||||||
2. Issue's lazy-import checklist: `bidict`,
|
|
||||||
`multiaddr`, `colorlog`, `wrapt` moved to
|
|
||||||
`TYPE_CHECKING`/function-local imports;
|
|
||||||
`platformdirs` function-local; `asyncio` +
|
|
||||||
`.to_asyncio` deferred out of the `devx.debug` +
|
|
||||||
`spawn._entry` eager paths (~15ms saved).
|
|
||||||
3. PEP 562 `__getattr__` on `tractor/__init__.py`
|
|
||||||
preserving public `tractor.to_asyncio` attr access.
|
|
||||||
|
|
||||||
Results: `import tractor` 0.42s -> ~0.145s (~65%);
|
|
||||||
sequential `start_actor` latency 0.40-0.44s ->
|
|
||||||
~0.179s/actor. `pdbp` (needs `_repl.py` class-base
|
|
||||||
restructure) + `platformdirs` (needs
|
|
||||||
`UDSAddress.def_bindspace` protocol rework) documented
|
|
||||||
as follow-ups.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/log.py` — `get_caller_mod()` perf fix +
|
|
||||||
lazy `colorlog`
|
|
||||||
- `tractor/__init__.py` — PEP 562 lazy `to_asyncio`
|
|
||||||
- `tractor/discovery/_addr.py` — `bidict` ->
|
|
||||||
`TYPE_CHECKING`
|
|
||||||
- `tractor/discovery/_multiaddr.py` — lazy `multiaddr`
|
|
||||||
- `tractor/ipc/_tcp.py`, `tractor/ipc/_uds.py` —
|
|
||||||
`Multiaddr` -> `TYPE_CHECKING`
|
|
||||||
- `tractor/runtime/_state.py` — lazy `platformdirs`
|
|
||||||
- `tractor/devx/_frame_stack.py` — lazy `pdbp` +
|
|
||||||
`wrapt`
|
|
||||||
- `tractor/devx/debug/_trace.py`,
|
|
||||||
`tractor/devx/debug/_tty_lock.py` — lazy `asyncio` +
|
|
||||||
`.to_asyncio`
|
|
||||||
- `tractor/spawn/_entry.py` — lazy
|
|
||||||
`run_as_asyncio_guest`
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None yet — pending user review at the 12:50PM EST
|
|
||||||
pause gate (test-suite results reported in-session).
|
|
||||||
|
|
@ -1,122 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-07-02T15:56:26Z
|
|
||||||
git_ref: 65bf9df5
|
|
||||||
diff_cmd: git diff main..wkt/boot_latency_470
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw AI output — gh #470 `import tractor` latency trim
|
|
||||||
|
|
||||||
All generated code is committed on the
|
|
||||||
`wkt/boot_latency_470` branch; per diff-ref mode each
|
|
||||||
file's content is referenced via its diff instead of
|
|
||||||
copied verbatim.
|
|
||||||
|
|
||||||
## Profiling findings (verbatim analysis output)
|
|
||||||
|
|
||||||
Baseline: `import tractor` ~0.39-0.42s wall.
|
|
||||||
|
|
||||||
`python -X importtime` + `cProfile` traced the cost NOT
|
|
||||||
primarily to third-party deps (the issue's hypothesis)
|
|
||||||
but to `tractor/log.py:get_logger()` calling
|
|
||||||
`get_caller_mod()` -> `inspect.stack()` at module level
|
|
||||||
in ~39 tractor modules:
|
|
||||||
|
|
||||||
- `inspect.stack()` builds `FrameInfo` (incl. src-file
|
|
||||||
and line-context resolution) for EVERY frame on the
|
|
||||||
stack; during nested imports the stack is dozens of
|
|
||||||
importlib frames deep.
|
|
||||||
- each `FrameInfo` resolution calls
|
|
||||||
`inspect.getmodule()` which scans all of
|
|
||||||
`sys.modules` per frame (1.4M `ismodule()` calls in
|
|
||||||
one profiled import).
|
|
||||||
- aggregate: ~244ms of tractor-own module "self" time
|
|
||||||
vs ~20ms for ALL the issue-listed third-party deps
|
|
||||||
(`pdbp` ~10ms, `bidict` ~4.5ms, `multiaddr` ~3.5ms,
|
|
||||||
`wrapt`/`colorlog` ~1ms each); `trio` itself is
|
|
||||||
~70-100ms and unavoidable.
|
|
||||||
|
|
||||||
## Generated changes
|
|
||||||
|
|
||||||
> `git diff main..wkt/boot_latency_470 -- tractor/log.py`
|
|
||||||
|
|
||||||
`get_caller_mod()` rewritten from `inspect.stack()` +
|
|
||||||
`inspect.getmodule()` to `sys._getframe(frames_up)` +
|
|
||||||
`frame.f_globals['__name__']` -> `sys.modules` lookup
|
|
||||||
(O(1) vs O(stack x sys.modules)). Unused `inspect`
|
|
||||||
imports dropped; `FrameType` imported from `types`.
|
|
||||||
Also `colorlog` lazy-imported inside
|
|
||||||
`get_console_log()`.
|
|
||||||
|
|
||||||
> `git diff main..wkt/boot_latency_470 -- tractor/discovery/_addr.py`
|
|
||||||
|
|
||||||
`bidict` import moved under `TYPE_CHECKING`
|
|
||||||
(annotation-only use; `_address_types` is a plain dict
|
|
||||||
literal).
|
|
||||||
|
|
||||||
> `git diff main..wkt/boot_latency_470 -- tractor/discovery/_multiaddr.py`
|
|
||||||
|
|
||||||
`from __future__ import annotations` added; `multiaddr`
|
|
||||||
import moved under `TYPE_CHECKING` + function-local
|
|
||||||
imports in `mk_maddr()`/`parse_maddr()`.
|
|
||||||
|
|
||||||
> `git diff main..wkt/boot_latency_470 -- tractor/ipc/_tcp.py tractor/ipc/_uds.py`
|
|
||||||
|
|
||||||
`Multiaddr` imports moved under `TYPE_CHECKING`
|
|
||||||
(annotation-only in both transports).
|
|
||||||
|
|
||||||
> `git diff main..wkt/boot_latency_470 -- tractor/runtime/_state.py`
|
|
||||||
|
|
||||||
`platformdirs` lazy-imported inside `get_rt_dir()`
|
|
||||||
(NOTE: still imported eagerly via
|
|
||||||
`UDSAddress.def_bindspace` class-var eval; see
|
|
||||||
follow-ups).
|
|
||||||
|
|
||||||
> `git diff main..wkt/boot_latency_470 -- tractor/devx/_frame_stack.py`
|
|
||||||
|
|
||||||
`pdbp` + `wrapt` lazy-imported inside
|
|
||||||
`hide_runtime_frames()` / `api_frame()` respectively.
|
|
||||||
|
|
||||||
> `git diff main..wkt/boot_latency_470 -- tractor/devx/debug/_trace.py tractor/devx/debug/_tty_lock.py`
|
|
||||||
|
|
||||||
`asyncio` moved to `TYPE_CHECKING` + call-site local
|
|
||||||
imports (`asyncio.current_task()` sites);
|
|
||||||
`tractor.to_asyncio.run_trio_task_in_future` imports
|
|
||||||
moved into the infected-aio runtime branches.
|
|
||||||
|
|
||||||
> `git diff main..wkt/boot_latency_470 -- tractor/spawn/_entry.py`
|
|
||||||
|
|
||||||
`run_as_asyncio_guest` import moved into the
|
|
||||||
`infect_asyncio=True` branches of `_mp_main()` /
|
|
||||||
`_trio_main()`.
|
|
||||||
|
|
||||||
> `git diff main..wkt/boot_latency_470 -- tractor/__init__.py`
|
|
||||||
|
|
||||||
PEP 562 module `__getattr__` added so
|
|
||||||
`tractor.to_asyncio` attr-access still works (required
|
|
||||||
by `tests/test_child_manages_service_nursery.py` and
|
|
||||||
any downstream user) while keeping `asyncio` off the
|
|
||||||
eager import path.
|
|
||||||
|
|
||||||
## Measured results (verbatim)
|
|
||||||
|
|
||||||
- `import tractor`: 0.39-0.42s -> ~0.145s (~65% cut)
|
|
||||||
- `start_actor` spawn+boot+reg+cancel: ~0.40-0.44s ->
|
|
||||||
~0.179s/actor (n=5 sequential, warm parent)
|
|
||||||
- post-change eager-module check: only `pdbp` +
|
|
||||||
`platformdirs` of the issue's list remain eager.
|
|
||||||
|
|
||||||
## Known follow-ups (not implemented, deadline-bound)
|
|
||||||
|
|
||||||
- `pdbp` (~10ms): still eager via
|
|
||||||
`devx/debug/_repl.py` class bases
|
|
||||||
(`class PdbREPL(pdbp.Pdb)`) + `_tty_lock.py`
|
|
||||||
module-level `@pdbp.hideframe`; needs `_repl`
|
|
||||||
restructure + PEP 562 in `devx.debug.__init__`.
|
|
||||||
- `platformdirs` (~1.5ms): eager via
|
|
||||||
`UDSAddress.def_bindspace: ClassVar = get_rt_dir()`
|
|
||||||
class-body call; needs `Address`-protocol rework of
|
|
||||||
`def_bindspace` to a lazy accessor.
|
|
||||||
- `stackscope` + `setproctitle`: already lazy/absent —
|
|
||||||
no change needed.
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
session: f6c84722-471a-4458-9a80-e453fea9029f
|
|
||||||
timestamp: 2026-07-02T16:58:06Z
|
|
||||||
git_ref: a34aaf98
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260702T165806Z_a34aaf98_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Follow-up round in the same session as the
|
|
||||||
`tractor.to_actor` landing (see
|
|
||||||
`20260702T154255Z_65bf9df5_prompt_io.md`). After
|
|
||||||
committing that work the user green-lit the deferred
|
|
||||||
items:
|
|
||||||
|
|
||||||
> go go go on this with what time you have left, in
|
|
||||||
> particular see if you can get the _ria_nursery
|
|
||||||
> removal going!
|
|
||||||
|
|
||||||
then extended the deadline twice to iterate on the
|
|
||||||
discovered hang:
|
|
||||||
|
|
||||||
> continue on this up until a 12:58:30 deadline
|
|
||||||
|
|
||||||
and finally chose "Commit step A now" from the
|
|
||||||
next-steps prompt.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Step A of the `._ria_nursery` removal (issue #477): hoist
|
|
||||||
`.run_in_actor()` result-reaping out of the spawn
|
|
||||||
backends into the `ActorNursery` machinery so ria
|
|
||||||
children spawn via the default daemon nursery,
|
|
||||||
|
|
||||||
- new `_supervise._reap_ria_portals()` runs one
|
|
||||||
`_spawn.cancel_on_completion()` task per ria child
|
|
||||||
AFTER `._join_procs` is set; happy path awaits it
|
|
||||||
right after `._join_procs.set()`.
|
|
||||||
- error path SEQUENCES: snapshot ria
|
|
||||||
`(portal, subactor)` pairs -> `await an.cancel()` ->
|
|
||||||
0.5s-bounded reap. Two failed intermediates informed
|
|
||||||
this: a concurrent reap+cancel DEADLOCKED
|
|
||||||
`test_multierror`; a 3s bound blew
|
|
||||||
`test_cancel_while_childs_child_in_sync_sleep`'s
|
|
||||||
`fail_after` deadline.
|
|
||||||
- backends (`spawn/_trio.py`, `spawn/_mp.py`) lose the
|
|
||||||
`._cancel_after_result_on_exit` membership branch,
|
|
||||||
per-child reaper nursery + dead imports.
|
|
||||||
- design/probe-history doc:
|
|
||||||
`ai/conc-anal/ria_nursery_removal_plan.md` (from an
|
|
||||||
agent-verified machinery map).
|
|
||||||
|
|
||||||
Verification: `test_cancellation.py` fully green
|
|
||||||
(20 passed, 1 xfailed) incl. the previously-hung
|
|
||||||
`test_multierror`; `test_to_actor`+`test_spawning`
|
|
||||||
20/20; bounded full-suite gate SIGINT'd ~30s early at
|
|
||||||
303 passed / 0 failures (user opted to commit on that
|
|
||||||
signal, deferring the unbounded re-run to step-B
|
|
||||||
verification).
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_supervise.py` — `_reap_ria_portals()`
|
|
||||||
+ two call-sites; `run_in_actor()` off the ria nursery
|
|
||||||
- `tractor/spawn/_trio.py` — reaper branch + import drop
|
|
||||||
- `tractor/spawn/_mp.py` — same as `_trio.py`
|
|
||||||
- `ai/conc-anal/ria_nursery_removal_plan.md` — plan +
|
|
||||||
probe history
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None yet — committed via the drafted
|
|
||||||
`.claude/git_commit_msg_ria_step_a.md` (user-driven
|
|
||||||
`git commit --edit`).
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-07-02T16:58:06Z
|
|
||||||
git_ref: a34aaf98
|
|
||||||
diff_cmd: git diff a34aaf98..wkt/to_actor_subpkg
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw AI output (diff-ref mode)
|
|
||||||
|
|
||||||
Step-A code is committed on `wkt/to_actor_subpkg`
|
|
||||||
directly after `a34aaf98`; per diff-ref mode the verbatim
|
|
||||||
content is reachable via the pointers below.
|
|
||||||
|
|
||||||
## Generated files
|
|
||||||
|
|
||||||
> `git diff a34aaf98..wkt/to_actor_subpkg -- tractor/runtime/_supervise.py`
|
|
||||||
|
|
||||||
New `_reap_ria_portals(an, errors, ria_children=None)`
|
|
||||||
helper (one `_spawn.cancel_on_completion()` task per ria
|
|
||||||
child under `collapse_eg()` + a local nursery);
|
|
||||||
`run_in_actor()` drops `nursery=self._ria_nursery`; happy
|
|
||||||
path awaits the reap right after `._join_procs.set()`;
|
|
||||||
inner error handler snapshots ria pairs, runs
|
|
||||||
`await an.cancel()` then a `move_on_after(0.5)`-bounded
|
|
||||||
reap over the snapshot.
|
|
||||||
|
|
||||||
> `git diff a34aaf98..wkt/to_actor_subpkg -- tractor/spawn/_trio.py`
|
|
||||||
> `git diff a34aaf98..wkt/to_actor_subpkg -- tractor/spawn/_mp.py`
|
|
||||||
|
|
||||||
Both backends: the post-`_join_procs` block collapses to
|
|
||||||
a bare `soft_kill()` (membership branch, per-child reaper
|
|
||||||
nursery, reaper-cancel logging and the now-unused
|
|
||||||
`cancel_on_completion` imports all removed).
|
|
||||||
|
|
||||||
> `git diff a34aaf98..wkt/to_actor_subpkg -- ai/conc-anal/ria_nursery_removal_plan.md`
|
|
||||||
|
|
||||||
Agent-verified machinery map, 3-step design (A/B/C),
|
|
||||||
probe history (deadlock -> sequencing fix -> bound
|
|
||||||
tightening) and risk register.
|
|
||||||
|
|
||||||
## Test runs (verbatim)
|
|
||||||
|
|
||||||
```
|
|
||||||
tests/test_cancellation.py: 20 passed, 1 xfailed in 77.28s
|
|
||||||
tests/test_to_actor.py + tests/test_spawning.py: 20 passed
|
|
||||||
full-suite (SIGINT at 540s bound, ~30s short):
|
|
||||||
303 passed, 9 skipped, 1 xfailed, 2 xpassed in 539.42s
|
|
||||||
```
|
|
||||||
|
|
||||||
Failed intermediates (not in final diff): concurrent
|
|
||||||
reap+`an.cancel()` deadlocked `test_multierror`; a 3s
|
|
||||||
reap bound blew
|
|
||||||
`test_cancel_while_childs_child_in_sync_sleep`'s
|
|
||||||
`fail_after` deadline.
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-8
|
|
||||||
service: claude
|
|
||||||
session: c7860302-6a33-4d92-821c-0807d68be464
|
|
||||||
timestamp: 2026-07-02T17:22:33Z
|
|
||||||
git_ref: 5cd190c5
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260702T172233Z_5cd190c5_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Branched session (`to_actor_subpkg_POST_WEEKLY_CUTOFF_2OPUS`,
|
|
||||||
now on Opus 4.8) continuing the `._ria_nursery` removal after
|
|
||||||
step A committed + pushed (`5cd190c5`/`99310269`):
|
|
||||||
|
|
||||||
> ok continue where your superior left off please
|
|
||||||
|
|
||||||
then, after the commit-split discussion:
|
|
||||||
|
|
||||||
> yup committed and pushed so take a look ma fren.
|
|
||||||
|
|
||||||
i.e. green-lighting step-B prep against the committed step-A
|
|
||||||
tree.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Step B of the `._ria_nursery` removal (issue #477): delete
|
|
||||||
the now-vestigial secondary nursery,
|
|
||||||
|
|
||||||
- collapse the inner `async with (collapse_eg(),
|
|
||||||
trio.open_nursery() as ria_nursery)` layer in
|
|
||||||
`_open_and_supervise_one_cancels_all_nursery` — `da_nursery`
|
|
||||||
becomes the single nursery for ALL subactors.
|
|
||||||
- `ActorNursery.__init__` drops the `ria_nursery` param + the
|
|
||||||
`self._ria_nursery` attr; `start_actor()` drops its
|
|
||||||
`nursery=` escape-hatch param.
|
|
||||||
- `._cancel_after_result_on_exit` kept (ria-child
|
|
||||||
discriminator for `_reap_ria_portals()`).
|
|
||||||
|
|
||||||
Verified behavior-preserving via a first-principles argument
|
|
||||||
(zero-task nursery = a bare checkpoint) + the targeted gate
|
|
||||||
(`test_cancellation test_spawning test_local test_rpc
|
|
||||||
test_to_actor` = 49 passed, 1 xfailed on trio). The two
|
|
||||||
error handlers were deliberately NOT merged — that changes
|
|
||||||
propagation semantics and is deferred to its own PR (TODO
|
|
||||||
left at the outer `except`).
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_supervise.py` — collapse the ria nursery
|
|
||||||
layer + drop the ctor/`start_actor` params + refresh the
|
|
||||||
now-stale nursery comments
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None yet — committed via the drafted
|
|
||||||
`.claude/git_commit_msg_ria_step_b.md` (user-driven
|
|
||||||
`git commit --edit`).
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-8
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-07-02T17:22:33Z
|
|
||||||
git_ref: 5cd190c5
|
|
||||||
diff_cmd: git diff 5cd190c5..wkt/to_actor_subpkg
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw AI output (diff-ref mode)
|
|
||||||
|
|
||||||
Step-B code lives on `wkt/to_actor_subpkg` after `5cd190c5`;
|
|
||||||
per diff-ref mode the verbatim content is reachable via the
|
|
||||||
pointer below.
|
|
||||||
|
|
||||||
## Generated files
|
|
||||||
|
|
||||||
> `git diff 5cd190c5..wkt/to_actor_subpkg -- tractor/runtime/_supervise.py`
|
|
||||||
|
|
||||||
- `ActorNursery.__init__`: `ria_nursery` param removed;
|
|
||||||
`self._ria_nursery = ria_nursery` block deleted;
|
|
||||||
`_cancel_after_result_on_exit` comment refreshed.
|
|
||||||
- `start_actor()`: `nursery=` param removed; body uses
|
|
||||||
`self._da_nursery.start(...)` directly.
|
|
||||||
- `_open_and_supervise_one_cancels_all_nursery()`: the inner
|
|
||||||
`async with (collapse_eg(), trio.open_nursery() as
|
|
||||||
ria_nursery)` layer removed; `an = ActorNursery(actor,
|
|
||||||
da_nursery, errors)` constructed once under the single
|
|
||||||
`da_nursery`; the inner-try body de-indented one level;
|
|
||||||
both error handlers retained; the da-nursery lead comment
|
|
||||||
and the outer-`except` TODO refreshed to describe the
|
|
||||||
single-nursery reality + flag the (deferred) handler-merge.
|
|
||||||
|
|
||||||
> `git diff 5cd190c5..wkt/to_actor_subpkg -- ai/conc-anal/ria_nursery_removal_plan.md`
|
|
||||||
|
|
||||||
Added a "Step-B outcome" section (collapse rationale,
|
|
||||||
handler-merge deferral, safety argument, gate result).
|
|
||||||
|
|
||||||
## Test runs (verbatim)
|
|
||||||
|
|
||||||
```
|
|
||||||
targeted gate (trio):
|
|
||||||
tests/test_cancellation.py tests/test_spawning.py
|
|
||||||
tests/test_local.py tests/test_rpc.py tests/test_to_actor.py
|
|
||||||
-> 49 passed, 1 xfailed in 88.62s
|
|
||||||
|
|
||||||
signature checks:
|
|
||||||
ActorNursery.__init__ params: ['self', 'actor', 'da_nursery', 'errors']
|
|
||||||
start_actor has nursery param: False
|
|
||||||
|
|
||||||
full-suite gate: in progress (see stepB_full.log)
|
|
||||||
```
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-8
|
|
||||||
service: claude
|
|
||||||
session: c7860302-6a33-4d92-821c-0807d68be464
|
|
||||||
timestamp: 2026-07-02T22:25:44Z
|
|
||||||
git_ref: 9201a2ed
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260702T222544Z_9201a2ed_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continuing on branch `drop_ria_nursery` after step B
|
|
||||||
(`9201a2ed`). User green-lit the deferred error-handler merge:
|
|
||||||
|
|
||||||
> yes, i checked us out on a new branch for the ria-tn removal;
|
|
||||||
> work off the new wkt branch drop_ria_nursery.
|
|
||||||
|
|
||||||
and (re the migration) "nope, later we'll do this", plus:
|
|
||||||
|
|
||||||
> do ensure the test suite is green in the current state and
|
|
||||||
> those going forward ya?
|
|
||||||
|
|
||||||
and the standing reminder: no commits without a human ack.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Step B2 of the `._ria_nursery` removal (issue #477): merge the
|
|
||||||
two nested error handlers in
|
|
||||||
`_open_and_supervise_one_cancels_all_nursery` into one, now
|
|
||||||
that the 2ndary nursery (whose `__aexit__` the outer handler
|
|
||||||
originally guarded) is gone.
|
|
||||||
|
|
||||||
Trace-driven justification (see
|
|
||||||
`ai/conc-anal/ria_nursery_removal_plan.md` Step-B2 section):
|
|
||||||
the old inner handler records `errors[uid]` as its FIRST
|
|
||||||
action, so `errors` is always non-empty by the time control
|
|
||||||
could reach the outer handler — making the outer handler's
|
|
||||||
`raise` DEAD (always superseded by the `finally`'s
|
|
||||||
raise-from-`errors`). Collapsing to a single
|
|
||||||
`except BaseException as _scope_err` + the existing `finally`
|
|
||||||
is therefore behavior-preserving.
|
|
||||||
|
|
||||||
- removed the outer `try` + the
|
|
||||||
`except (Exception, BaseExceptionGroup, trio.Cancelled)`
|
|
||||||
handler; de-indented the inner handler one level.
|
|
||||||
- dropped the now-unused `outer_err`/`inner_err` locals
|
|
||||||
(single `_scope_err`).
|
|
||||||
- net ~30 lines lighter.
|
|
||||||
|
|
||||||
Gated hard: baseline (pre-B2 tip) = 300 passed + 1
|
|
||||||
load-induced `TooSlowError` flake (passes 6/6 in isolation);
|
|
||||||
B2 error/cancel gate (`test_cancellation remote_exc_relay
|
|
||||||
inter_peer_cancellation advanced_faults oob_cancellation
|
|
||||||
to_actor spawning local rpc`) = 71 passed, 1 xfailed; B2
|
|
||||||
full-suite run following.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_supervise.py` — collapse the two handlers
|
|
||||||
into one; drop `outer_err`/`inner_err`
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None yet — committed via the drafted
|
|
||||||
`.claude/git_commit_msg_ria_b2.md` (user-driven
|
|
||||||
`git commit --edit`).
|
|
||||||
|
|
@ -1,55 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-opus-4-8
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-07-02T22:25:44Z
|
|
||||||
git_ref: 9201a2ed
|
|
||||||
diff_cmd: git diff 9201a2ed..drop_ria_nursery
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw AI output (diff-ref mode)
|
|
||||||
|
|
||||||
Step-B2 code lives on `drop_ria_nursery` after `9201a2ed`; per
|
|
||||||
diff-ref mode the verbatim content is reachable via the pointer
|
|
||||||
below.
|
|
||||||
|
|
||||||
## Generated files
|
|
||||||
|
|
||||||
> `git diff 9201a2ed..drop_ria_nursery -- tractor/runtime/_supervise.py`
|
|
||||||
|
|
||||||
`_open_and_supervise_one_cancels_all_nursery`:
|
|
||||||
- removed the outer `try:` wrapper and the
|
|
||||||
`except (Exception, BaseExceptionGroup, trio.Cancelled) as
|
|
||||||
_outer_err:` safety-net handler.
|
|
||||||
- the former inner `except BaseException` is now THE handler,
|
|
||||||
renamed local `_inner_err` -> `_scope_err`, de-indented one
|
|
||||||
level; it sets `an._scope_error`, records `errors[uid]`,
|
|
||||||
waits on the debugger, `_join_procs.set()`, then a shielded
|
|
||||||
classify/log + snapshot-ria + `an.cancel()` + 0.5s-bounded
|
|
||||||
`_reap_ria_portals()`. No re-raise (the `finally` raises
|
|
||||||
from `errors`).
|
|
||||||
- `finally` block unchanged.
|
|
||||||
- dropped the `outer_err`/`inner_err` local decls at fn top.
|
|
||||||
|
|
||||||
(The diff is large — ~119+/149- — because de-indenting the
|
|
||||||
handler body one level rewrites every line in the block; the
|
|
||||||
logic delta is just "two handlers -> one".)
|
|
||||||
|
|
||||||
## Test runs (verbatim)
|
|
||||||
|
|
||||||
```
|
|
||||||
baseline (pre-B2, step-B tip 9201a2ed), full suite
|
|
||||||
(dynamic_pub_sub deselected):
|
|
||||||
1 failed, 300 passed, 9 skipped, 2 deselected, 1 xfailed,
|
|
||||||
2 xpassed in 1499.49s
|
|
||||||
-> the 1 failure = test_ext_types_over_ipc[...] trio.TooSlowError
|
|
||||||
(load-induced; passes 6/6 in isolation in 4.89s)
|
|
||||||
|
|
||||||
B2 error/cancel gate:
|
|
||||||
tests/test_cancellation test_remote_exc_relay
|
|
||||||
test_inter_peer_cancellation test_advanced_faults
|
|
||||||
test_oob_cancellation test_to_actor test_spawning test_local
|
|
||||||
test_rpc
|
|
||||||
-> 71 passed, 1 xfailed in 125.26s
|
|
||||||
|
|
||||||
B2 full-suite run: see b2_full.log
|
|
||||||
```
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
session: 6db64ac6-6986-4505-9343-df4ee31e67db
|
|
||||||
timestamp: 2026-07-06T17:28:18Z
|
|
||||||
git_ref: ad42871e
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260706T172818Z_ad42871e_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Resuming the `drop_ria_nursery` (wkt) branch after the human's
|
|
||||||
review pause at `ba4af0ad`, with the directive:
|
|
||||||
|
|
||||||
> we need to resolve the remaining outstandings ... to finish
|
|
||||||
> this endeavour we ideally,
|
|
||||||
> - finish the ria-tn hoist into the machinery in `to_actor` if
|
|
||||||
> possible.
|
|
||||||
> - avoid supporting the `.run_in_actor()` non-blocking API
|
|
||||||
> going forward other then if we can re-wrap `to_actor.run()`
|
|
||||||
> per prior discussion with opus4.8.
|
|
||||||
> - get the test harness completely green locally and in CI -
|
|
||||||
> AND ESPECIALLY without any hangs due to the "the old
|
|
||||||
> spawn-backend reaper raced `soft_kill()`'s scope-cancel"
|
|
||||||
> - update all docs to reflect these changes once we're happy.
|
|
||||||
|
|
||||||
plus mid-flight additions:
|
|
||||||
|
|
||||||
> make sure both all the test and example code uses the naming
|
|
||||||
> `an: ActorNursery` for variable names
|
|
||||||
|
|
||||||
> where we at in terms of a potential hackaround design for
|
|
||||||
> supporting a to_actor.run_async() ... without requiring the
|
|
||||||
> same malarky ... with a `trio.Event` relay ... i'm totes fine
|
|
||||||
> with rejigging the iface to NOT return a Portal as well.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Completed the entire #477 `run_in_actor` removal on
|
|
||||||
`drop_ria_nursery` (16 commits this session), gated per-commit:
|
|
||||||
|
|
||||||
- ported the remaining `test_cancellation` groups
|
|
||||||
(`some_cancels_all`, `nested_multierrors`+`spawn_and_error`,
|
|
||||||
SIGINT + sync-sleep) — one-shots become concurrent
|
|
||||||
`to_actor.run()` tasks in local task-nurseries; BEG-shape
|
|
||||||
assertions loosened for cancel-on-first + the runtime's
|
|
||||||
`collapse_eg()` single-member unwrap (a fully-raced nested
|
|
||||||
tree relays a bare annotated `RemoteActorError` chain).
|
|
||||||
- fixed a pre-existing `UnboundLocalError` (`timeout` `match`
|
|
||||||
had no default arm for non-trio/MTF backends).
|
|
||||||
- ported `test_dynamic_pub_sub`, 4 non-debugging examples, all
|
|
||||||
8 `debugging/` examples (debugger suite byte-identical,
|
|
||||||
28p/6s; `multi_subactors` introduces the "collect don't
|
|
||||||
cancel" reap-all replacement pattern), 8 docs pages + the
|
|
||||||
`experimental/_pubsub` docstring.
|
|
||||||
- EXCISED the API + cluster: `run_in_actor`,
|
|
||||||
`_reap_ria_portals`, `_cancel_after_result_on_exit`,
|
|
||||||
`Portal._submit_for_result/_expect_result_ctx/
|
|
||||||
wait_for_result/result`, `exhaust_portal`,
|
|
||||||
`cancel_on_completion`, `NoResult` — net -402 lines. The
|
|
||||||
reap-hang class dissolves structurally (result-waits now only
|
|
||||||
in caller task-scope).
|
|
||||||
- found + fixed a real migration race: mutual-rendezvous peers
|
|
||||||
(`test_trynamic_trio`, `a_trynamic_first_scene.py`) flaked
|
|
||||||
because an eagerly-reaped one-shot dies while its peer still
|
|
||||||
dials the registry-resolved (dead) sockaddr — such peers now
|
|
||||||
pin lifetimes via `start_actor()` + concurrent `Portal.run()`
|
|
||||||
+ explicit `an.cancel()`.
|
|
||||||
- `an: ActorNursery` naming sweep across tests/examples (±82
|
|
||||||
lines, scoped renames, prose untouched).
|
|
||||||
- parked a `to_actor.open_one_shot()` design sketch (acm +
|
|
||||||
private task-nursery over blocking `run()`; done-Event as
|
|
||||||
memo not cancel-relay; no Portal) in the plan doc.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
See commits `d01a2123..ad42871e` on `drop_ria_nursery`
|
|
||||||
(tests, examples, docs, `tractor/{runtime,spawn,to_actor,msg}`
|
|
||||||
+ `_exceptions/_context/experimental`).
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
---
|
|
||||||
model: claude-fable-5
|
|
||||||
service: claude
|
|
||||||
timestamp: 2026-07-06T17:28:18Z
|
|
||||||
git_ref: ad42871e
|
|
||||||
diff_cmd: git diff ba4af0ad..ad42871e
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw AI output (diff-ref mode)
|
|
||||||
|
|
||||||
This session's output spans the 16 migration/excision commits
|
|
||||||
`d01a2123..ad42871e` on `drop_ria_nursery`; per diff-ref mode
|
|
||||||
the verbatim content is reachable via the pointer below.
|
|
||||||
|
|
||||||
## Generated files
|
|
||||||
|
|
||||||
> `git diff ba4af0ad..ad42871e`
|
|
||||||
|
|
||||||
Commit-wise (each `Gate:`-footed msg documents its own module
|
|
||||||
gate):
|
|
||||||
|
|
||||||
- `d01a2123` port `test_some_cancels_all`
|
|
||||||
- `697c6152` fix unbound `timeout` (non-trio/MTF `match` arm)
|
|
||||||
- `fa8799d5` port `test_nested_multierrors`
|
|
||||||
- `f11754ce` port SIGINT + sync-sleep cancel tests
|
|
||||||
- `cb6202e3` port `test_dynamic_pub_sub`
|
|
||||||
- `d8af5f12` port non-debugging examples
|
|
||||||
- `a3057cb2` port debugging examples (+ `test_debugger`
|
|
||||||
nested-nurseries final-shape expectations)
|
|
||||||
- `d6bed7c4` port docs (8 rst pages)
|
|
||||||
- `07e1669e` fix stale `@pub` docstring example
|
|
||||||
- `2a59cefb` REMOVE `run_in_actor()` + the ria reap cluster
|
|
||||||
(net -402 lines)
|
|
||||||
- `a297a32a` fix mutual-rendezvous premature-reap race
|
|
||||||
- `ad42871e` `an: ActorNursery` naming sweep
|
|
||||||
|
|
||||||
Plan/design record updated in
|
|
||||||
`ai/conc-anal/ria_nursery_removal_plan.md` (RESOLVED section +
|
|
||||||
the `to_actor.open_one_shot()` follow-up sketch).
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_0799212ebffe42arY96czXn89F
|
|
||||||
timestamp: 2026-08-11T23:38:33Z
|
|
||||||
git_ref: 7cbd64ee
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260811T233833Z_7cbd64ee_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Open a new isolated worktree in the local Tractor repository and draft a fix
|
|
||||||
for `BroadcastReceiver` reporting that a lagged one-slot consumer dropped
|
|
||||||
zero values when one value had actually been displaced.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Corrected the off-by-one lag count while preserving cursor recovery and added
|
|
||||||
deterministic narrow- and wider-window regressions for exact loss reporting.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/trionics/_broadcast.py` - exact broadcast overrun count.
|
|
||||||
- `tests/test_task_broadcasting.py` - lag count and recovery regression.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None - generated output follows the user's diagnosed edge case.
|
|
||||||
|
|
@ -1,50 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-11T23:38:33Z
|
|
||||||
git_ref: 7cbd64ee
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
The user asked for a Tractor fix in a new isolated worktree after a live piker
|
|
||||||
failure reported:
|
|
||||||
|
|
||||||
```text
|
|
||||||
tractor.trionics._broadcast.Lagged:
|
|
||||||
Task `piker.brokers.ib.broker.handle_order_requests` overrun and
|
|
||||||
dropped `0` values
|
|
||||||
```
|
|
||||||
|
|
||||||
Inspection showed the lag exception was valid but its count was off by one.
|
|
||||||
`BroadcastReceiver.receive_nowait()` treats `seq` as a deque index. With a
|
|
||||||
one-entry queue, index zero is the only retained value and `seq == 1` already
|
|
||||||
means one value was displaced. The old `seq - maxlen` calculation therefore
|
|
||||||
reported zero instead of one.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/trionics/_broadcast.py`
|
|
||||||
|
|
||||||
Adjusted the lag count to `seq - maxlen + 1` and documented why the first
|
|
||||||
invalid deque index must be included. The existing Tokio-style cursor reset
|
|
||||||
remains unchanged.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_task_broadcasting.py`
|
|
||||||
|
|
||||||
Added a deterministic parameterized regression covering a one-slot queue
|
|
||||||
with one dropped value and a three-slot queue with two dropped values. The
|
|
||||||
test keeps the root receiver idle while a child subscriber synchronously
|
|
||||||
drains each produced value, asserts the exact `Lagged` message, and proves the
|
|
||||||
next receive resumes at the oldest retained item.
|
|
||||||
|
|
||||||
Verification output:
|
|
||||||
|
|
||||||
```text
|
|
||||||
.. [100%]
|
|
||||||
2 passed in 0.04s
|
|
||||||
|
|
||||||
.......... [100%]
|
|
||||||
10 passed in 4.65s
|
|
||||||
```
|
|
||||||
|
|
||||||
The targeted import resolved to the new Tractor worktree. Python compilation
|
|
||||||
and `git diff --check` passed. Adversarial review found no actionable issues;
|
|
||||||
zero-capacity channels remain a pre-existing untested edge outside this fix.
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_0799212ebffe42arY96czXn89F
|
|
||||||
timestamp: 2026-08-12T01:23:24Z
|
|
||||||
git_ref: 06c4af17
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260812T012324Z_06c4af17_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Iteratively refine Tractor PR 490. For item one, correct broadcast statistics
|
|
||||||
queue counts and Trio event checks, verify and review the exact change, then
|
|
||||||
return a complete commit plan before proceeding.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Converted subscriber cursor indexes into clamped retained queue counts,
|
|
||||||
removed deprecated event truthiness, and added deterministic state and
|
|
||||||
deprecation regressions.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/trionics/_broadcast.py` - accurate queue and waiter statistics.
|
|
||||||
- `tests/test_task_broadcasting.py` - retained-count and event regression.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None - generated output follows the requested first iterative item.
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-12T01:23:24Z
|
|
||||||
git_ref: 06c4af17
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
After opening draft Tractor PR 490, the user requested an iterative pass over
|
|
||||||
additional broadcast subsystem findings. The first item was to correct
|
|
||||||
`BroadcastState.statistics()` queued counts and its deprecated Trio event
|
|
||||||
truthiness check, then stop for a complete commit plan.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/trionics/_broadcast.py`
|
|
||||||
|
|
||||||
Changed `queued_len_by_task` from raw deque cursor indexes to retained,
|
|
||||||
receivable counts. Caught-up `-1` reports zero, valid indexes report index plus
|
|
||||||
one, and lagged cursors clamp to the current retained queue length. Replaced
|
|
||||||
`trio.Event` truthiness with an explicit `is not None` branch.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_task_broadcasting.py`
|
|
||||||
|
|
||||||
Added a deterministic statistics regression using actual sends and receives.
|
|
||||||
It verifies caught-up and one-queued states, drives a root receiver beyond a
|
|
||||||
three-slot retention window to prove clamping, and installs a real
|
|
||||||
`trio.Event` while treating deprecations as errors.
|
|
||||||
|
|
||||||
Verification output:
|
|
||||||
|
|
||||||
```text
|
|
||||||
........... [100%]
|
|
||||||
11 passed in 5.76s
|
|
||||||
```
|
|
||||||
|
|
||||||
Python compilation and `git diff --check` passed. Initial adversarial review
|
|
||||||
caught unclamped lagged cursors and an ineffective event test; both were
|
|
||||||
fixed. Final review found no actionable issues.
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_0799212ebffe42arY96czXn89F
|
|
||||||
timestamp: 2026-08-12T03:06:08Z
|
|
||||||
git_ref: 1095e7f7
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260812T030608Z_1095e7f7_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
For Tractor PR 490 item two, make shared underlying receive failures wake and
|
|
||||||
terminate every broadcast subscriber without losing retained values. Review,
|
|
||||||
verify and return a complete commit plan before continuing.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Published ordinary receive failures as terminal broadcast state, introduced
|
|
||||||
a public chained peer exception, kept control-flow exits transient while
|
|
||||||
waking peers, documented the contract, and added deterministic regressions.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/trionics/_broadcast.py` - terminal failure and peer wake protocol.
|
|
||||||
- `tractor/trionics/__init__.py` - public peer exception export.
|
|
||||||
- `docs/api/trionics.rst` - failure-delivery API contract.
|
|
||||||
- `tests/test_task_broadcasting.py` - terminal and transient failure tests.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None - generated output follows the requested second iterative item.
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-12T03:06:08Z
|
|
||||||
git_ref: 1095e7f7
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
The user requested the second iterative refinement for Tractor PR 490: ensure
|
|
||||||
non-EOC failures from a shared underlying broadcast receiver do not leave peer
|
|
||||||
subscribers blocked forever, then stop for a complete commit plan.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/trionics/_broadcast.py`
|
|
||||||
|
|
||||||
Added shared terminal failure publication for ordinary `Exception` values.
|
|
||||||
The receive owner gets the original exception; peers may drain retained
|
|
||||||
values and then get a fresh `BroadcastReceiveError` chained from the original.
|
|
||||||
Late subscribers observe the same terminal state without retrying the failed
|
|
||||||
underlying receiver. Process-control and cancellation-like `BaseException`
|
|
||||||
values wake peers but are re-raised without becoming durable channel state.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/trionics/__init__.py`
|
|
||||||
|
|
||||||
Exported `BroadcastReceiveError` as the public peer-delivery exception.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- docs/api/trionics.rst`
|
|
||||||
|
|
||||||
Documented `BroadcastReceiveError` and the owner-versus-peer delivery
|
|
||||||
contract, including retained-value draining and late subscribers.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_task_broadcasting.py`
|
|
||||||
|
|
||||||
Added deterministic bounded regressions. One scripts a successful receive
|
|
||||||
followed by `RuntimeError`, proving the root drains retained data, all current
|
|
||||||
and late receivers observe terminal failure, and the source is not retried.
|
|
||||||
The second scripts a custom `BaseException`, proving peers wake and take over
|
|
||||||
the next source receive without retaining control-flow state.
|
|
||||||
|
|
||||||
Verification output:
|
|
||||||
|
|
||||||
```text
|
|
||||||
............. [100%]
|
|
||||||
13 passed in 5.62s
|
|
||||||
```
|
|
||||||
|
|
||||||
Python compilation and `git diff --check` passed. Iterative adversarial review
|
|
||||||
drove independent peer exception wrappers, ordinary-versus-control-flow
|
|
||||||
classification, bounded test completion, public docs, and the final catch-all
|
|
||||||
peer wake. Final review found no issues.
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_0799212ebffe42arY96czXn89F
|
|
||||||
timestamp: 2026-08-12T15:00:27Z
|
|
||||||
git_ref: c2a6ccef
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260812T150027Z_c2a6ccef_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
For Tractor PR 490 item three, prevent subscriber closure from waking another
|
|
||||||
receiver's shared event or stranding peers. Preserve close-safe source-read
|
|
||||||
ownership handoff, then review, verify and return a complete commit plan.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Introduced receiver-local wait/source cancellation, owner-specific handoff,
|
|
||||||
and close precedence over shielded source values/errors/EOC. Clarified that
|
|
||||||
private scope cancellation means explicit close while outer task cancellation
|
|
||||||
remains `trio.Cancelled` for both source-read and peer-wait scopes, with
|
|
||||||
deterministic receiver-close regressions.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/trionics/_broadcast.py` - receiver-local close and ownership scopes.
|
|
||||||
- `tests/test_task_broadcasting.py` - peer close and owner handoff regressions.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None - generated output follows the requested third iterative item.
|
|
||||||
|
|
@ -1,51 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-12T15:00:27Z
|
|
||||||
git_ref: c2a6ccef
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
The user requested the third iterative refinement for Tractor PR 490: closing
|
|
||||||
one broadcast subscriber must not set another receiver owner's shared event
|
|
||||||
and create a runnable hot loop, then stop for a complete commit plan.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/trionics/_broadcast.py`
|
|
||||||
|
|
||||||
Added receiver-local wait cancellation and source-read ownership scopes.
|
|
||||||
Closing a non-owner waiting behind another source reader cancels only that
|
|
||||||
receiver's private wait and maps it to `ClosedResourceError`; the shared event
|
|
||||||
remains untouched. Closing the active source owner cancels only its private
|
|
||||||
source-read scope, wakes peers after cleanup, and lets one peer take ownership.
|
|
||||||
|
|
||||||
Source outcomes are captured inside the owner scope and classified only after
|
|
||||||
checking close/cancel state. A cancellation-shielding source therefore cannot
|
|
||||||
publish a returned value, ordinary error, or EOC after its owner was closed.
|
|
||||||
The private scope's `cancel_called` bit is asserted to imply receiver closure;
|
|
||||||
outer task cancellation remains `trio.Cancelled` and is not translated into
|
|
||||||
`ClosedResourceError`. Owner-key comments document that only the receiver
|
|
||||||
identified by `recv_ready[0]` may cancel the shared source-read scope. The
|
|
||||||
same explicit-close invariant is enforced symmetrically for private peer-wait
|
|
||||||
scope cancellation.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_task_broadcasting.py`
|
|
||||||
|
|
||||||
Added deterministic bounded regressions for both close positions. The
|
|
||||||
non-owner test places two peers behind an active source read, closes one and
|
|
||||||
proves only that peer exits while the shared event stays unset. The owner test
|
|
||||||
closes a source owner whose receive shields cancellation and parameterizes a
|
|
||||||
returned value, `RuntimeError`, and `EndOfChannel`; each discarded outcome
|
|
||||||
hands the next source receive to the waiting root without terminal-state or
|
|
||||||
EOC publication.
|
|
||||||
|
|
||||||
Verification output:
|
|
||||||
|
|
||||||
```text
|
|
||||||
................. [100%]
|
|
||||||
17 passed in 5.84s
|
|
||||||
```
|
|
||||||
|
|
||||||
Python compilation and `git diff --check` passed. Iterative adversarial review
|
|
||||||
caught a waiting non-owner hang, cancellation-shielded source returns, and
|
|
||||||
shielded source exceptions. All were fixed. Final review found no actionable
|
|
||||||
issues.
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_0799212ebffe42arY96czXn89F
|
|
||||||
timestamp: 2026-08-12T21:31:17Z
|
|
||||||
git_ref: 51185487
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260812T213117Z_51185487_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
For Tractor PR 490 item four, expose `raise_on_lag` through the public IPC and
|
|
||||||
asyncio linked-channel subscription wrappers. Review, verify and return a
|
|
||||||
complete commit plan before applying the API downstream in piker.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Added public per-subscription lag policy to both wrappers, preserved first-call
|
|
||||||
root policy, documented the semantics, and covered forwarding plus real fan-out
|
|
||||||
paths.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/_streaming.py` - `MsgStream` lag policy forwarding.
|
|
||||||
- `tractor/to_asyncio.py` - linked-channel lag policy forwarding.
|
|
||||||
- `docs/guide/streaming.rst` - IPC fan-out policy docs.
|
|
||||||
- `docs/guide/asyncio.rst` - linked-channel fan-out policy docs.
|
|
||||||
- `tests/test_task_broadcasting.py` - wrapper policy regression.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None - generated output follows the requested fourth iterative item.
|
|
||||||
|
|
@ -1,53 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-12T21:31:17Z
|
|
||||||
git_ref: 51185487
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
The user requested the fourth iterative refinement for Tractor PR 490: expose
|
|
||||||
subscriber lag policy through the public `MsgStream.subscribe()` and
|
|
||||||
`LinkedTaskChannel.subscribe()` wrappers, then stop for a complete commit
|
|
||||||
plan. This enables piker to replace private receiver mutation.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/_streaming.py`
|
|
||||||
|
|
||||||
Added `raise_on_lag: bool = True` to `MsgStream.subscribe()`. The first call
|
|
||||||
passes the policy to the irreversibly allocated root broadcaster and its
|
|
||||||
child; later calls configure each child independently while retaining the
|
|
||||||
root's first-call policy.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/to_asyncio.py`
|
|
||||||
|
|
||||||
Added equivalent lag-policy forwarding to `LinkedTaskChannel.subscribe()`.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- docs/guide/streaming.rst`
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- docs/guide/asyncio.rst`
|
|
||||||
|
|
||||||
Documented strict versus warn/drop/resume behavior, independent child policy,
|
|
||||||
and first-call root policy for both wrapper types.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_task_broadcasting.py`
|
|
||||||
|
|
||||||
Added a parameterized wrapper-level regression using minimal receive-compatible
|
|
||||||
handles. It verifies a first non-raising subscription configures root and
|
|
||||||
child, then a later strict child does not mutate the sticky root policy.
|
|
||||||
|
|
||||||
Verification output:
|
|
||||||
|
|
||||||
```text
|
|
||||||
................... [100%]
|
|
||||||
19 passed in 5.71s
|
|
||||||
|
|
||||||
.................... [100%]
|
|
||||||
20 passed in 6.88s
|
|
||||||
|
|
||||||
. [100%]
|
|
||||||
1 passed in 0.86s
|
|
||||||
```
|
|
||||||
|
|
||||||
The second and third runs cover actual `MsgStream` and infected-asyncio
|
|
||||||
`LinkedTaskChannel` fan-out respectively. Python compilation and
|
|
||||||
`git diff --check` passed. Adversarial review found no actionable issues.
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: unavailable
|
|
||||||
timestamp: 2026-08-13T18:19:01Z
|
|
||||||
git_ref: a2e0df4b
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260813T181901Z_a2e0df4b_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue Tractor PR 490 after the paired piker EMS consumer commit. Clean
|
|
||||||
cancelled-task diagnostics, define or reject zero-buffer broadcast behavior,
|
|
||||||
review and verify the change, then stop at a complete commit plan.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Bound cancellation diagnostics to receiver progress, terminal state and
|
|
||||||
resource lifetime; made EOC durable across peers; released wrapper-owned root
|
|
||||||
broadcasters without breaking graceful EOC or subclass overrides; and rejected
|
|
||||||
non-positive fan-out retention capacity.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/trionics/_broadcast.py` - diagnostic lifecycle, durable EOC and
|
|
||||||
buffer validation.
|
|
||||||
- `tractor/_streaming.py` - safe `MsgStream` root broadcaster cleanup.
|
|
||||||
- `tractor/to_asyncio.py` - linked-channel root broadcaster cleanup.
|
|
||||||
- `tests/test_task_broadcasting.py` - cancellation, EOC, wrapper and capacity
|
|
||||||
regressions.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None - generated output follows the requested fifth iterative item.
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-13T18:19:01Z
|
|
||||||
git_ref: a2e0df4b
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
The user asked to continue after committing the paired piker EMS consumer
|
|
||||||
fix. The next isolated Tractor PR 490 item was to clean cancelled-task
|
|
||||||
diagnostics and define zero-buffer broadcast behavior, then review, test and
|
|
||||||
stop at a complete commit plan.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/trionics/_broadcast.py`
|
|
||||||
|
|
||||||
Made `BroadcastState.cancelled` transient: receiver progress and close clear
|
|
||||||
that receiver's diagnostic, terminal EOC and shared receive failure clear all
|
|
||||||
stale cancelled tasks, and durable EOC prevents peers from re-entering the
|
|
||||||
closed source. `broadcast_receiver()` now rejects non-positive retention
|
|
||||||
capacity before creating an unusable zero-length deque.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/_streaming.py`
|
|
||||||
|
|
||||||
Made explicit `MsgStream.aclose()` release its internally allocated root
|
|
||||||
broadcaster while preserving graceful receive-internal EOC teardown. Used a
|
|
||||||
task-local marker so the public zero-argument `aclose()` signature and valid
|
|
||||||
subclass overrides remain compatible.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/to_asyncio.py`
|
|
||||||
|
|
||||||
Made `LinkedTaskChannel.aclose()` release its internally allocated root
|
|
||||||
broadcaster before closing the underlying Trio receive channel.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_task_broadcasting.py`
|
|
||||||
|
|
||||||
Added synchronized regressions for transient child cancellation diagnostics,
|
|
||||||
cross-receiver terminal cleanup, durable EOC peer wakeups, root broadcaster
|
|
||||||
cleanup through both public wrappers, `MsgStream.aclose()` subclass
|
|
||||||
compatibility, and zero-buffer rejection.
|
|
||||||
|
|
||||||
Verification output:
|
|
||||||
|
|
||||||
```text
|
|
||||||
........................... [100%]
|
|
||||||
27 passed in 5.89s
|
|
||||||
|
|
||||||
. [100%]
|
|
||||||
1 passed in 1.22s
|
|
||||||
|
|
||||||
. [100%]
|
|
||||||
1 passed in 0.88s
|
|
||||||
```
|
|
||||||
|
|
||||||
The integration runs cover real `MsgStream` actor fan-out and infected-asyncio
|
|
||||||
`LinkedTaskChannel` fan-out. Ruff, Python compilation and `git diff --check`
|
|
||||||
passed. Repeated adversarial review found and resolved root close re-entrancy,
|
|
||||||
cross-receiver terminal retention, durable-EOC and subclass-compatibility
|
|
||||||
issues; final review reported no findings.
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: pr475-review-fixes-20260817
|
|
||||||
timestamp: 2026-08-17T23:18:25Z
|
|
||||||
git_ref: 359fe75c
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260817T231825Z_359fe75c_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue the `/code-review-changes` pass for PR #475 in its isolated
|
|
||||||
worktree. Address the seven accepted manual-review findings in
|
|
||||||
`tractor/ipc/_types.py` and `tractor/ipc/_uds.py`, preserve the existing
|
|
||||||
Windows capability behavior, verify the result, and prepare the work for
|
|
||||||
human-controlled commit and review-reply steps. Do not publish replies,
|
|
||||||
stage, commit, or push without the required explicit authorization.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Restored project quote, docstring, multiline-expression, and
|
|
||||||
`match/case` conventions while retaining the Windows-safe UDS guard.
|
|
||||||
Removed unnecessary structural and comment churn, then verified the
|
|
||||||
focused transport, discovery, and lazy-import paths plus the missing
|
|
||||||
`AF_UNIX` behavior.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/ipc/_types.py` - restore project style and guarded
|
|
||||||
socket-family dispatch.
|
|
||||||
- `tractor/ipc/_uds.py` - format the UDS capability gate
|
|
||||||
consistently.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None - the generated patch remains uncommitted and awaits human
|
|
||||||
review.
|
|
||||||
|
|
@ -1,45 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-17T23:18:25Z
|
|
||||||
git_ref: 359fe75c
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Applied the seven accepted manual-review fixes for PR #475 while
|
|
||||||
preserving the Windows transport capability behavior.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/ipc/_types.py`
|
|
||||||
|
|
||||||
The generated changes restore the project's single-quote docstring and
|
|
||||||
string conventions, remove the unnecessary helper divider, simplify the
|
|
||||||
transport-registry comments, and restore `match/case` socket-family
|
|
||||||
dispatch. The UDS case retains a `HAS_UDS` guard that short-circuits
|
|
||||||
before `socket.AF_UNIX` is evaluated on unsupported hosts. Nearby error
|
|
||||||
messages are wrapped without changing their content.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/ipc/_uds.py`
|
|
||||||
|
|
||||||
The generated change reformats the `HAS_UDS` conjunction according to
|
|
||||||
the project's multiline boolean-expression convention and simplifies
|
|
||||||
the adjacent capability comment.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
`/home/goodboy/repos/tractor/py313/bin/pytest -q tests/test_lazy_imports.py tests/discovery tests/ipc/test_server.py`
|
|
||||||
|
|
||||||
Result: `66 passed, 2 xpassed in 60.62s`.
|
|
||||||
|
|
||||||
`ruff check --no-cache --output-format=json tractor/ipc/_types.py tractor/ipc/_uds.py`
|
|
||||||
|
|
||||||
Result: no findings.
|
|
||||||
|
|
||||||
`git diff --check`
|
|
||||||
|
|
||||||
Result: no whitespace errors.
|
|
||||||
|
|
||||||
An explicit missing-`AF_UNIX` probe set `HAS_UDS = False`, removed the
|
|
||||||
socket constant, and exercised an unsupported socket family. It raised
|
|
||||||
the expected `NotImplementedError` instead of `AttributeError`.
|
|
||||||
|
|
||||||
No review replies, commits, or pushes were published.
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: pr481-review-fixes-p1-20260818
|
|
||||||
timestamp: 2026-08-18T03:15:32Z
|
|
||||||
git_ref: 4151b956
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260818T031532Z_4151b956_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Address the approved review findings on PR #481, but work
|
|
||||||
iteratively: implement and verify one finding at a time, prepare a
|
|
||||||
separate `/commit-plan` after each fix, and stop for the human commit
|
|
||||||
before starting the next finding. Begin with the P1 per-child
|
|
||||||
lifecycle issue. Also publish the already-approved review findings
|
|
||||||
against the reviewed PR head before editing.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Published the approved non-approving review at head `4151b956`, then
|
|
||||||
implemented only the P1 lifecycle fix. Owned one-shot actors now use
|
|
||||||
a child-specific cancellation and process-reap handshake, including
|
|
||||||
hard escalation for unacknowledged cancellation and deterministic
|
|
||||||
bookkeeping removal before `to_actor.run()` returns.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_supervise.py` - coordinate child-specific cancel
|
|
||||||
and reap.
|
|
||||||
- `tractor/spawn/_trio.py` - wait on the Trio child's reap request.
|
|
||||||
- `tractor/spawn/_mp.py` - wait on the multiprocessing child's reap
|
|
||||||
request.
|
|
||||||
- `tractor/spawn/_spawn.py` - publish monitor completion centrally.
|
|
||||||
- `tractor/to_actor/_api.py` - await owned-child process reaping.
|
|
||||||
- `tests/test_to_actor.py` - cover cleanup, escalation, and startup
|
|
||||||
ordering.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
None - the generated P1 patch remains uncommitted for human review.
|
|
||||||
|
|
@ -1,74 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-18T03:15:32Z
|
|
||||||
git_ref: 4151b956
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Implemented only the P1 lifecycle finding from the approved PR #481
|
|
||||||
review, preserving the requested one-fix-at-a-time commit boundary.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
|
|
||||||
|
|
||||||
Added per-child reap request/completion events to `ActorNursery`, a
|
|
||||||
shielded child-specific cancel-and-reap operation, late-registration
|
|
||||||
latching for nursery teardown, and cancellation escalation that waits
|
|
||||||
for debugger release before using non-ignorable process termination.
|
|
||||||
The nursery-wide cancellation path snapshots child records before
|
|
||||||
checkpointing so concurrent one-shot cleanup cannot invalidate its
|
|
||||||
iteration.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_trio.py`
|
|
||||||
|
|
||||||
Changed Trio child monitors to wait on their per-child reap requests.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_mp.py`
|
|
||||||
|
|
||||||
Changed multiprocessing child monitors to wait on their per-child reap
|
|
||||||
requests.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_spawn.py`
|
|
||||||
|
|
||||||
Ensured every backend publishes child-reap completion after its process
|
|
||||||
monitor exits.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/to_actor/_api.py`
|
|
||||||
|
|
||||||
Changed owned one-shot cleanup to await child-specific process joining
|
|
||||||
and bookkeeping removal instead of treating the cancel RPC as reaping.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
|
|
||||||
|
|
||||||
Added regressions for immediate caller-managed nursery cleanup, failed
|
|
||||||
cancel acknowledgement escalation, and child registration after a
|
|
||||||
latched nursery-wide teardown request.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
`pytest -q tests/test_to_actor.py tests/test_cancellation.py tests/test_spawning.py tests/discovery/test_multi_program.py`
|
|
||||||
|
|
||||||
Result: `46 passed, 1 xfailed, 3 xpassed`.
|
|
||||||
|
|
||||||
`pytest -q tests/test_to_actor.py --tpt-proto uds`
|
|
||||||
|
|
||||||
Result: `13 passed`.
|
|
||||||
|
|
||||||
`pytest -q tests/test_to_actor.py --spawn-backend mp_spawn --tpt-proto tcp`
|
|
||||||
|
|
||||||
Result: `13 passed`.
|
|
||||||
|
|
||||||
One broad verification run was mistakenly launched in parallel with
|
|
||||||
the UDS and `mp_spawn` actor suites. It timed out
|
|
||||||
`test_remote_error_from_caller_nursery`; the node passed immediately
|
|
||||||
in isolation and the complete broad selection then passed serially.
|
|
||||||
The failure was classified as concurrent test-session interference,
|
|
||||||
not accepted as a passing boundary result.
|
|
||||||
|
|
||||||
Ruff, Python compilation, and `git diff --check` passed for the changed
|
|
||||||
boundary. Ruff's existing `_trio.py` F401 finding was reproduced at the
|
|
||||||
unmodified PR head and excluded from attribution to this patch.
|
|
||||||
|
|
||||||
No source files were staged, committed, pushed, or used for review
|
|
||||||
replies. The previously approved top-level review was published before
|
|
||||||
the fix at reviewed head `4151b956`.
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
|
|
||||||
timestamp: 2026-08-18T19:30:01Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260818T193001Z_bf06b4f8_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Fix cancellation during an indeterminate transport write without
|
|
||||||
allowing a second RPC msg to corrupt the stream. Keep the transport
|
|
||||||
fix separate from the higher-level `to_actor` remediation and explain
|
|
||||||
the length-prefixed protocol rather than using unexplained "framed
|
|
||||||
send" terminology.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Close a msgpack transport stream when cancellation interrupts its
|
|
||||||
length-prefixed `send_all()` operation. The stream can no longer be
|
|
||||||
safely reused because Trio cannot report how many bytes were written.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/ipc/_transport.py` - close an interrupted send stream.
|
|
||||||
- `tests/ipc/test_each_tpt.py` - cover cancellation during the write.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human required this transport edge-case fix to land as its own
|
|
||||||
behavioral commit with a detailed message. During staged review, the
|
|
||||||
human also rejected the unexplained "framed send" wording and asked
|
|
||||||
for terminology tied directly to the actual transport operation.
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-18T19:30:01Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Prospective review found that cancellation can interrupt
|
|
||||||
`MsgpackTransport.send()` after `send_all()` writes only part of its
|
|
||||||
length-prefixed msg. Sending a cancellation request afterward can
|
|
||||||
append another msg to the indeterminate stream and desynchronize the
|
|
||||||
peer decoder.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/ipc/_transport.py tests/ipc/test_each_tpt.py`
|
|
||||||
|
|
||||||
Close the stream under a cancellation shield when `send_all()` is
|
|
||||||
cancelled. Cover the behavior with a fake stream that checkpoints
|
|
||||||
inside the write and records forced closure.
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
|
|
||||||
timestamp: 2026-08-18T19:30:02Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260818T193002Z_bf06b4f8_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Distill repeated `Actor._contexts.pop()` machinery into a wrapper like
|
|
||||||
the RPC-task registration helper so future teardown sites do not keep
|
|
||||||
reconstructing the context-registry key independently. Preserve the
|
|
||||||
existing lifecycle-specific cleanup behavior.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Add idempotent `Actor._drop_context()` registry removal keyed from the
|
|
||||||
context's own channel and CID. Use it for caller context teardown and
|
|
||||||
the strict callee-side RPC deregistration path.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_runtime.py` - own context-registry removal.
|
|
||||||
- `tractor/runtime/_rpc.py` - use the helper for callee teardown.
|
|
||||||
- `tractor/_context.py` - use the helper after caller teardown.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human identified the repeated registry-pop code and requested a
|
|
||||||
central primitive analogous to `_register_rpc_task()`. The agent first
|
|
||||||
suggested an async helper that also closed receive channels; the final
|
|
||||||
design was narrowed to registry removal only so each lifecycle owner
|
|
||||||
retains its existing closure, debugger, shielding, and error policy.
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-18T19:30:02Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Repeated teardown sites reconstruct the `Actor._contexts` registry
|
|
||||||
key from a portal channel and context ID before popping it. Add an
|
|
||||||
idempotent actor-owned helper deriving the key from the context itself,
|
|
||||||
then route caller and callee context teardown through that helper.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_runtime.py tractor/runtime/_rpc.py tractor/_context.py`
|
|
||||||
|
|
||||||
Keep receive-channel closure and cancellation shielding in each
|
|
||||||
lifecycle owner so the helper centralizes registry machinery without
|
|
||||||
changing their teardown ordering.
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
|
|
||||||
timestamp: 2026-08-18T19:30:03Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260818T193003Z_bf06b4f8_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Cancel a remote task when its caller is cancelled after `Start`
|
|
||||||
publication but before startup acknowledgement. Keep cancellation
|
|
||||||
bounded, prevent its private `_cancel_task` RPC from recursively
|
|
||||||
cancelling itself and preserve public target kwargs unchanged.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Add private portal startup policy, use it for non-recursive context
|
|
||||||
cancellation and clean caller-side startup state under a shield.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_portal.py` - separate private startup policy.
|
|
||||||
- `tractor/_context.py` - disable recursion for cancellation RPCs.
|
|
||||||
- `tractor/runtime/_runtime.py` - clean cancelled task startup.
|
|
||||||
- `tests/test_context_stream_semantics.py` - control cancellation
|
|
||||||
between `Start` publication and acknowledgement.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human required this cancellation behavior to remain a distinct
|
|
||||||
commit from general startup failures and from the public `to_actor`
|
|
||||||
API. The human also requested that its runtime comment describe the
|
|
||||||
actual length-prefixed transport guarantee and concrete `_cancel_task`
|
|
||||||
operation rather than referring to an unnamed wrapper.
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-18T19:30:03Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Cancellation while `Actor.start_remote_task()` waits for `StartAck`
|
|
||||||
can strand its caller-side context and leave the remote task running.
|
|
||||||
Make one bounded cleanup request, remove local startup state and close
|
|
||||||
its receive channel.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_runtime.py tractor/runtime/_portal.py tractor/_context.py tests/test_context_stream_semantics.py`
|
|
||||||
|
|
||||||
Separate private startup-cancellation policy from public target kwargs
|
|
||||||
using `Portal._run_from_ns()`. Have `Context.cancel()` disable recursive
|
|
||||||
startup cancellation for its own `_cancel_task` RPC. Exercise
|
|
||||||
cancellation after `Start` publication and prove the caller-owned actor
|
|
||||||
remains reusable without leaked contexts.
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
|
|
||||||
timestamp: 2026-08-18T19:30:04Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260818T193004Z_bf06b4f8_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Release caller-side context state for every remote-task startup failure,
|
|
||||||
not only local cancellation. Preserve the remote error, avoid unsafe
|
|
||||||
follow-up sends and prove pre-publication serialization failures leave
|
|
||||||
a reused portal healthy.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Extend remote-task startup cleanup across send, acknowledgement and
|
|
||||||
validation errors. Track completed publication, perform only safe
|
|
||||||
best-effort cancellation and deterministically remove local state.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_runtime.py` - clean every startup failure path.
|
|
||||||
- `tests/test_context_stream_semantics.py` - cover authorization and
|
|
||||||
serialization failures before context entry.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human accepted the discovered edge-case fixes but required general
|
|
||||||
startup cleanup to land separately from cancellation cleanup, transport
|
|
||||||
integrity and the public API. This boundary preserves that behavioral
|
|
||||||
distinction and its dedicated commit-message rationale.
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-18T19:30:04Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
`Actor.start_remote_task()` inserts a context before sending `Start`,
|
|
||||||
but startup errors other than cancellation escape without removing or
|
|
||||||
closing that caller state. Serialization errors, acknowledgement
|
|
||||||
timeouts, malformed acknowledgements and remote authorization errors
|
|
||||||
can therefore leak context-registry entries.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_runtime.py tests/test_context_stream_semantics.py`
|
|
||||||
|
|
||||||
Cover the complete send, acknowledgement and validation phase with
|
|
||||||
exceptional cleanup. Attempt remote cancellation only when publication
|
|
||||||
is known complete or protocol-safe, and always release local state.
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
|
|
||||||
timestamp: 2026-08-18T19:30:05Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260818T193005Z_bf06b4f8_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Replace abandoned `Portal.run()` one-shots with a static linked-context
|
|
||||||
endpoint. Follow Trio positional-call semantics, use partials for target
|
|
||||||
keywords, preserve Python 3.14 Placeholder behavior, keep target lookup
|
|
||||||
behind the RPC allowlist and support private, nursery and portal
|
|
||||||
placement.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Use `Portal.open_context()` and `Context.wait_for_result()` for one-shot
|
|
||||||
tasks. Normalize every partial layer, validate signatures locally and
|
|
||||||
send target namespace/function components separately to the authorized
|
|
||||||
remote resolver. Retain the client-side function in its `NamespacePath`
|
|
||||||
so `to_tuple()` does not re-import it. Owned actors enable the declaring
|
|
||||||
`_api.__name__` directly; caller-owned portals opt in through the public
|
|
||||||
`to_actor.MODULE` alias.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/to_actor/_api.py` - implement linked one-shot calls.
|
|
||||||
- `tractor/to_actor/__init__.py` - export `MODULE`.
|
|
||||||
- `tractor/msg/ptr.py` - retain refs created by `from_ref()`.
|
|
||||||
- `tests/test_to_actor.py` - cover the public API and authorization.
|
|
||||||
- `examples/parallelism/to_actor_one_shots.py` - use positional inputs.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human rejected nested target-kwargs configuration and selected
|
|
||||||
Trio-style positional inputs plus `functools.partial()`. During staged
|
|
||||||
review the human required a Python 3.14 compatibility comment rather
|
|
||||||
than removing Placeholder support, requested separate namespace and
|
|
||||||
function inputs, preserved `_get_rpc_func(ns: str, funcname: str)`
|
|
||||||
authorization, renamed `RPC_MODULE` to `MODULE`, rejected global module
|
|
||||||
exposure and deferred speculative nursery/module-list helpers to the
|
|
||||||
`open_taskman()` design line. The human also required this public API
|
|
||||||
to land only after its lower-level safety dependencies. In final staged
|
|
||||||
review, the human required `_invoke_from_portal()` to use
|
|
||||||
`NamespacePath.to_tuple()` with the already-held function ref and
|
|
||||||
required internal actor setup to use `_api.__name__` directly, keeping
|
|
||||||
`to_actor.MODULE` solely as the public importer-facing alias.
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-18T19:30:05Z
|
|
||||||
git_ref: bf06b4f8
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Implement `to_actor.run()` with Trio-style positional target arguments,
|
|
||||||
`functools.partial` keyword and Python 3.14 Placeholder binding, and a
|
|
||||||
static context endpoint that links remote results, errors and caller
|
|
||||||
cancellation.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/to_actor/_api.py tractor/to_actor/__init__.py`
|
|
||||||
|
|
||||||
Resolve target functions through `Actor._get_rpc_func()` so module
|
|
||||||
authorization remains authoritative. Automatically expose the helper
|
|
||||||
module for actors owned by `to_actor.run()` and document explicit
|
|
||||||
exposure for a caller-owned portal.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py examples/parallelism/to_actor_one_shots.py`
|
|
||||||
|
|
||||||
Cover placement modes, argument binding, nested partials, caller-linked
|
|
||||||
cancellation, remote errors and module authorization.
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: ses_3e4c90d3eafeqHEtRYSIHgHhpA
|
|
||||||
timestamp: 2026-08-19T02:07:57Z
|
|
||||||
git_ref: b38efed7
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260819T020757Z_b38efed7_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Resolve the remaining P3 review finding before landing PR #481:
|
|
||||||
`runtime_kwargs={}` must not be silently accepted alongside either
|
|
||||||
`an=` or `portal=` merely because the dict is falsey. Keep this as its
|
|
||||||
own final review-remediation commit.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Treat any non-`None` `runtime_kwargs` value as provided when validating
|
|
||||||
placement. Cover both placement APIs with empty and configured dicts,
|
|
||||||
proving the error is raised locally before actor startup.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/to_actor/_api.py` - validate option presence explicitly.
|
|
||||||
- `tests/test_to_actor.py` - cover four invalid option combinations.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
No direct line edits. The human accepted the P3 finding, required it to
|
|
||||||
remain separate from the five P2 behavioral commits and prioritized it
|
|
||||||
before the final PR #484 integration rebase and PR #481 landing steps.
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-19T02:07:57Z
|
|
||||||
git_ref: b38efed7
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix the final PR #481 review finding: `runtime_kwargs` is mutually
|
|
||||||
exclusive with both caller placement options whenever it is provided,
|
|
||||||
including an empty dict.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/to_actor/_api.py tests/test_to_actor.py`
|
|
||||||
|
|
||||||
Use an explicit `is not None` check rather than dict truthiness. Expand
|
|
||||||
the validation regression across `an=` and `portal=`, each with empty
|
|
||||||
and configured runtime kwargs, so every invalid combination fails
|
|
||||||
before actor runtime startup.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
- Trio/TCP: `23 passed`
|
|
||||||
- Trio/UDS: `23 passed`
|
|
||||||
- `mp_spawn`/TCP: `23 passed`
|
|
||||||
- Ruff and `git diff --check`: clean
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-19T18:46:40Z
|
|
||||||
git_ref: 481ba003
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260819T184640Z_481ba003_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Rebase PR #484 onto final PR #481, migrate every affected one-shot call
|
|
||||||
to the new positional target API and continue through downstream tests,
|
|
||||||
examples and documentation review.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Converted stale target keyword calls to target partials so previously
|
|
||||||
named inputs remain explicit while placement/runtime controls stay
|
|
||||||
direct. Updated error expectations for local signature validation and
|
|
||||||
linked remote error propagation, then corrected docs which still
|
|
||||||
described the removed one-shot implementation. Linked spawning and
|
|
||||||
context lifecycle prose to the corresponding API methods and detailed
|
|
||||||
context guide.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `docs/api/core.rst` - describe linked one-shot context execution.
|
|
||||||
- `docs/guide/rpc.rst` - update placement and target call semantics.
|
|
||||||
- `docs/guide/spawning.rst` - document positional target inputs.
|
|
||||||
- `examples/debugging/multi_nested_subactors_error_up_through_nurseries.py` - migrate nested actor target inputs.
|
|
||||||
- `examples/debugging/root_cancelled_but_child_is_in_tty_lock.py` - preserve named recursive target inputs with partials.
|
|
||||||
- `tests/test_advanced_streaming.py` - migrate streaming target inputs.
|
|
||||||
- `tests/test_cancellation.py` - migrate calls and tighten errors.
|
|
||||||
- `tests/test_infected_asyncio.py` - bind asyncio target options.
|
|
||||||
- `tests/test_rpc.py` - migrate RPC target argument binding.
|
|
||||||
- `tests/test_runtime.py` - preserve named runtime target inputs.
|
|
||||||
- `tests/test_spawning.py` - preserve named spawning target inputs.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human selected the stack order and final PR #481 base, asked the
|
|
||||||
agent to continue after each diagnostic step and required a complete
|
|
||||||
commit plan after independently force-pushing the rebased history.
|
|
||||||
After reviewing the migration, the human required every formerly named
|
|
||||||
target input to remain visibly named through `functools.partial()`
|
|
||||||
rather than becoming positional. These were human-directed agent edits;
|
|
||||||
the human also required plain `start_actor()` and `open_context()`
|
|
||||||
references in the spawning and RPC guides to link to their API methods
|
|
||||||
and the detailed context guide, then clarified that `to_actor.run()`
|
|
||||||
already uses the full context API while `Portal.run()` should share
|
|
||||||
linked lifecycle machinery without necessarily delegating through
|
|
||||||
`Portal.open_context()` or adding a `Started` message. The human made
|
|
||||||
no direct source-line edits.
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-19T18:46:40Z
|
|
||||||
git_ref: 481ba003
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Migrate PR #484's downstream one-shot calls to PR #481's final
|
|
||||||
`tractor.to_actor.run()` contract after the stack rebase.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- docs examples tests`
|
|
||||||
|
|
||||||
Pass target arguments positionally and bind target keyword-only inputs
|
|
||||||
with `functools.partial()`. Keep placement and runtime controls as
|
|
||||||
direct `to_actor.run()` keywords. Update the invalid-target-argument
|
|
||||||
test to expect local signature binding before actor startup and require
|
|
||||||
direct `RemoteActorError` propagation from linked one-shots.
|
|
||||||
|
|
||||||
Update API and guide prose to describe positional target inputs,
|
|
||||||
linked `Portal.open_context()` execution and per-child reaping instead
|
|
||||||
of the removed `Portal.run()` and target-`**kwargs` conventions.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
- core and migrated runtime batches: `97 passed`
|
|
||||||
- discovery and related lifecycle batch: `33 passed, 1 skipped`
|
|
||||||
- changed executable examples: `9 passed`
|
|
||||||
- mapped debugger cases: `12 passed, 6 skipped`
|
|
||||||
- Ruff, compilation and `git diff --check`: clean
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-19T23:48:23Z
|
|
||||||
git_ref: 557065d8
|
|
||||||
scope: tests
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260819T234823Z_557065d8_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Investigate PR #481's red CI run, explain the missing T-800 and
|
|
||||||
debugger-output failures, and proceed with fixes in the PR #481
|
|
||||||
worktree.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Updated stale teardown assertions to match #481's direct hard-reap
|
|
||||||
path and observable process-lifetime invariants. Made nested debugger
|
|
||||||
checks consume the complete pexpect transcript rather than only the
|
|
||||||
last prompt latch.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tests/devx/test_debugger.py` - assert EOF/dead-process teardown and
|
|
||||||
accumulate nested debugger output across prompt boundaries.
|
|
||||||
- `tests/devx/test_tooling.py` - assert cancel-timeout hard-reap
|
|
||||||
escalation instead of the bypassed T-800 backend marker.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human reported the still-red PR #481 CI, supplied a failing job URL,
|
|
||||||
required work in `/wkts/pr481_review_fixes` and directed the agent to
|
|
||||||
continue immediately. No direct source-line edits were made by the
|
|
||||||
human.
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-19T23:48:23Z
|
|
||||||
git_ref: 557065d8
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Diagnose and fix the stale debugger and reaper assertions failing PR
|
|
||||||
#481's Unix CI jobs.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/devx/test_debugger.py tests/devx/test_tooling.py`
|
|
||||||
|
|
||||||
Replace the old T-800 backend-log requirement with the new bounded
|
|
||||||
cancel-ack escalation evidence. Prove debugger teardown with EOF and a
|
|
||||||
dead child process instead of requiring optional `KeyboardInterrupt`
|
|
||||||
text. Accumulate all pexpect prompt chunks for nested error propagation
|
|
||||||
so expected tracebacks are not lost when `child.before` advances.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
- exact failed debugger/reaper nodes: `4 passed`
|
|
||||||
- debugger/tooling TCP: `39 passed, 6 skipped`
|
|
||||||
- debugger/tooling UDS: `39 passed, 6 skipped`
|
|
||||||
- full TCP suite: `478 passed, 9 skipped, 7 xfailed, 3 xpassed`
|
|
||||||
- full UDS rerun: `476 passed, 11 skipped, 8 xfailed, 2 xpassed`
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-19T23:48:24Z
|
|
||||||
git_ref: 557065d8
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260819T234824Z_557065d8_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Investigate and fix PR #481's macOS TCP clustering and stream-overrun
|
|
||||||
failures without sacrificing IPC frame integrity or structured
|
|
||||||
concurrency.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Changed cancellation during `send_all()` from actor-wide stream closure
|
|
||||||
to shielded complete-frame publication followed by immediate pending
|
|
||||||
cancellation. Prevented failed overrun error shipment from promoting a
|
|
||||||
secondary transport closure over the context-local primary condition.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/ipc/_transport.py` - complete in-flight frames before
|
|
||||||
delivering sender cancellation.
|
|
||||||
- `tractor/_context.py` - absorb transport closure while reporting an
|
|
||||||
overrun on an already-closing channel.
|
|
||||||
- `tests/ipc/test_each_tpt.py` - prove complete framing, cancellation
|
|
||||||
delivery and channel reuse.
|
|
||||||
- `tests/test_context_stream_semantics.py` - prove overrun reporting
|
|
||||||
tolerates a closed transport.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human reported PR #481's red CI, asked for diagnosis and directed
|
|
||||||
the agent to proceed in the dedicated PR #481 worktree. During final
|
|
||||||
review, the human required preservation of the original far-end
|
|
||||||
cancellation rationale and fuller documentation of frame shielding,
|
|
||||||
shared-channel ownership and cancellation-delay tradeoffs. These were
|
|
||||||
human-directed agent edits; the human made no direct source-line edits.
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-19T23:48:24Z
|
|
||||||
git_ref: 557065d8
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Fix the macOS TCP regressions where cancellation during a framed send
|
|
||||||
closed the actor-wide channel and replaced primary stream errors with
|
|
||||||
secondary `TransportClosed` failures.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/ipc/_transport.py tractor/_context.py tests/ipc/test_each_tpt.py tests/test_context_stream_semantics.py`
|
|
||||||
|
|
||||||
Shield complete frame publication, then deliver pending cancellation
|
|
||||||
immediately after leaving the shield. Preserve channel reuse instead of
|
|
||||||
closing the multiplexed socket from a context-local sender. Treat
|
|
||||||
`TransportClosed` while shipping `StreamOverrun` as failed delivery so
|
|
||||||
the secondary error can not crash the actor-wide RPC loop.
|
|
||||||
|
|
||||||
Add deterministic unit regressions for cancellation in the middle of a
|
|
||||||
frame and overrun reporting after transport closure.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
- transport/context unit regressions: `3 passed`
|
|
||||||
- exact TCP and UDS CI-node batches: `11 passed, 1 skipped`
|
|
||||||
- transport/context/clustering/RPC TCP: `88 passed`
|
|
||||||
- transport/context/clustering/RPC UDS: `86 passed, 2 skipped`
|
|
||||||
- full TCP suite: `478 passed, 9 skipped, 7 xfailed, 3 xpassed`
|
|
||||||
- full UDS rerun: `476 passed, 11 skipped, 8 xfailed, 2 xpassed`
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-20T02:30:04Z
|
|
||||||
git_ref: 88a23449
|
|
||||||
scope: tests
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260820T023004Z_88a23449_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Inspect the two failed macOS jobs in PR #481's new CI run and continue
|
|
||||||
toward a green landing candidate.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Confirmed both jobs fail only the known nested crash-REPL scenario from
|
|
||||||
issue #320, while Ubuntu TCP/UDS and Windows pass. Added a targeted
|
|
||||||
macOS-CI skip without reducing Linux coverage.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tests/devx/test_debugger.py` - skip the issue #320 nested
|
|
||||||
crash-REPL node on Darwin CI.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human monitored the new CI run, reported both macOS jobs dead and
|
|
||||||
directed the agent to continue diagnosis. No direct source-line edits
|
|
||||||
were made by the human.
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-20T02:30:04Z
|
|
||||||
git_ref: 88a23449
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Diagnose the remaining macOS PR #481 CI failures after the Linux
|
|
||||||
debugger and transport fixes passed.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/devx/test_debugger.py`
|
|
||||||
|
|
||||||
Both macOS transports failed the same deeply nested crash-REPL test
|
|
||||||
already tracked by issue #320: TCP omitted one actor-specific traceback
|
|
||||||
record and UDS timed out waiting for a nested prompt. Apply an explicit
|
|
||||||
Darwin-CI skip to this one node while retaining Linux TCP/UDS coverage.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
- debugger/tooling TCP: `39 passed, 6 skipped`
|
|
||||||
- debugger/tooling UDS: `39 passed, 6 skipped`
|
|
||||||
- Ruff, compilation and `git diff --check`: clean
|
|
||||||
|
|
@ -1,40 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-20T02:30:05Z
|
|
||||||
git_ref: 88a23449
|
|
||||||
scope: docs
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260820T023005Z_88a23449_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Audit all documentation and executable examples once more, replacing
|
|
||||||
prescriptive `run_in_actor()` usage with `to_actor.run()` or explicit
|
|
||||||
actor/context lifetime APIs before PR #481 lands.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Rewrote one-shot documentation around direct blocking result delivery,
|
|
||||||
linked context execution and per-call reaping. Migrated all runnable
|
|
||||||
examples, using daemon actors where reciprocal dialogs require longer
|
|
||||||
lifetimes. Added API/guide cross-links and retained only three explicit
|
|
||||||
legacy references.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `docs/` - update API, quickstart and subsystem guides to showcase
|
|
||||||
`tractor.to_actor.run()` and link its underlying core APIs.
|
|
||||||
- `examples/` - migrate one-shot calls and preserve explicit daemon
|
|
||||||
lifetimes for reciprocal or long-lived actor dialogs.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human requested a final docs pass covering every place that should
|
|
||||||
showcase `to_actor` over `.run_in_actor()`. Earlier review also required
|
|
||||||
named target arguments to remain visible through `functools.partial()`
|
|
||||||
and core API references to link to local guides/reference pages. These
|
|
||||||
were human-directed agent edits; the human made no direct source-line
|
|
||||||
edits.
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-20T02:30:05Z
|
|
||||||
git_ref: 88a23449
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Perform a final rendered-documentation and executable-example pass so
|
|
||||||
PR #481 showcases `tractor.to_actor.run()` instead of the legacy
|
|
||||||
`ActorNursery.run_in_actor()` API.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- docs examples`
|
|
||||||
|
|
||||||
Migrate one-shot guides and examples to direct result delivery through
|
|
||||||
`to_actor.run()`, preserving named target inputs with target partials.
|
|
||||||
Use daemon actors and concurrent portal calls where reciprocal actor
|
|
||||||
lifetimes require both peers to coexist. Add API and guide cross-links,
|
|
||||||
and retain only explicit legacy/removal notes.
|
|
||||||
|
|
||||||
Verification:
|
|
||||||
|
|
||||||
- executable docs examples: `23 passed`
|
|
||||||
- debugger/tooling TCP: `39 passed, 6 skipped`
|
|
||||||
- debugger/tooling UDS: `39 passed, 6 skipped`
|
|
||||||
- Ruff, compilation and `git diff --check`: clean
|
|
||||||
- local Sphinx build unavailable because Sphinx is not installed
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-20T13:51:25Z
|
|
||||||
git_ref: 9f99043b
|
|
||||||
scope: tests
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260820T135125Z_9f99043b_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue preparing PR #481 for landing after the prior test and
|
|
||||||
documentation commits were pushed. Follow CI and proceed with clear next
|
|
||||||
steps without merging or changing remote content unasked.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Followed CI through completion and found both macOS jobs failed because the
|
|
||||||
new `skipif` expression returned the `CI=true` environment string. Corrected
|
|
||||||
the condition to pass pytest a boolean before evaluating the marker. A
|
|
||||||
simulated Darwin-CI run now skips cleanly, and the sequential TCP and UDS
|
|
||||||
debugger/tooling suites each pass with 39 passed and 6 skipped.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tests/devx/test_debugger.py` - coerce the Darwin-CI skip condition to a
|
|
||||||
boolean.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human pushed the preceding commits, directed the agent to continue, and
|
|
||||||
approved recording this test-only follow-up in Prompt-IO. No direct
|
|
||||||
source-line edits were made by the human.
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-20T13:51:25Z
|
|
||||||
git_ref: 9f99043b
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Continue preparing PR #481 for landing after the test and documentation
|
|
||||||
commits were pushed. Follow the new CI run to completion and diagnose any
|
|
||||||
failures.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/devx/test_debugger.py`
|
|
||||||
|
|
||||||
Both macOS jobs failed while evaluating the new `skipif` marker. The
|
|
||||||
expression returned the `CI=true` environment string instead of a boolean,
|
|
||||||
so pytest evaluated `true` as Python source and raised `NameError` during
|
|
||||||
test setup. Coerce `_ci_env` to `bool` so pytest receives a boolean marker
|
|
||||||
condition on Darwin CI.
|
|
||||||
|
|
||||||
Verification should exercise the condition with `CI=true` and a simulated
|
|
||||||
Darwin platform, then rerun the debugger/tooling TCP and UDS suites.
|
|
||||||
|
|
@ -1,43 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-20T14:38:50Z
|
|
||||||
git_ref: 559fd0f1
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260820T143845Z_559fd0f1_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue preparing PR #481 after the latest fix was pushed. Follow CI and
|
|
||||||
proceed with clear next steps toward a green landing candidate.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Traced the remaining macOS UDS failure to cancellation racing transport
|
|
||||||
teardown inside the shielded framed-send path. Preserve pending cancellation
|
|
||||||
over a transport error caused by concurrent teardown, and add a deterministic
|
|
||||||
regression for that ordering. A follow-up A/B run showed the corrected
|
|
||||||
cancellation precedence changes which nested debugger intermediary is
|
|
||||||
rendered as the immediate source versus relay, so retain coverage for both
|
|
||||||
actor levels without pinning those racy roles. The adjusted UDS node passes
|
|
||||||
three consecutive runs, and both debugger/tooling transport suites pass with
|
|
||||||
39 passed and 6 skipped.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/ipc/_transport.py` - deliver pending cancellation before
|
|
||||||
translating a shielded send's transport error.
|
|
||||||
- `tests/ipc/test_each_tpt.py` - reproduce cancellation followed by local
|
|
||||||
stream closure during shielded frame publication.
|
|
||||||
- `tests/devx/test_debugger.py` - accept either valid source/relay role for
|
|
||||||
each nested intermediary while retaining the actor and error assertions.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human pushed the preceding fix, ran the proposed verification plan, and
|
|
||||||
reported a repeated UDS debugger failure. That report prompted the A/B
|
|
||||||
comparison and role-insensitive assertion. No direct source-line edits were
|
|
||||||
made by the human.
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-20T14:38:50Z
|
|
||||||
git_ref: 559fd0f1
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Continue preparing PR #481 after pushing the macOS debugger skip fix.
|
|
||||||
Follow the replacement CI run and address any remaining PR-specific
|
|
||||||
failure.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/ipc/_transport.py`
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/ipc/test_each_tpt.py`
|
|
||||||
|
|
||||||
macOS UDS failed `test_reqresp_ontopof_streaming` when its two-second
|
|
||||||
`move_on_after()` scope cancelled during `stream.send('ping')`. Commit
|
|
||||||
`88a23449` shields framed `send_all()` and checks pending cancellation only
|
|
||||||
after a successful write. Concurrent transport teardown instead closed the
|
|
||||||
socket, causing `ClosedResourceError` to escape as `TransportClosed` before
|
|
||||||
the pending cancellation could be delivered.
|
|
||||||
|
|
||||||
Preserve structured cancellation precedence on the shielded send's
|
|
||||||
transport-error path, and add a deterministic regression that cancels the
|
|
||||||
sender before making the fake stream raise `ClosedResourceError`.
|
|
||||||
|
|
@ -1,33 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 7b9c97c4-fff7-4ac4-97fb-35720453308e
|
|
||||||
timestamp: 2026-08-20T15:02:50Z
|
|
||||||
git_ref: pformat_caller_frame_render_guard
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260820T150250Z_9afda1c6_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Fix both newly exposed send-side `MsgTypeError` formatting failures
|
|
||||||
and pin them with an end-to-end regression in PR #503.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Corrected codec-spec formatting and default error-message assembly so
|
|
||||||
`_mk_send_mte()` returns a printable error instead of raising another
|
|
||||||
formatter exception.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/msg/_codec.py` - pass the codec to its supported formatter.
|
|
||||||
- `tractor/_exceptions.py` - assemble the default message as `str`.
|
|
||||||
- `tests/devx/test_pformat.py` - render the complete default error.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human selected both one-line fixes and the single end-to-end test
|
|
||||||
as coherent additions to PR #503, while leaving broader formatter
|
|
||||||
cleanup out of scope.
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-20T15:02:50Z
|
|
||||||
git_ref: pformat_caller_frame_render_guard
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
After reviewing additional `tractor.devx.pformat` work suitable for
|
|
||||||
PR #503, the user approved fixing both send-side `MsgTypeError`
|
|
||||||
formatting failures and adding an end-to-end regression.
|
|
||||||
|
|
||||||
## Response
|
|
||||||
|
|
||||||
The generated code corrects the `MsgCodec.msg_spec_str` formatter
|
|
||||||
input, keeps `_mk_send_mte()`'s assembled default message a string,
|
|
||||||
and tests that the resulting `MsgTypeError` can be rendered:
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/msg/_codec.py tractor/_exceptions.py tests/devx/test_pformat.py`
|
|
||||||
|
|
||||||
These failures were hidden behind the original
|
|
||||||
`pformat_caller_frame()` keyword error addressed by the first two
|
|
||||||
commits on the branch.
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-21T02:35:37Z
|
|
||||||
git_ref: ae6f2ac3
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260821T023537Z_ae6f2ac3_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Simplify bounded actor cancellation by passing an explicit absolute
|
|
||||||
deadline from `Portal.cancel_actor()` through `_run_from_ns()`,
|
|
||||||
`Actor.start_remote_task()`, and `Channel.send()` into
|
|
||||||
`MsgpackTransport.send()`. Avoid a `ContextVar`, watcher tasks, shared
|
|
||||||
status, coalescing, and waiter state. After tracing the current
|
|
||||||
`Start -> StartAck -> CancelAck` transaction, rename the local result to
|
|
||||||
`cancel_ack_received`, document its exact semantics, and link a focused
|
|
||||||
follow-up for a dedicated `Cancel -> CancelAck` protocol.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Threaded one absolute Trio deadline through the existing private
|
|
||||||
actor-cancel RPC path. The transport retains complete-frame shielding
|
|
||||||
for ordinary sends, while a cancel-control send that overruns its
|
|
||||||
deadline force-closes the potentially corrupted stream before releasing
|
|
||||||
the send lock. The outer actor-cancel scope uses the same deadline for
|
|
||||||
ack waiting and redelivers pending caller cancellation afterward.
|
|
||||||
|
|
||||||
Renamed the completion flag to `cancel_ack_received` and documented that
|
|
||||||
the current private call consumes `StartAck`, then receives a real
|
|
||||||
`CancelAck` after `Actor.cancel()` completes; this does not establish
|
|
||||||
that the OS process exited. Added a source TODO linking issue #506 for
|
|
||||||
the future first-class `Cancel -> CancelAck` transaction.
|
|
||||||
|
|
||||||
Focused transport and actor-cancel verification passed all four tests.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_portal.py` - own the absolute deadline, accurately
|
|
||||||
record ack receipt, and link the dedicated cancellation protocol.
|
|
||||||
- `tractor/runtime/_runtime.py` - forward the optional deadline for the
|
|
||||||
exact private `Start` publication.
|
|
||||||
- `tractor/ipc/_chan.py` - pass the operation-specific deadline to the
|
|
||||||
transport without changing ordinary sends.
|
|
||||||
- `tractor/ipc/_transport.py` - bound the shielded frame publication and
|
|
||||||
close a partial-frame stream before unlocking it.
|
|
||||||
- `tests/ipc/test_each_tpt.py` - cover deadline expiry after a partial
|
|
||||||
frame prefix reaches the stream.
|
|
||||||
- `tests/test_to_actor.py` - prove actor-cancel publication and ack
|
|
||||||
waiting share one absolute timeout budget.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human rejected the initial watcher-task, shared `_SendStatus`, cancel
|
|
||||||
coalescing, and per-waiter design as unnecessary complexity. They also
|
|
||||||
rejected `ContextVar` propagation in favor of explicit functional
|
|
||||||
threading, selected a single absolute deadline for publication and ack
|
|
||||||
waiting, and required item 2 to remain separate from the item-3 child
|
|
||||||
reaping work. After reviewing the result, they requested the precise
|
|
||||||
`cancel_ack_received` name, a detailed protocol-trace comment, a focused
|
|
||||||
follow-up issue, and a linked source TODO. No direct source-line edits
|
|
||||||
were made by the human.
|
|
||||||
|
|
@ -1,41 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-21T02:35:37Z
|
|
||||||
git_ref: ae6f2ac3
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Replace the actor-cancel timeout watcher/status experiment with one
|
|
||||||
explicit absolute deadline threaded through the existing private call
|
|
||||||
path. Do not use a `ContextVar`, shared result state, waiter
|
|
||||||
coalescing, or polling tasks.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_portal.py`
|
|
||||||
|
|
||||||
`Portal.cancel_actor()` computes one absolute deadline and uses it for
|
|
||||||
both `Start` frame publication and the subsequent cancel-ack wait.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_runtime.py`
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/ipc/_chan.py`
|
|
||||||
|
|
||||||
The private RPC path forwards the operation-specific deadline. Lower
|
|
||||||
layers preserve the ordinary infinite-deadline call shape.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/ipc/_transport.py`
|
|
||||||
|
|
||||||
`MsgpackTransport.send()` applies the deadline inside its complete-frame
|
|
||||||
shield. If the deadline expires after partial publication, it closes
|
|
||||||
the unusable stream before releasing the send lock.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/ipc/test_each_tpt.py`
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
|
|
||||||
|
|
||||||
Focused regressions prove a partial-frame timeout closes the stream and
|
|
||||||
that actor-cancel publication and acknowledgement share one budget.
|
|
||||||
|
|
||||||
The implementation removes the earlier `_SendStatus`, watcher task,
|
|
||||||
coalescing, shared cancel result, and per-waiter state. Four focused
|
|
||||||
transport and actor-cancel tests pass.
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-21T04:08:03Z
|
|
||||||
git_ref: 3c1bbe73
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260821T040803Z_3c1bbe73_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue PR #481 review cleanup one item at a time. Compare the
|
|
||||||
remaining child-registration/reaping experiment against PR #484,
|
|
||||||
identify the next valid behavior, and generate an exact commit plan for
|
|
||||||
that boundary without committing automatically.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Separated late-child admission from the broader bounded-reap rewrite.
|
|
||||||
Added one synchronous `ActorNursery._register_child()` publication step
|
|
||||||
that stores the child and its reap events before sampling nursery
|
|
||||||
cancellation. This closes both scheduler orderings around
|
|
||||||
`ActorNursery.cancel()`'s child snapshot.
|
|
||||||
|
|
||||||
The MP backend registers immediately before synchronous process startup
|
|
||||||
and refuses to start when cancellation already owns the child. The Trio
|
|
||||||
backend registers immediately after `open_process()` and kills that
|
|
||||||
already-created process when registration observes cancellation. An
|
|
||||||
early `start_actor()` guard rejects calls begun after cancellation is
|
|
||||||
already visible.
|
|
||||||
|
|
||||||
Deterministic tests cover the nursery registration ordering and the MP
|
|
||||||
no-start invariant. Comparison with PR #484 confirmed that its retained
|
|
||||||
generic nursery/backends do not close this race.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_supervise.py` - atomically publish child ownership
|
|
||||||
and reject actor starts after nursery cancellation.
|
|
||||||
- `tractor/spawn/_mp.py` - register before synchronous process startup
|
|
||||||
and abort a cancellation-owned child.
|
|
||||||
- `tractor/spawn/_trio.py` - register immediately after process creation,
|
|
||||||
kill a cancellation-owned child, and remove its stale unused import.
|
|
||||||
- `tests/test_to_actor.py` - cover late registration and MP startup
|
|
||||||
suppression.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human required review extras to be handled one item and one
|
|
||||||
behavioral commit at a time, with each item compared against PR #484
|
|
||||||
before acceptance. That direction split this late-registration fix from
|
|
||||||
the original broad experiment's bounded post-ack reaping,
|
|
||||||
`ActorNursery.cancel()` hard-reap rewrite, and debugger/error behavior.
|
|
||||||
The human accepted the narrower late-registration boundary by requesting
|
|
||||||
its commit plan. No direct source-line edits were made by the human.
|
|
||||||
|
|
@ -1,48 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-21T04:08:03Z
|
|
||||||
git_ref: 3c1bbe73
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Compare the remaining child-registration and reaping experiment with
|
|
||||||
PR #484, then identify the next review item without changing code.
|
|
||||||
|
|
||||||
The next item is the late-child admission race. A spawn can pass
|
|
||||||
`ActorNursery.start_actor()`'s early cancellation check, then be absent
|
|
||||||
from `ActorNursery.cancel()`'s child snapshot and register afterward.
|
|
||||||
The existing reap-request latch releases its monitor but does not send
|
|
||||||
runtime cancellation, so the monitor can wait forever for a still-live
|
|
||||||
process.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
|
|
||||||
|
|
||||||
`ActorNursery._register_child()` publishes the child, installs its reap
|
|
||||||
events, and samples `ActorNursery._cancel_called` without a checkpoint.
|
|
||||||
The two scheduler orderings are then complete: registration first puts
|
|
||||||
the child in the cancel snapshot, while cancellation first makes the
|
|
||||||
backend abort the late registration.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_mp.py`
|
|
||||||
|
|
||||||
The multiprocessing backend registers immediately before `proc.start()`
|
|
||||||
and refuses to start a process already owned by nursery cancellation.
|
|
||||||
There is no Trio checkpoint between registration and process startup.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_trio.py`
|
|
||||||
|
|
||||||
The Trio backend registers immediately after `open_process()` and kills
|
|
||||||
the newly opened process if cancellation won the registration race. Its
|
|
||||||
stale unused `get_runtime_vars` import is removed so the touched module
|
|
||||||
remains lint-clean.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
|
|
||||||
|
|
||||||
Deterministic regressions prove late registration observes cancellation
|
|
||||||
and that the MP backend never starts a process after cancellation owns
|
|
||||||
its registration.
|
|
||||||
|
|
||||||
PR #484 retains the affected generic nursery and spawn-backend paths and
|
|
||||||
does not close this race. Keep this fix in PR #481 as its own commit;
|
|
||||||
review bounded post-`CancelAck` reaping separately.
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
---
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: tractor-addr-unpacking
|
|
||||||
timestamp: 2026-08-21T05:20:52Z
|
|
||||||
git_ref: 3690e43a
|
|
||||||
scope: config
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260821T052052Z_3690e43a_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
The human asked for a main-first patch using an off-the-shelf pytest
|
|
||||||
plugin to cope with tractor's changing macOS CI flakes without mixing
|
|
||||||
that mitigation into PR #505.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Added `pytest-rerunfailures` to tractor's testing dependencies and
|
|
||||||
configured the GitHub Actions matrix to retry failures only on macOS.
|
|
||||||
Linux and Windows remain strict first-attempt runs, while persistent
|
|
||||||
macOS failures still fail after two visible reruns.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `.github/workflows/ci.yml` - macOS-only pytest rerun budget.
|
|
||||||
- `pyproject.toml` - testing plugin dependency and rationale.
|
|
||||||
- `uv.lock` - resolved `pytest-rerunfailures` package metadata.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human selected a main-first mitigation after PR #505 failed two
|
|
||||||
different macOS tests on consecutive runs and required the change to
|
|
||||||
remain an incremental patch with its own commit plan. The agent
|
|
||||||
implemented and verified that direction; no direct manual source
|
|
||||||
edits were observed.
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
---
|
|
||||||
model: gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-21T05:20:52Z
|
|
||||||
git_ref: 3690e43a
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
# Raw output - retry flaky macOS CI tests
|
|
||||||
|
|
||||||
The human requested an off-the-shelf pytest plugin patch suitable
|
|
||||||
for landing directly on tractor `main` after PR #505's macOS job
|
|
||||||
failed two different timing-sensitive tests on consecutive runs.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- .github/workflows/ci.yml pyproject.toml uv.lock`
|
|
||||||
|
|
||||||
Added the pytest-dev-maintained `pytest-rerunfailures` plugin and
|
|
||||||
gave only the macOS matrix leg two reruns with a one-second delay.
|
|
||||||
Linux and Windows receive a zero retry budget; deterministic macOS
|
|
||||||
failures still fail after the final attempt and reruns remain visible
|
|
||||||
in pytest output.
|
|
||||||
|
|
||||||
The lockfile is current, actionlint passed, all 471 tests collected,
|
|
||||||
and the four tests covering both observed PR #505 failure areas
|
|
||||||
passed with the rerun plugin enabled.
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: 76c5d31c-5a2f-4503-9b16-410ee7f4fab3
|
|
||||||
timestamp: 2026-08-22T02:25:26Z
|
|
||||||
git_ref: eb3c99c9
|
|
||||||
scope: config
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260822T022526Z_5562fd9a_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Perform a full Tractor repository scan for related `ai.skillz` work,
|
|
||||||
then correct the run-tests landing branch, prune unrelated `.gitignore`
|
|
||||||
additions, preserve only the focused migration, and provide canonical
|
|
||||||
deployment commands.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Audited all local branches, worktrees, affected-path history, deployment
|
|
||||||
state, canonical skill dependencies, and current Tractor harness behavior.
|
|
||||||
Corrected the local test reference where it overstated cleanup safety or
|
|
||||||
omitted current environment, platform, debugger, timeout, and CI details.
|
|
||||||
Narrowed the correction commit to three managed `run-tests` deployment
|
|
||||||
blocks. A later dedicated commit records the complete generated `ai.skillz`
|
|
||||||
deployment state.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `.claude/skills/run-tests/test-harness-reference.md` - correct the
|
|
||||||
project-specific test and cleanup contract.
|
|
||||||
- `.gitignore` - narrow the correction commit before the later dedicated
|
|
||||||
deployment-state expansion.
|
|
||||||
- `ai/prompt-io/opencode/20260822T022526Z_5562fd9a_prompt_io.md` - record
|
|
||||||
the migration review provenance.
|
|
||||||
- `ai/prompt-io/opencode/20260822T022526Z_5562fd9a_prompt_io.raw.md` -
|
|
||||||
preserve the unedited response record.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human required an existing-work scan after duplicate implementation
|
|
||||||
was discovered, approved correcting the landing branch during PR #481
|
|
||||||
review, and directed removal or reconciliation of unrelated ignore rules.
|
|
||||||
During PR #510 review, the human required the stackscope and shared-memory
|
|
||||||
safety clarifications and immutable provenance pointers before landing. No
|
|
||||||
direct source-line edits were made by the human. Copilot review then prompted
|
|
||||||
the human to require explicit canonical deployment instructions and clarify
|
|
||||||
the later `.gitignore` expansion.
|
|
||||||
|
|
@ -1,24 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-22T02:25:26Z
|
|
||||||
git_ref: eb3c99c9
|
|
||||||
diff_cmd: git diff eb3c99c9^..eb3c99c9
|
|
||||||
---
|
|
||||||
|
|
||||||
Audit Tractor's repository-wide `ai.skillz` state and correct the
|
|
||||||
`wkt/ai_skillz_run_tests_landing` migration before landing it.
|
|
||||||
|
|
||||||
> `git diff eb3c99c9^..eb3c99c9 -- .claude/skills/run-tests/test-harness-reference.md`
|
|
||||||
|
|
||||||
The harness reference is corrected against current Tractor behavior:
|
|
||||||
environment overrides, CI/platform matrices, randomized registry
|
|
||||||
addresses, interactive debugger capture, timeout mechanisms, stackscope
|
|
||||||
artifacts, reaper platform limits, dry-run expansion, and exact
|
|
||||||
change-to-test mappings.
|
|
||||||
|
|
||||||
> `git diff eb3c99c9^..eb3c99c9 -- .gitignore`
|
|
||||||
|
|
||||||
Unrelated Taken and broad OpenCode command ignore additions are removed;
|
|
||||||
only the managed canonical `run-tests` link and command blocks remain in
|
|
||||||
this migration.
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
|
||||||
timestamp: 2026-08-24T22:20:33Z
|
|
||||||
git_ref: ce38cb6f
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260824T222033Z_ce38cb6f_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue the PR #481 review after triage. The human explicitly accepted
|
|
||||||
the proposed merge-blocking `Context.cancel()` deadline update with
|
|
||||||
"keep" and required the work to remain limited to that review item.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Update `Context.cancel()` so one absolute deadline bounds both shielded
|
|
||||||
cancel-request publication and acknowledgement waiting. Add a focused
|
|
||||||
mocked-clock regression for the blocked-publication failure mode and run
|
|
||||||
the narrow cancellation tests.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/_context.py` - forward the cancel transaction's absolute
|
|
||||||
deadline to frame publication.
|
|
||||||
- `tests/test_to_actor.py` - prove blocked context-cancel publication is
|
|
||||||
bounded by the shared deadline.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human retained ownership of review scope and explicitly selected
|
|
||||||
"keep" for this item after receiving keep/defer/drop options. The human
|
|
||||||
required no unrelated cancellation changes and did not directly edit
|
|
||||||
source lines.
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-24T22:20:33Z
|
|
||||||
git_ref: ce38cb6f
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Implement the approved PR #481 review update for `Context.cancel()`.
|
|
||||||
Use one absolute deadline for both cancellation-request frame
|
|
||||||
publication and acknowledgement waiting, without broadening the change
|
|
||||||
to unrelated cancellation behavior.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/_context.py`
|
|
||||||
|
|
||||||
`Context.cancel()` computes one absolute cancellation deadline, uses it
|
|
||||||
for the outer bounded wait, and forwards it through
|
|
||||||
`Portal._run_from_ns()` to shielded frame publication.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
|
|
||||||
|
|
||||||
A deterministic mocked-clock regression arranges a shielded blocked
|
|
||||||
publication and proves that `Context.cancel()` forwards the same deadline
|
|
||||||
which bounds the complete cancel transaction.
|
|
||||||
|
|
||||||
Run the focused cancellation deadline regressions after the edit.
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
|
||||||
timestamp: 2026-08-24T22:36:14Z
|
|
||||||
git_ref: 88d538e3
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260824T223614Z_88d538e3_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue PR #481 review remediation after committing the shared
|
|
||||||
`Context.cancel()` deadline fix. The human accepted the proposed
|
|
||||||
child-reap bookkeeping invariant, asking only that the first fix receive
|
|
||||||
its own commit plan and commit before this update began.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Check that `ActorNursery` removes its paired reap-coordination entries
|
|
||||||
together while preserving valid pre-registration and immediate-cancel
|
|
||||||
paths. Extend the existing real-runtime reap tests to prove all three
|
|
||||||
child bookkeeping mappings are empty before `to_actor.run()` returns.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_supervise.py` - assert paired reap-map cleanup.
|
|
||||||
- `tests/test_to_actor.py` - verify graceful and hard-reap bookkeeping.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human explicitly accepted this invariant update but directed the
|
|
||||||
preceding cancellation fix to be planned and committed as a separate
|
|
||||||
boundary first. No direct source-line edits were made by the human.
|
|
||||||
|
|
@ -1,26 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-24T22:36:14Z
|
|
||||||
git_ref: 88d538e3
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Implement the approved PR #481 child-reap bookkeeping update after the
|
|
||||||
preceding `Context.cancel()` fix was committed separately.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
|
|
||||||
|
|
||||||
`ActorNursery._mark_child_reaped()` captures both reap-coordination
|
|
||||||
entries and asserts that they are either both present or both absent.
|
|
||||||
It intentionally does not require the reap-request event to be set,
|
|
||||||
because backend cancellation can reap immediately after registration.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
|
|
||||||
|
|
||||||
Existing real-runtime graceful and hard-reap tests verify that
|
|
||||||
`ActorNursery._children`, `ActorNursery._child_reap_requests`, and
|
|
||||||
`ActorNursery._child_reaped` are all empty before the one-shot call
|
|
||||||
returns.
|
|
||||||
|
|
||||||
Run focused bookkeeping and real-runtime reap tests after the edit.
|
|
||||||
|
|
@ -1,39 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
|
||||||
timestamp: 2026-08-24T22:53:56Z
|
|
||||||
git_ref: 2f86dd1a
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260824T225356Z_2f86dd1a_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue PR #481 review remediation after committing the paired
|
|
||||||
`ActorNursery` reap-state invariant. The human selected "keep" for the
|
|
||||||
reviewer's request to factor a duplicated debugger predicate in
|
|
||||||
`_try_cancel_then_kill()`.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Factor the child/tree debugger predicate into a local sampler used both
|
|
||||||
before and after the cancel-RPC checkpoint. Preserve dynamic debugger
|
|
||||||
lock re-evaluation and its distinction from root-wide debug mode.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_supervise.py` - factor the duplicated debugger
|
|
||||||
predicate without changing cancellation behavior.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human explicitly selected "keep" after receiving keep/defer/drop
|
|
||||||
options for this isolated review item. During commit-plan review, the
|
|
||||||
agent found that a single pre-checkpoint snapshot could become stale;
|
|
||||||
the human selected a local helper which re-evaluates the lock after the
|
|
||||||
cancel RPC. The human then considered moving the predicate into
|
|
||||||
`.devx.debug` and accepted keeping it local after confirming that no
|
|
||||||
existing helper shares its supervisor-owned semantics. No direct
|
|
||||||
source-line edits were made by the human.
|
|
||||||
|
|
@ -1,18 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-24T22:53:56Z
|
|
||||||
git_ref: 2f86dd1a
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Implement the approved PR #481 review refactor in
|
|
||||||
`_try_cancel_then_kill()` without changing debugger behavior.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
|
|
||||||
|
|
||||||
Compute the child/tree debugger predicate once, reuse it in the broader
|
|
||||||
hard-kill protection predicate, and pass it directly to
|
|
||||||
`debug.maybe_wait_for_debugger()`.
|
|
||||||
|
|
||||||
Run focused debugger/cancellation coverage and lint after the edit.
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
|
||||||
timestamp: 2026-08-24T23:39:57Z
|
|
||||||
git_ref: 5327b25e
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260824T233957Z_5327b25e_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue PR #481 review remediation after committing the debugger-state
|
|
||||||
sampler. The human selected "keep" for the paired review request to use
|
|
||||||
`Aid` objects as keys in the newly added reap-coordination maps.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Migrate only `ActorNursery._child_reap_requests` and
|
|
||||||
`ActorNursery._child_reaped` to `Aid` keys. Preserve the legacy
|
|
||||||
`ActorNursery._children` `.uid` key and pass full actor identities
|
|
||||||
through the narrow process-monitor bookkeeping path.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/runtime/_supervise.py` - key fresh reap maps by `Aid`.
|
|
||||||
- `tractor/spawn/_spawn.py` - pass `Aid` into completed-reap cleanup.
|
|
||||||
- `tests/test_to_actor.py` - exercise `Aid` registration keys.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human explicitly selected "keep" after reviewing the scope,
|
|
||||||
performance, and mutability tradeoffs. The human retained the legacy
|
|
||||||
tuple key for `_children` and accepted `Aid` for only the two fresh
|
|
||||||
private mappings. No direct source-line edits were made by the human.
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-24T23:39:57Z
|
|
||||||
git_ref: 5327b25e
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Implement the approved PR #481 review update which uses `Aid` keys for
|
|
||||||
the two fresh `ActorNursery` reap-coordination maps while preserving the
|
|
||||||
legacy `.uid` key for `ActorNursery._children`.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/runtime/_supervise.py`
|
|
||||||
|
|
||||||
Type and access `_child_reap_requests` and `_child_reaped` by `Aid`.
|
|
||||||
Pass full actor identities through registration, cancellation, and
|
|
||||||
completed-reap bookkeeping, deriving `.uid` only for `_children`.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_spawn.py`
|
|
||||||
|
|
||||||
Forward `subactor.aid` when publishing completed process teardown.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tests/test_to_actor.py`
|
|
||||||
|
|
||||||
Update deterministic registration tests to exercise `Aid` map keys.
|
|
||||||
|
|
||||||
Run focused registration/reaping tests and the full `to_actor` suite.
|
|
||||||
|
|
@ -1,38 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
|
||||||
timestamp: 2026-08-25T01:57:42Z
|
|
||||||
git_ref: e42ecb55
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260825T015742Z_e42ecb55_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue PR #481 review remediation after committing the `Aid` reap-map
|
|
||||||
migration. The human selected "keep" for comments explaining why both
|
|
||||||
spawn backends provisionally register children with `portal=None`.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Document that a child has no `Portal` until its IPC handshake yields a
|
|
||||||
`Channel`, and make `portal=None` explicit at both registration calls.
|
|
||||||
Identify the later replacement of each provisional entry with
|
|
||||||
`Portal(chan)`. Update the MP registration test double to accept and
|
|
||||||
assert the explicit provisional portal state. Name every registration
|
|
||||||
argument consistently in both backends.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/spawn/_mp.py` - clarify provisional MP registration.
|
|
||||||
- `tractor/spawn/_trio.py` - clarify provisional Trio registration.
|
|
||||||
- `tests/test_to_actor.py` - model explicit provisional registration.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human explicitly selected "keep" after receiving keep/defer/drop
|
|
||||||
options for this paired clarification. During local review, the human
|
|
||||||
then requested that `subactor` and `proc` also be passed by name in both
|
|
||||||
backend calls. No direct source-line edits were made by the human.
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-25T01:57:42Z
|
|
||||||
git_ref: e42ecb55
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Implement the approved PR #481 clarification for provisional child
|
|
||||||
registration in both process-spawn backends.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_mp.py`
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/spawn/_trio.py`
|
|
||||||
|
|
||||||
Explain that `portal=None` is provisional because no `Portal` can exist
|
|
||||||
until the child completes its IPC handshake and returns a `Channel`.
|
|
||||||
Use an explicit keyword argument and identify the later replacement with
|
|
||||||
`Portal(chan)`.
|
|
||||||
|
|
||||||
Run lint and the full `to_actor` runtime suite.
|
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: d9d7df2c-7044-463f-8768-ec024718eac9
|
|
||||||
timestamp: 2026-08-25T02:13:19Z
|
|
||||||
git_ref: ce430fca
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260825T021319Z_ce430fca_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Continue PR #481 review remediation after committing provisional child
|
|
||||||
registration clarifications. The human selected "keep" for inlining the
|
|
||||||
guarded `functools.Placeholder` lookup with a walrus assignment.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Remove the standalone placeholder assignment and bind the optional
|
|
||||||
Python 3.14 sentinel directly in the existing conditional while
|
|
||||||
preserving compatibility behavior.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/to_actor/_api.py` - inline placeholder feature detection.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human explicitly selected "keep" after receiving keep/defer/drop
|
|
||||||
options for this isolated cleanup. No direct source-line edits were made
|
|
||||||
by the human.
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-25T02:13:19Z
|
|
||||||
git_ref: ce430fca
|
|
||||||
diff_cmd: git diff HEAD~1..HEAD
|
|
||||||
---
|
|
||||||
|
|
||||||
Implement the approved PR #481 review cleanup for Python 3.14 partial
|
|
||||||
placeholder detection.
|
|
||||||
|
|
||||||
> `git diff HEAD~1..HEAD -- tractor/to_actor/_api.py`
|
|
||||||
|
|
||||||
Inline the guarded `functools.Placeholder` lookup into the existing
|
|
||||||
condition with a walrus assignment, preserving fallback behavior when
|
|
||||||
the attribute is unavailable.
|
|
||||||
|
|
||||||
Run partial/placeholder normalization tests and the full `to_actor`
|
|
||||||
suite.
|
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
session: pr490-landing-review
|
|
||||||
timestamp: 2026-08-28T01:42:47Z
|
|
||||||
git_ref: wkt/fix_broadcast_lag_count
|
|
||||||
scope: code
|
|
||||||
substantive: true
|
|
||||||
raw_file: 20260828T014247Z_d06ee34b_prompt_io.raw.md
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Prime PR #490 for landing, review its concurrent broadcast changes,
|
|
||||||
fix valid findings, and keep the landing pass focused.
|
|
||||||
|
|
||||||
## Response summary
|
|
||||||
|
|
||||||
Rejected an invalid mutable-default finding under `msgspec.Struct`
|
|
||||||
semantics. Found and fixed a close-time race where concurrent receives
|
|
||||||
on one `BroadcastReceiver` could overwrite its sole cancellation scope
|
|
||||||
and strand an earlier waiter. The receiver now rejects overlapping
|
|
||||||
calls with `trio.BusyResourceError`. Public docs now define each
|
|
||||||
receiver as one logical subscription cursor and require one receiver
|
|
||||||
per concurrent lossless consumer. Busy errors identify the active Trio
|
|
||||||
task which owns the receiver.
|
|
||||||
|
|
||||||
## Files changed
|
|
||||||
|
|
||||||
- `tractor/trionics/_broadcast.py` - reject concurrent receives on one
|
|
||||||
receiver handle.
|
|
||||||
- `tests/test_task_broadcasting.py` - prove the second call fails while
|
|
||||||
the original receive remains usable.
|
|
||||||
- `docs/guide/streaming.rst` - document per-task receiver ownership and
|
|
||||||
explicit lag behavior.
|
|
||||||
|
|
||||||
## Human edits
|
|
||||||
|
|
||||||
The human selected PR #490 as the next landing target, rebased it onto
|
|
||||||
current `main`, directed the agent to continue the landing pass, and
|
|
||||||
required a faster, tightly scoped execution after the review expanded.
|
|
||||||
The human then confirmed the per-receiver guard and required the public
|
|
||||||
method, type, and surrounding guide to emphasize lossless per-consumer
|
|
||||||
semantics. The human also required the regression's checkpoint loop to
|
|
||||||
explain its synchronization and the busy error to report the active
|
|
||||||
consumer task. The agent applied the source, test, and documentation
|
|
||||||
changes; no direct manual edits to these files were identified.
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
---
|
|
||||||
model: openai/gpt-5.6-sol
|
|
||||||
service: opencode
|
|
||||||
timestamp: 2026-08-28T01:42:47Z
|
|
||||||
git_ref: wkt/fix_broadcast_lag_count
|
|
||||||
diff_cmd: git diff
|
|
||||||
---
|
|
||||||
|
|
||||||
## Prompt
|
|
||||||
|
|
||||||
Get PR #490 primed and landed. Continue after rebasing, but keep the
|
|
||||||
review and fix pass focused.
|
|
||||||
|
|
||||||
## Response
|
|
||||||
|
|
||||||
The mutable `{}` default is safe because `msgspec.Struct` creates a
|
|
||||||
fresh dictionary per instance. A separate concurrency defect remains:
|
|
||||||
two tasks can call `receive()` through one `BroadcastReceiver`, and the
|
|
||||||
second call overwrites the only `_wait_scope`. Closing that receiver can
|
|
||||||
then leave the first task blocked indefinitely.
|
|
||||||
|
|
||||||
> `git diff -- tractor/trionics/_broadcast.py tests/test_task_broadcasting.py`
|
|
||||||
|
|
||||||
Added a per-receiver in-progress guard that raises
|
|
||||||
`trio.BusyResourceError` for an overlapping receive. Added a regression
|
|
||||||
test that blocks the first receive, verifies the second call is
|
|
||||||
rejected, and releases the source to prove the first call still works.
|
|
||||||
The complete broadcast test file passes: 28 tests in 4.57 seconds.
|
|
||||||
|
|
||||||
## Follow-up prompt
|
|
||||||
|
|
||||||
Clarify that the guard is per receiver, preserves lossless consumer
|
|
||||||
semantics, and document the contract on the public method, type, and
|
|
||||||
surrounding guide.
|
|
||||||
|
|
||||||
## Follow-up response
|
|
||||||
|
|
||||||
> `git diff -- tractor/trionics/_broadcast.py docs/guide/streaming.rst`
|
|
||||||
|
|
||||||
Documented that each `BroadcastReceiver` owns one logical cursor, each
|
|
||||||
concurrent consumer needs its own subscribed receiver, overlapping
|
|
||||||
calls on one handle raise `BusyResourceError`, and strict lag handling
|
|
||||||
never skips values silently.
|
|
||||||
|
|
||||||
## Second follow-up prompt
|
|
||||||
|
|
||||||
Explain that the polling loop waits until the background receive is
|
|
||||||
blocked before making the concurrent call, and include the first
|
|
||||||
consumer task's runtime information in the busy exception.
|
|
||||||
|
|
||||||
## Second follow-up response
|
|
||||||
|
|
||||||
Replaced the boolean guard with the active `trio.lowlevel.Task`, added
|
|
||||||
its name and representation to `BusyResourceError`, named the fixture
|
|
||||||
task for a deterministic assertion, and documented the checkpoint-loop
|
|
||||||
interleaving directly above the poll.
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
NOTE: you MUST pause this work at 12:50PM EST (BEFORE your weekly
|
|
||||||
limit reset) for review by a human!
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
attempt to resolve https://github.com/goodboy/tractor/issues/477
|
|
||||||
do it with /open-wkt.
|
|
||||||
|
|
@ -37,6 +37,7 @@ Spawning actors
|
||||||
|
|
||||||
.. autoclass:: ActorNursery
|
.. autoclass:: ActorNursery
|
||||||
:members: start_actor,
|
:members: start_actor,
|
||||||
|
run_in_actor,
|
||||||
cancel,
|
cancel,
|
||||||
cancel_called,
|
cancel_called,
|
||||||
cancelled_caught
|
cancelled_caught
|
||||||
|
|
@ -45,25 +46,11 @@ Spawning actors
|
||||||
|
|
||||||
:meth:`ActorNursery.start_actor` (daemon actor + portal) is the
|
:meth:`ActorNursery.start_actor` (daemon actor + portal) is the
|
||||||
blessed spawning primitive; pair it with
|
blessed spawning primitive; pair it with
|
||||||
:meth:`Portal.open_context` for SC-linked remote tasks.
|
``Portal.open_context()`` for SC-linked remote tasks.
|
||||||
|
:meth:`ActorNursery.run_in_actor` is a *convenience* one-shot —
|
||||||
One-shot task actors
|
spawn, run a single task, auto-cancel after the result — slated
|
||||||
--------------------
|
to be rebuilt as a high-level wrapper, so don't design around
|
||||||
|
it as the core model.
|
||||||
.. autofunction:: tractor.to_actor.run
|
|
||||||
|
|
||||||
.. note::
|
|
||||||
|
|
||||||
Without ``portal=``, :func:`tractor.to_actor.run` (parlance of
|
|
||||||
``trio.to_thread.run_sync()`` and friends) is the convenience
|
|
||||||
one-shot: spawn, run one task, block on its result and reap. It
|
|
||||||
combines :meth:`ActorNursery.start_actor`, a linked
|
|
||||||
:meth:`Portal.open_context` call and per-child reaping. With
|
|
||||||
``portal=`` it owns only the linked task and leaves the existing
|
|
||||||
actor's lifetime to the portal owner; that actor must expose both
|
|
||||||
the target module and ``tractor.to_actor.MODULE``. It supersedes
|
|
||||||
the removed (legacy, non-blocking)
|
|
||||||
``ActorNursery.run_in_actor()``.
|
|
||||||
|
|
||||||
.. deprecated:: 0.1.0a6
|
.. deprecated:: 0.1.0a6
|
||||||
|
|
||||||
|
|
@ -84,12 +71,14 @@ flowing back `exactly like trio`_.
|
||||||
:members: run,
|
:members: run,
|
||||||
run_from_ns,
|
run_from_ns,
|
||||||
open_stream_from,
|
open_stream_from,
|
||||||
|
wait_for_result,
|
||||||
cancel_actor,
|
cancel_actor,
|
||||||
chan
|
chan
|
||||||
|
|
||||||
.. deprecated:: 0.1.0a6
|
.. deprecated:: 0.1.0a6
|
||||||
|
|
||||||
The str-form ``Portal.run('mod.path', 'fn_name')`` warns;
|
``Portal.result()`` warns; use :meth:`Portal.wait_for_result`.
|
||||||
|
The str-form ``Portal.run('mod.path', 'fn_name')`` also warns;
|
||||||
pass a function *object* whose module is listed in the target's
|
pass a function *object* whose module is listed in the target's
|
||||||
``enable_modules``. ``Portal.channel`` is the legacy spelling
|
``enable_modules``. ``Portal.channel`` is the legacy spelling
|
||||||
of :attr:`Portal.chan`.
|
of :attr:`Portal.chan`.
|
||||||
|
|
|
||||||
|
|
@ -5,9 +5,8 @@ This is the curated reference for ``tractor``'s public surface: the
|
||||||
names you can import and lean on without reading runtime internals.
|
names you can import and lean on without reading runtime internals.
|
||||||
Everything below is re-exported at the top level (``import
|
Everything below is re-exported at the top level (``import
|
||||||
tractor``) unless a page says otherwise; subsystems like
|
tractor``) unless a page says otherwise; subsystems like
|
||||||
``tractor.msg``, ``tractor.trionics``, ``tractor.to_actor``,
|
``tractor.msg``, ``tractor.trionics``, ``tractor.to_asyncio``,
|
||||||
``tractor.to_asyncio``, ``tractor.devx`` and ``tractor.log`` are
|
``tractor.devx`` and ``tractor.log`` are importable as submodules.
|
||||||
importable as submodules.
|
|
||||||
|
|
||||||
``tractor`` is "just trio_" extended across processes: every API
|
``tractor`` is "just trio_" extended across processes: every API
|
||||||
here is designed to keep the structured concurrency (SC) rules you
|
here is designed to keep the structured concurrency (SC) rules you
|
||||||
|
|
@ -24,7 +23,6 @@ Most-used names at a glance:
|
||||||
|
|
||||||
open_root_actor
|
open_root_actor
|
||||||
open_nursery
|
open_nursery
|
||||||
to_actor.run
|
|
||||||
run_daemon
|
run_daemon
|
||||||
ActorNursery
|
ActorNursery
|
||||||
Portal
|
Portal
|
||||||
|
|
|
||||||
|
|
@ -30,12 +30,11 @@ Starting asyncio tasks from trio
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
:func:`open_channel_from` mirrors the
|
:func:`open_channel_from` mirrors the
|
||||||
:meth:`tractor.Portal.open_context` handshake: the asyncio side calls
|
``Portal.open_context()`` handshake: the asyncio side calls
|
||||||
``chan.started_nowait(value)`` and that value pops out as
|
``chan.started_nowait(value)`` and that value pops out as
|
||||||
``first`` on the trio side. :func:`run_task` is the one-shot
|
``first`` on the trio side. :func:`run_task` is the one-shot
|
||||||
form — run a single asyncio-compatible coroutine fn and return
|
form — run a single asyncio-compatible coroutine fn and return
|
||||||
its result to trio; :func:`tractor.to_actor.run` is its
|
its result to trio.
|
||||||
cross-process sibling.
|
|
||||||
|
|
||||||
The inter-loop channel
|
The inter-loop channel
|
||||||
----------------------
|
----------------------
|
||||||
|
|
|
||||||
|
|
@ -40,9 +40,6 @@ Broadcast fan-out
|
||||||
.. autoexception:: Lagged
|
.. autoexception:: Lagged
|
||||||
:show-inheritance:
|
:show-inheritance:
|
||||||
|
|
||||||
.. autoexception:: BroadcastReceiveError
|
|
||||||
:show-inheritance:
|
|
||||||
|
|
||||||
A single-producer, many-consumer broadcast layer over any
|
A single-producer, many-consumer broadcast layer over any
|
||||||
``trio``-style receive channel: non-lossy for the *fastest*
|
``trio``-style receive channel: non-lossy for the *fastest*
|
||||||
consumer while slower consumers raise :class:`Lagged` (a
|
consumer while slower consumers raise :class:`Lagged` (a
|
||||||
|
|
@ -51,13 +48,6 @@ internal ring. This is exactly the machinery behind
|
||||||
:meth:`tractor.MsgStream.subscribe` — see
|
:meth:`tractor.MsgStream.subscribe` — see
|
||||||
``examples/streaming_broadcast_fanout.py``.
|
``examples/streaming_broadcast_fanout.py``.
|
||||||
|
|
||||||
If the shared underlying receiver raises an ordinary exception, the
|
|
||||||
subscriber which owned that receive gets the original failure.
|
|
||||||
Waiting peers drain their retained values and then raise
|
|
||||||
:class:`BroadcastReceiveError`, with the original failure available
|
|
||||||
as ``__cause__``. Later subscribers observe the same terminal state
|
|
||||||
without retrying the failed underlying receiver.
|
|
||||||
|
|
||||||
ExceptionGroup helpers
|
ExceptionGroup helpers
|
||||||
----------------------
|
----------------------
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -130,10 +130,9 @@ UDS: same-host, creds included
|
||||||
|
|
||||||
Pass ``enable_transports=['uds']`` and actors instead talk over
|
Pass ``enable_transports=['uds']`` and actors instead talk over
|
||||||
unix-domain sockets, with socket files placed in the per-user
|
unix-domain sockets, with socket files placed in the per-user
|
||||||
runtime dir: ``$XDG_RUNTIME_DIR/tractor/`` on linux, a short
|
runtime dir (``$XDG_RUNTIME_DIR/tractor/`` on linux, the
|
||||||
owner-only ``/tmp/tractor-<uid>`` dir on Darwin, and the
|
``platformdirs`` equivalent elsewhere). Two perks over tcp on a
|
||||||
``platformdirs`` equivalent elsewhere. Two perks over tcp on a single
|
single host:
|
||||||
host:
|
|
||||||
|
|
||||||
- no ports to fight over; addrs are just file paths,
|
- no ports to fight over; addrs are just file paths,
|
||||||
- the kernel snitches on your peer for free: the listening side
|
- the kernel snitches on your peer for free: the listening side
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,8 @@ Just flip the flag on :meth:`tractor.ActorNursery.start_actor`:
|
||||||
infect_asyncio=True,
|
infect_asyncio=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
The one-shot convenience ``tractor.to_actor.run()`` accepts the
|
The one-shot convenience ``ActorNursery.run_in_actor()`` accepts
|
||||||
same flag. The ``to_asyncio`` APIs may **only** be called from
|
the same flag. The ``to_asyncio`` APIs may **only** be called from
|
||||||
tasks inside an infected actor; calling them anywhere else raises
|
tasks inside an infected actor; calling them anywhere else raises
|
||||||
a loud ``RuntimeError``. You can introspect at runtime with
|
a loud ``RuntimeError``. You can introspect at runtime with
|
||||||
``tractor.current_actor().is_infected_aio()``.
|
``tractor.current_actor().is_infected_aio()``.
|
||||||
|
|
@ -209,11 +209,6 @@ The underlying broadcast machinery is lazily allocated on first
|
||||||
use and is *not* reversible for the channel's remaining lifetime,
|
use and is *not* reversible for the channel's remaining lifetime,
|
||||||
so only reach for it when you actually want the fan-out.
|
so only reach for it when you actually want the fan-out.
|
||||||
|
|
||||||
As with ``MsgStream``, pass ``raise_on_lag=False`` for a consumer
|
|
||||||
which may warn, drop old values and resume from the retained window.
|
|
||||||
Each child chooses independently; the first subscription also fixes
|
|
||||||
the linked channel's root receive policy.
|
|
||||||
|
|
||||||
One-shot calls with ``run_task()``
|
One-shot calls with ``run_task()``
|
||||||
----------------------------------
|
----------------------------------
|
||||||
When you just want a single ``asyncio`` result and no streaming
|
When you just want a single ``asyncio`` result and no streaming
|
||||||
|
|
@ -234,7 +229,7 @@ dialog, skip the channel ceremony and use
|
||||||
|
|
||||||
It schedules the fn as an ``asyncio.Task``, waits for completion
|
It schedules the fn as an ``asyncio.Task``, waits for completion
|
||||||
and hands the return value back to ``trio``; think of it as the
|
and hands the return value back to ``trio``; think of it as the
|
||||||
cross-loop sibling of ``tractor.to_actor.run()``. Errors and
|
cross-loop sibling of ``ActorNursery.run_in_actor()``. Errors and
|
||||||
cancellation are translated exactly as for channels.
|
cancellation are translated exactly as for channels.
|
||||||
|
|
||||||
Cross-loop errors and cancellation
|
Cross-loop errors and cancellation
|
||||||
|
|
|
||||||
|
|
@ -64,13 +64,11 @@ What's going on here?
|
||||||
- three healthy actors are spawned as daemons via
|
- three healthy actors are spawned as daemons via
|
||||||
:meth:`tractor.ActorNursery.start_actor`; left alone they'd
|
:meth:`tractor.ActorNursery.start_actor`; left alone they'd
|
||||||
happily idle forever,
|
happily idle forever,
|
||||||
- a fourth actor runs ``assert_err()`` via a blocking
|
- a fourth actor runs ``assert_err()`` via ``.run_in_actor()`` and
|
||||||
``tractor.to_actor.run()`` one-shot and promptly trips its
|
promptly trips its ``assert 0``,
|
||||||
``assert 0``,
|
|
||||||
- the resulting ``AssertionError`` ships back over IPC as a
|
- the resulting ``AssertionError`` ships back over IPC as a
|
||||||
serialized error msg and re-raises *boxed* right at the call
|
serialized error msg and re-raises *boxed* inside the nursery
|
||||||
inside the nursery block as a
|
block as a :class:`tractor.RemoteActorError`,
|
||||||
:class:`tractor.RemoteActorError`,
|
|
||||||
- the nursery reacts like any ``trio`` nursery would: it cancels
|
- the nursery reacts like any ``trio`` nursery would: it cancels
|
||||||
the three healthy siblings (graceful runtime-cancel requests,
|
the three healthy siblings (graceful runtime-cancel requests,
|
||||||
acks awaited), reaps all four processes, then re-raises,
|
acks awaited), reaps all four processes, then re-raises,
|
||||||
|
|
@ -230,23 +228,22 @@ Graceful first, hard as a last resort
|
||||||
|
|
||||||
The hard-kill path is *skipped* whenever an actor in the tree
|
The hard-kill path is *skipped* whenever an actor in the tree
|
||||||
holds the debug-REPL lock (``debug_mode=True`` flavors):
|
holds the debug-REPL lock (``debug_mode=True`` flavors):
|
||||||
Process signals raining down on a tree mid-``pdb`` session would
|
SIGTERM raining down on a tree mid-``pdb`` session would
|
||||||
clobber your prompt. See :doc:`/guide/debugging`.
|
clobber your prompt. See :doc:`/guide/debugging`.
|
||||||
|
|
||||||
Owned-child teardown in ``tractor`` begins with the same graceful
|
Every process teardown in ``tractor`` walks the same escalation
|
||||||
steps, then selects the escalation path used by its supervisor,
|
ladder, top rung first,
|
||||||
|
|
||||||
1. **graceful cancel request**: a runtime-cancel msg over IPC; the
|
1. **graceful cancel request**: a runtime-cancel msg over IPC; the
|
||||||
target actor cancels its tasks, closes its channels and exits
|
target actor cancels its tasks, closes its channels and exits
|
||||||
its :func:`trio.run` cleanly,
|
its :func:`trio.run` cleanly,
|
||||||
2. **soft wait**: the parent waits (bounded) for the child process
|
2. **soft wait**: the parent waits (bounded) for the child process
|
||||||
to exit on its own,
|
to exit on its own,
|
||||||
3. **actor-nursery hard reap**: no cancel ack within the bounded wait
|
3. **SIGTERM**: no ack within the bounded wait (internally an
|
||||||
(internally an ``ActorTooSlowError``) escalates directly to
|
``ActorTooSlowError``) escalates to ``proc.terminate()``,
|
||||||
``proc.kill()`` before the child monitor joins the process,
|
4. **SIGKILL ultimatum**: still alive after the hard-kill timeout
|
||||||
4. **legacy soft-kill path**: older teardown callers may first issue
|
(~1.6s)? The runtime logs that the "T-800" has been deployed to
|
||||||
``proc.terminate()`` and then deploy the "T-800" ``proc.kill()``
|
collect the zombie and issues ``proc.kill()``. No survivors.
|
||||||
ultimatum if the process survives that additional bounded wait.
|
|
||||||
|
|
||||||
The result is the **no-zombies guarantee**: ``tractor`` tries to
|
The result is the **no-zombies guarantee**: ``tractor`` tries to
|
||||||
protect you from zombies, no matter what. Quoting the project
|
protect you from zombies, no matter what. Quoting the project
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,7 @@ one kwarg away,
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
async with tractor.open_actor_cluster(
|
async with tractor.open_actor_cluster(
|
||||||
modules=[
|
modules=['mylib.workers'],
|
||||||
'mylib.workers',
|
|
||||||
tractor.to_actor.MODULE,
|
|
||||||
],
|
|
||||||
count=4,
|
count=4,
|
||||||
names=['scout', 'miner', 'smelter', 'smith'],
|
names=['scout', 'miner', 'smelter', 'smith'],
|
||||||
debug_mode=True, # whole-fleet crash-to-REPL
|
debug_mode=True, # whole-fleet crash-to-REPL
|
||||||
|
|
@ -73,12 +70,9 @@ one kwarg away,
|
||||||
...
|
...
|
||||||
|
|
||||||
From here the composition patterns are the usual ``tractor`` fare:
|
From here the composition patterns are the usual ``tractor`` fare:
|
||||||
``portal.run()`` for bare one-shot RPCs (as in the demo),
|
``portal.run()`` for one-shot calls (as in the demo), or — for a
|
||||||
``tractor.to_actor.run(..., portal=portal)`` for cancellation-linked
|
persistent bidirectional dialog per worker — concurrently enter N
|
||||||
one-shot tasks in an existing worker (include
|
``portal.open_context()`` blocks with
|
||||||
``tractor.to_actor.MODULE`` in ``modules``; the cluster still owns
|
|
||||||
the worker's lifetime), or — for a persistent bidirectional dialog
|
|
||||||
per worker — concurrently enter N ``portal.open_context()`` blocks with
|
|
||||||
``tractor.trionics.gather_contexts()``; see :doc:`/guide/context`
|
``tractor.trionics.gather_contexts()``; see :doc:`/guide/context`
|
||||||
for that whole layer.
|
for that whole layer.
|
||||||
|
|
||||||
|
|
@ -93,8 +87,8 @@ Clusters vs. nurseries
|
||||||
|
|
||||||
``open_actor_cluster()`` is sugar, not a new primitive: under the
|
``open_actor_cluster()`` is sugar, not a new primitive: under the
|
||||||
hood it's just :func:`tractor.open_nursery` plus N concurrent
|
hood it's just :func:`tractor.open_nursery` plus N concurrent
|
||||||
:meth:`~tractor.ActorNursery.start_actor` calls plus a ``.cancel()``
|
``start_actor()`` calls plus a ``.cancel()`` on the way out. Reach
|
||||||
on the way out. Reach for it when,
|
for it when,
|
||||||
|
|
||||||
- you want a *flat*, homogeneous fleet (classic worker-pool or
|
- you want a *flat*, homogeneous fleet (classic worker-pool or
|
||||||
map-style fan-out shapes),
|
map-style fan-out shapes),
|
||||||
|
|
|
||||||
|
|
@ -15,12 +15,12 @@ a single `structured concurrency`_ (SC) scope over IPC.
|
||||||
:alt: sequence diagram of the context handshake msg flow
|
:alt: sequence diagram of the context handshake msg flow
|
||||||
|
|
||||||
Pretty much everything else is (or is slated to be) built on this
|
Pretty much everything else is (or is slated to be) built on this
|
||||||
one primitive: ``tractor.to_actor.run()`` uses it for a linked
|
one primitive: ``ActorNursery.run_in_actor()`` is a convenience
|
||||||
one-shot task, spawning and reaping an actor only when no ``portal=``
|
for "spawn, open a context, await the result, tear down"; plain
|
||||||
is supplied; plain ``Portal.run()`` RPC is planned to be
|
``Portal.run()`` RPC is planned to be re-implemented on top of it;
|
||||||
re-implemented on top of it; the multi-process debugger's tree-wide
|
the multi-process debugger's tree-wide REPL lock rides one. Grok
|
||||||
REPL lock rides one. Grok this page and the rest of the library reads
|
this page and the rest of the library reads as convenience
|
||||||
as convenience wrappers B)
|
wrappers B)
|
||||||
|
|
||||||
The endpoint contract
|
The endpoint contract
|
||||||
---------------------
|
---------------------
|
||||||
|
|
|
||||||
|
|
@ -44,9 +44,8 @@ clan shares one registry with zero config on your part.
|
||||||
The bootstrap rule inside ``open_root_actor()`` is delightfully
|
The bootstrap rule inside ``open_root_actor()`` is delightfully
|
||||||
simple:
|
simple:
|
||||||
|
|
||||||
- on boot, probe every addr in ``registry_addrs`` with a bounded
|
- on boot, ping every socket addr in ``registry_addrs``; when none
|
||||||
Tractor ``Aid`` handshake; when none are passed the per-transport
|
are passed the per-transport defaults are used: for TCP the
|
||||||
defaults are used: for TCP the
|
|
||||||
loopback ``('127.0.0.1', 1616)``, for UDS a
|
loopback ``('127.0.0.1', 1616)``, for UDS a
|
||||||
``registry@1616.sock`` file,
|
``registry@1616.sock`` file,
|
||||||
|
|
||||||
|
|
@ -54,11 +53,9 @@ simple:
|
||||||
actor and register with the *existing* registry; your own IPC
|
actor and register with the *existing* registry; your own IPC
|
||||||
server binds random same-transport addrs instead,
|
server binds random same-transport addrs instead,
|
||||||
|
|
||||||
- if every address is absent, congratulations: you just became the
|
- if **nothing answers, congratulations: you just became the
|
||||||
registrar. Your transport server binds the registry addrs
|
registrar**. Your transport server binds the registry addrs
|
||||||
themselves and you start serving lookups for everyone else,
|
themselves and you start serving lookups for everyone else.
|
||||||
- if no registrar answers but an address is occupied by a foreign or
|
|
||||||
non-responsive endpoint, startup fails instead of binding over it.
|
|
||||||
|
|
||||||
Pass ``ensure_registry=True`` when your program *requires* being
|
Pass ``ensure_registry=True`` when your program *requires* being
|
||||||
the one-and-only registrar; boot then fails loudly with a
|
the one-and-only registrar; boot then fails loudly with a
|
||||||
|
|
@ -221,10 +218,9 @@ the existing registrar:
|
||||||
|
|
||||||
trio.run(main)
|
trio.run(main)
|
||||||
|
|
||||||
Per the bootstrap rules above, if those addrs are absent this process
|
Per the bootstrap rules above, if the registrar at those addrs is
|
||||||
becomes its own registrar root, so the same code works standalone and
|
*not* reachable this process simply becomes its own (registrar)
|
||||||
as a tree-joiner. An occupied address that does not complete a Tractor
|
root — so the same code works standalone and as a tree-joiner.
|
||||||
registrar handshake fails startup instead of being rebound.
|
|
||||||
|
|
||||||
"Arbiter"? A legacy naming note
|
"Arbiter"? A legacy naming note
|
||||||
-------------------------------
|
-------------------------------
|
||||||
|
|
|
||||||
|
|
@ -9,8 +9,8 @@ docs; what you read is what CI runs).
|
||||||
Roughly in "first date to long term relationship"
|
Roughly in "first date to long term relationship"
|
||||||
order,
|
order,
|
||||||
|
|
||||||
- :doc:`spawning` — actor nurseries, daemons,
|
- :doc:`spawning` — actor nurseries, daemons +
|
||||||
``to_actor.run()`` one-shots and process lifetimes.
|
one-shot workers, process lifetimes.
|
||||||
- :doc:`rpc` — portals: calling into another
|
- :doc:`rpc` — portals: calling into another
|
||||||
process like it's a local ``await``.
|
process like it's a local ``await``.
|
||||||
- :doc:`context` — the cross-actor task-pair
|
- :doc:`context` — the cross-actor task-pair
|
||||||
|
|
|
||||||
|
|
@ -119,16 +119,15 @@ Run a func in a process
|
||||||
|
|
||||||
Even a pool can be overkill; "run this one async func in a
|
Even a pool can be overkill; "run this one async func in a
|
||||||
subprocess and give me the result" is a one-liner via
|
subprocess and give me the result" is a one-liner via
|
||||||
:func:`tractor.to_actor.run`,
|
:meth:`tractor.ActorNursery.run_in_actor`,
|
||||||
|
|
||||||
.. literalinclude:: ../../examples/parallelism/single_func.py
|
.. literalinclude:: ../../examples/parallelism/single_func.py
|
||||||
:caption: examples/parallelism/single_func.py
|
:caption: examples/parallelism/single_func.py
|
||||||
:language: python
|
:language: python
|
||||||
|
|
||||||
``to_actor.run()`` is a *convenience wrapper* — spawn an actor,
|
``run_in_actor()`` is a *convenience wrapper* — spawn an actor, run
|
||||||
run exactly one task in it, block on and return its result, reap
|
exactly one task in it, reap on result — not the core spawning
|
||||||
— not the core spawning model (that's
|
model (that's :meth:`tractor.ActorNursery.start_actor` plus
|
||||||
:meth:`tractor.ActorNursery.start_actor` plus
|
|
||||||
:meth:`tractor.Portal.open_context`; see :doc:`/guide/context`).
|
:meth:`tractor.Portal.open_context`; see :doc:`/guide/context`).
|
||||||
But for this fire-and-collect shape it's exactly the right amount
|
But for this fire-and-collect shape it's exactly the right amount
|
||||||
of typing.
|
of typing.
|
||||||
|
|
|
||||||
|
|
@ -80,56 +80,28 @@ One special namespace exists: ``'self'`` resolves to the remote
|
||||||
how internal machinery (cancel requests, registry ops) travels;
|
how internal machinery (cancel requests, registry ops) travels;
|
||||||
don't build your app on it.
|
don't build your app on it.
|
||||||
|
|
||||||
One-shot subactors: ``to_actor.run()``
|
One-shot results: ``wait_for_result()``
|
||||||
--------------------------------------
|
---------------------------------------
|
||||||
When the call should own a fresh subactor whose entire job is one
|
A portal returned from
|
||||||
function call, :func:`tractor.to_actor.run` spawns it, runs the task,
|
:meth:`~tractor.ActorNursery.run_in_actor` has exactly one
|
||||||
returns its result and reaps the process — all in one blocking call:
|
"main" task running remotely; that task's ``return`` value is
|
||||||
|
delivered as the portal's *final result*:
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
from functools import partial
|
portal = await an.run_in_actor(fib, n=10)
|
||||||
|
final = await portal.wait_for_result()
|
||||||
final = await tractor.to_actor.run(
|
|
||||||
partial(fib, n=10),
|
|
||||||
an=an,
|
|
||||||
)
|
|
||||||
|
|
||||||
Semantics worth knowing:
|
Semantics worth knowing:
|
||||||
|
|
||||||
- it blocks until the remote task returns, re-raising any
|
- it blocks until the remote task returns, re-raising any
|
||||||
remote error in the usual boxed form right in the calling
|
remote error in the usual boxed form.
|
||||||
task.
|
- once resolved it's idempotent: later calls return the same
|
||||||
- lifetime mode also determines process ownership: ``an=`` spawns and
|
cached value.
|
||||||
reaps a fresh child in an existing actor nursery, while passing
|
- a *daemon* portal (from ``start_actor()``) has no main task,
|
||||||
neither does the same in a private call-scoped nursery (booting
|
so there's no final result to wait for: you'll get a warning
|
||||||
the runtime if needed). ``portal=`` instead runs one linked task
|
plus a ``NoResult`` sentinel. Results of individual daemon
|
||||||
in an existing actor; it neither spawns nor reaps that actor, so
|
calls come straight back from each ``await portal.run()``.
|
||||||
the portal's owner remains responsible for its lifetime.
|
|
||||||
- concurrency composes the plain ``trio`` way: schedule
|
|
||||||
multiple ``run()`` calls into a local task nursery (see
|
|
||||||
``examples/parallelism/concurrent_toactor_primes.py``).
|
|
||||||
|
|
||||||
A reused actor must expose both the target module and the
|
|
||||||
``to_actor`` context trampoline:
|
|
||||||
|
|
||||||
.. code:: python
|
|
||||||
|
|
||||||
async with tractor.open_nursery() as an:
|
|
||||||
portal = await an.start_actor(
|
|
||||||
'worker',
|
|
||||||
enable_modules=[
|
|
||||||
__name__,
|
|
||||||
tractor.to_actor.MODULE,
|
|
||||||
],
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
final = await tractor.to_actor.run(
|
|
||||||
partial(fib, n=10),
|
|
||||||
portal=portal,
|
|
||||||
)
|
|
||||||
finally:
|
|
||||||
await portal.cancel_actor()
|
|
||||||
|
|
||||||
Pure RPC daemons: ``run_daemon()``
|
Pure RPC daemons: ``run_daemon()``
|
||||||
----------------------------------
|
----------------------------------
|
||||||
|
|
@ -175,8 +147,7 @@ call tears down the entire sub-tree — SC, transitively.
|
||||||
|
|
||||||
When to graduate to ``Context``
|
When to graduate to ``Context``
|
||||||
-------------------------------
|
-------------------------------
|
||||||
The :meth:`~tractor.Portal.run` method is great for one-shot,
|
``portal.run()`` is great for one-shot, request-response calls.
|
||||||
request-response calls.
|
|
||||||
Reach for :meth:`~tractor.Portal.open_context` with an
|
Reach for :meth:`~tractor.Portal.open_context` with an
|
||||||
``@tractor.context`` endpoint as soon as you want:
|
``@tractor.context`` endpoint as soon as you want:
|
||||||
|
|
||||||
|
|
@ -189,15 +160,10 @@ Reach for :meth:`~tractor.Portal.open_context` with an
|
||||||
:meth:`~tractor.Portal.cancel_actor` nukes the **entire**
|
:meth:`~tractor.Portal.cancel_actor` nukes the **entire**
|
||||||
remote runtime and its process.
|
remote runtime and its process.
|
||||||
|
|
||||||
:func:`tractor.to_actor.run` already enters the full
|
In fact the source plans for ``Portal.run()`` itself to be
|
||||||
:meth:`~tractor.Portal.open_context` lifecycle. The older
|
rebuilt on top of ``open_context()`` — contexts *are* the core
|
||||||
:meth:`~tractor.Portal.run` path instead uses the ``Context`` returned
|
inter-actor protocol. Take the full tour in
|
||||||
by the lower-level ``Actor.start_remote_task()`` directly, avoiding a
|
:doc:`/guide/context`.
|
||||||
``Started`` handshake but owning less lifecycle machinery. A follow-up
|
|
||||||
should factor their shared linked-task lifecycle without requiring
|
|
||||||
``Portal.run()`` to delegate through the public context API or add
|
|
||||||
another wire message. Take the full tour in
|
|
||||||
:doc:`the context guide </guide/context>`.
|
|
||||||
|
|
||||||
.. seealso::
|
.. seealso::
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -91,34 +91,31 @@ somebody-ing:
|
||||||
|
|
||||||
What's going on here?
|
What's going on here?
|
||||||
|
|
||||||
- :meth:`~tractor.ActorNursery.start_actor` forks off
|
- ``start_actor('frank', enable_modules=[__name__])`` forks off
|
||||||
a new process, boots a ``tractor`` runtime inside it, and
|
a new process, boots a ``tractor`` runtime inside it, and
|
||||||
allows it to serve functions from the current module (see the
|
allows it to serve functions from the current module (see the
|
||||||
allowlist section below).
|
allowlist section below).
|
||||||
- each :meth:`~tractor.Portal.run` call schedules a *new* task in
|
- each ``await portal.run(...)`` schedules a *new* task in
|
||||||
frank's task tree and waits on its result — the full RPC story
|
frank's task tree and waits on its result — the full RPC story
|
||||||
lives in :doc:`/guide/rpc`.
|
lives in :doc:`/guide/rpc`.
|
||||||
- frank has no main task to complete, so without the final
|
- frank has no main task to complete, so without the final
|
||||||
:meth:`~tractor.Portal.cancel_actor` call the nursery block would
|
``await portal.cancel_actor()`` the nursery block would wait
|
||||||
wait on him **forever**. Daemon lifetimes are *yours* to end;
|
on him **forever**. Daemon lifetimes are *yours* to end; that
|
||||||
that explicitness is the point.
|
explicitness is the point.
|
||||||
|
|
||||||
``to_actor.run()``: quick one-shot parallelism
|
``run_in_actor()``: quick one-shot parallelism
|
||||||
----------------------------------------------
|
----------------------------------------------
|
||||||
Without ``portal=``, :func:`tractor.to_actor.run` is the convenience
|
:meth:`~tractor.ActorNursery.run_in_actor` is the convenience
|
||||||
wrapper: spawn an actor, run exactly one async function in it, block
|
wrapper: spawn an actor, run exactly one async function in it,
|
||||||
on the result, then reap the process — the distributed sibling of
|
then reap the process as soon as the result arrives.
|
||||||
``trio.to_thread.run_sync()``.
|
|
||||||
|
|
||||||
.. code:: python
|
.. code:: python
|
||||||
|
|
||||||
async with (
|
async with tractor.open_nursery() as an:
|
||||||
tractor.open_nursery() as an,
|
portal = await an.run_in_actor(burn_cpu)
|
||||||
trio.open_nursery() as tn,
|
|
||||||
):
|
|
||||||
# burn rubber in the parent too...
|
# burn rubber in the parent too...
|
||||||
tn.start_soon(burn_cpu)
|
await burn_cpu()
|
||||||
total = await tractor.to_actor.run(burn_cpu, an=an)
|
total = await portal.wait_for_result()
|
||||||
|
|
||||||
A few details worth knowing:
|
A few details worth knowing:
|
||||||
|
|
||||||
|
|
@ -126,61 +123,43 @@ A few details worth knowing:
|
||||||
``name='something_cuter'``.
|
``name='something_cuter'``.
|
||||||
- the function's module is auto-added to the child's
|
- the function's module is auto-added to the child's
|
||||||
``enable_modules`` allowlist.
|
``enable_modules`` allowlist.
|
||||||
- targets cross IPC as ``module:name`` references, so portable calls
|
- extra ``**kwargs`` are forwarded to the function itself.
|
||||||
use module-global async functions or ``functools.partial`` objects
|
- the child is *auto-cancelled* once its "main" result lands;
|
||||||
wrapping them. Nested functions, methods and callable objects do not
|
at nursery exit these run-once children are always reaped
|
||||||
provide that stable address.
|
first (causality_ is paramount!).
|
||||||
- target arguments are positional; use ``functools.partial()``
|
|
||||||
to bind target keyword arguments. Keywords passed directly to
|
|
||||||
``run()`` configure actor placement and spawning.
|
|
||||||
- the call blocks until the result (or error) lands and the
|
|
||||||
child is *auto-cancelled* (reaped) right after — so remote
|
|
||||||
errors raise directly in your calling task (causality_ is
|
|
||||||
paramount!).
|
|
||||||
- "placement" composes: ``an=`` spawns a call-owned child from an
|
|
||||||
existing actor nursery, while passing neither opens a private
|
|
||||||
call-scoped nursery. ``portal=`` instead reuses an existing actor:
|
|
||||||
the call scopes only its linked remote task, neither spawns nor
|
|
||||||
reaps the actor, and leaves its lifetime with the portal's owner.
|
|
||||||
That actor must expose both the target module and
|
|
||||||
``tractor.to_actor.MODULE``.
|
|
||||||
|
|
||||||
.. note::
|
.. note::
|
||||||
|
|
||||||
:func:`tractor.to_actor.run` is a convenience, **not** the core
|
``run_in_actor()`` is a convenience, **not** the core model.
|
||||||
model. For actor-owning placements it combines
|
The source literally marks it for an eventual rebuild as
|
||||||
:meth:`~tractor.ActorNursery.start_actor`, a linked
|
a thin "hilevel" wrapper on top of
|
||||||
:meth:`~tractor.Portal.open_context` call, and per-child
|
:meth:`~tractor.Portal.open_context` (the modern inter-actor
|
||||||
cancellation/reaping. With ``portal=`` it uses only the linked
|
task API). Teach your fingers to use it for quick
|
||||||
context call and leaves the existing actor's lifetime untouched.
|
fire-and-collect parallelism — think a per-function
|
||||||
Teach your fingers to use it for quick
|
trio-parallel_ style one-shot — and reach for
|
||||||
fire-and-collect parallelism — think a per-function trio-parallel_
|
``start_actor()`` + ``open_context()`` for anything
|
||||||
style one-shot — and reach for
|
long-lived, stateful or streaming
|
||||||
:meth:`~tractor.ActorNursery.start_actor` plus
|
(:doc:`/guide/context`).
|
||||||
:meth:`~tractor.Portal.open_context` for anything long-lived,
|
|
||||||
stateful or streaming; see :doc:`/guide/context`.
|
|
||||||
|
|
||||||
Actor lifetimes and teardown order
|
Actor lifetimes and teardown order
|
||||||
----------------------------------
|
----------------------------------
|
||||||
There are two actor-lifetime flavors:
|
So we have two lifetime flavors:
|
||||||
|
|
||||||
- **call-owned one-shot** (``to_actor.run()`` without ``portal=``):
|
- **run-once** (``run_in_actor()``): lives exactly as long as
|
||||||
spawned for one task, then cancelled and joined before ``run()``
|
its single task; reaped the moment its result (or error)
|
||||||
returns its result or raises its error.
|
arrives.
|
||||||
- **caller-owned daemon** (:meth:`~tractor.ActorNursery.start_actor`),
|
- **daemon** (``start_actor()``): lives until *someone* cancels
|
||||||
including an actor later reused through
|
it — an explicit ``await portal.cancel_actor()``, a bulk
|
||||||
``to_actor.run(..., portal=portal)``: lives until *someone*
|
``await an.cancel()``, or the one-cancels-all strategy kicking
|
||||||
cancels it via an explicit
|
in on error.
|
||||||
:meth:`~tractor.Portal.cancel_actor`, a bulk
|
|
||||||
:meth:`~tractor.ActorNursery.cancel`, or the one-cancels-all
|
|
||||||
strategy kicking in on error.
|
|
||||||
|
|
||||||
On a clean exit of the nursery block the teardown order is:
|
On a clean exit of the nursery block the teardown order is:
|
||||||
|
|
||||||
1. call-owned actors do not survive their own ``to_actor.run()``
|
1. the nursery waits on every run-once actor's final result;
|
||||||
calls; each is reaped before its call returns.
|
any errors from these are raised immediately so your code
|
||||||
2. the nursery waits on caller-owned daemon actors
|
(acting as supervisor) gets first crack at handling them.
|
||||||
**indefinitely**. If you spawned one, you own its lifetime.
|
2. then it waits on daemon actors — **indefinitely**. If you
|
||||||
|
spawned a daemon, you own its lifetime.
|
||||||
|
|
||||||
When a child *is* cancelled, teardown is graceful-first per SC
|
When a child *is* cancelled, teardown is graceful-first per SC
|
||||||
discipline: the runtime sends an IPC cancel request and gives
|
discipline: the runtime sends an IPC cancel request and gives
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue