Compare commits
4 Commits
a966928dea
...
b64c89f05a
| Author | SHA1 | Date |
|---|---|---|
|
|
b64c89f05a | |
|
|
bf2bb1c9a6 | |
|
|
0553fcf88c | |
|
|
9a0f220fd5 |
|
|
@ -0,0 +1,142 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-11T05:37:37Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: wkt/address_logging_levels
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20261011T053737Z_a966928d_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
The user supplied this original request:
|
||||||
|
|
||||||
|
> maybe we can add a commit to tractor on top of addr_unpacking,
|
||||||
|
> also scan for any other downstream from main logging history and
|
||||||
|
> cherry it there and then add your fix, and finally move piker_pin
|
||||||
|
> to that? rather not hackaround bug alreadypinningnonmain
|
||||||
|
|
||||||
|
The current instruction narrowed execution to a managed Tractor
|
||||||
|
worktree, `wkt/address_logging_levels`, starting at the verified
|
||||||
|
`addr_unpacking` OID a966928dead4cbf369ad244834ce9dbe988ac8ca.
|
||||||
|
Scan local logging history, preserve relevant originals where
|
||||||
|
possible, implement the standard-name fix with meaningful tests,
|
||||||
|
and verify Piker's original cache-hit regression through an import
|
||||||
|
overlay. Leave changes uncommitted for human review and leave
|
||||||
|
`piker_pin`, Piker's workaround, environments, and lockfiles intact.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Created the managed worktree and recorded approved local discovery
|
||||||
|
in Git administrative storage. No equivalent name-resolution fix
|
||||||
|
or additional prerequisite was found. Patch-ID comparisons of
|
||||||
|
`tractor/log.py` and `tests/test_log_sys.py` identified these
|
||||||
|
already-landed equivalents:
|
||||||
|
|
||||||
|
- d653cd2c -> a2c8af55: fast caller lookup.
|
||||||
|
- 4f811a87 -> d1d3fc58: lazy optional imports.
|
||||||
|
- 361f6fcc and other copies -> bb0b2ffa: custom-level factory.
|
||||||
|
- 11b9a870 and other copies -> b6bf865f: nested logger names.
|
||||||
|
- 74784780 and other copies -> 8bb9df9e: logging-spec API.
|
||||||
|
|
||||||
|
The base also includes 88cc68ed (custom-level re-registration),
|
||||||
|
67280c28 (disable-aware guards), and 089e158d (dynamic caller
|
||||||
|
fallback). The non-equivalent 53e10861 thread-header change is
|
||||||
|
unrelated and was excluded. No cherry-pick was needed.
|
||||||
|
|
||||||
|
The source fix uses public stdlib name resolution after Tractor's
|
||||||
|
custom-level lookup. It keeps the existing `KeyError` contract
|
||||||
|
instead of introducing a `ValueError` compatibility change.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/log.py`: standard/application name resolution and
|
||||||
|
integer-capable adapter annotation.
|
||||||
|
- `tests/test_log_sys.py`: 62 raw/adapted regression cases with
|
||||||
|
detailed failure-mechanism documentation and isolated log state.
|
||||||
|
- `ai/prompt-io/README.md`: shared provenance policy.
|
||||||
|
- This structured record and its immutable raw partner.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Run from the new Tractor worktree:
|
||||||
|
|
||||||
|
```xsh
|
||||||
|
env PYTHONPATH=/home/goodboy/repos/tractor/wkts/address_logging_levels /home/goodboy/repos/tractor/py313/bin/python -m pytest tests/test_log_sys.py -k at_least_level
|
||||||
|
```
|
||||||
|
|
||||||
|
Before the source fix: 30 failed, 32 passed, 8 deselected. Failures
|
||||||
|
were name-lookup `KeyError`s, including INFO and NOTSET.
|
||||||
|
|
||||||
|
```xsh
|
||||||
|
env PYTHONPATH=/home/goodboy/repos/tractor/wkts/address_logging_levels /home/goodboy/repos/tractor/py313/bin/python -m pytest tests/test_log_sys.py
|
||||||
|
/nix/store/qmrz7inmmlfmx1nghix8ymghh5yd9gnv-ruff-0.15.22/bin/ruff check tractor/log.py tests/test_log_sys.py
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
After the fix: 70 passed; Ruff and whitespace checks passed.
|
||||||
|
Tractor tests used CPython 3.13.11 and pytest 9.1.0. Source imports
|
||||||
|
were asserted beneath the new worktree. The inherited VIRTUAL_ENV
|
||||||
|
named Modden, but the explicit Tractor interpreter was used;
|
||||||
|
TRACTOR_SPAWN_METHOD and TRACTOR_LOGLEVEL were unset.
|
||||||
|
|
||||||
|
The existing `modern_consumer_probe.py` was run through an isolated
|
||||||
|
wrapper with the new Tractor source and Piker's existing
|
||||||
|
`land_flake_update/py313` interpreter (CPython 3.13.14). Original
|
||||||
|
Piker Git-tree modules and tests were overlaid only in memory;
|
||||||
|
the `--standard-level-fix` option was absent. All 8 cases passed,
|
||||||
|
including both cache-miss and cache-hit variants from b691422b.
|
||||||
|
The installed Tractor distribution metadata still identifies the
|
||||||
|
base OID; import assertions independently verified that executable
|
||||||
|
Tractor and its logging module came from the new source worktree.
|
||||||
|
|
||||||
|
Direct Piker imports first failed for missing native libraries
|
||||||
|
(libstdc++, then libzstd). A process-local LD_LIBRARY_PATH containing
|
||||||
|
already-present library outputs resolved from Piker's locked Nix
|
||||||
|
input fixed those imports. No shell hook, dependency installation,
|
||||||
|
environment synchronization, live broker, or actor suite was run.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
The human directed the upstream Tractor fix instead of a Piker
|
||||||
|
workaround, selected the exact stack base and local discovery scope,
|
||||||
|
required compatibility and consumer verification, and reserved
|
||||||
|
commits and ref publication for review. These are human-directed
|
||||||
|
agent changes; no subsequent manual edits or acceptance are recorded.
|
||||||
|
|
||||||
|
Subsequent local Tuicr review supplied 17 human-selected comments in
|
||||||
|
`address_logging_levels@wkt-address_logging_levels/staged/a966928`.
|
||||||
|
The reviewer directed explicit INFO(20) fixture documentation,
|
||||||
|
boolean selector meanings, the `is_info_level_enabled` callable
|
||||||
|
name, threshold comments at numeric values, and precise API and
|
||||||
|
failure descriptions instead of vague terms or Piker-specific
|
||||||
|
context. The agent applied those directions and replaced the custom
|
||||||
|
level membership/subscript pair with `CUSTOM_LEVELS.get(name)` plus
|
||||||
|
a lazy stdlib fallback on `None`, preserving zero and `KeyError`.
|
||||||
|
The registered-level regression now uses 35, between WARNING(30)
|
||||||
|
and ERROR(40), and tests it at INFO(20) then ERROR(40).
|
||||||
|
|
||||||
|
After these review adjustments, all 70 logging tests passed with
|
||||||
|
the same source-verified Tractor CPython 3.13.11 environment.
|
||||||
|
Targeted Ruff and `git diff --check` also passed. The staged index
|
||||||
|
was preserved; review adjustments remain unstaged for human review.
|
||||||
|
The original `.raw.md` watermark and response capture are unchanged.
|
||||||
|
|
||||||
|
A second review selected three follow-up comments about the logger
|
||||||
|
registration hook, fixture parameter meaning, and the INFO-specific
|
||||||
|
query name after changing the logger's threshold to ERROR. The
|
||||||
|
fixture now distinguishes instance-local level/disabled settings
|
||||||
|
from temporary registration and the process-global disable cutoff,
|
||||||
|
and includes a short consumer example. Each fixture case allocates
|
||||||
|
a fresh raw logger; only the query form varies between the module
|
||||||
|
function and an adapter method wrapping that logger. The test which
|
||||||
|
changes the threshold now names its query `is_level_enabled`.
|
||||||
|
|
||||||
|
All 70 logging tests passed again with source imports verified in
|
||||||
|
this worktree under Tractor's CPython 3.13.11 interpreter. Targeted
|
||||||
|
Ruff and whitespace checks passed. These follow-up adjustments are
|
||||||
|
unstaged; the current human-staged baseline and original raw prompt
|
||||||
|
record are preserved.
|
||||||
|
|
@ -0,0 +1,34 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-11T05:37:37Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: wkt/address_logging_levels
|
||||||
|
diff_cmd: git diff a966928dead4cbf369ad244834ce9dbe988ac8ca wkt/address_logging_levels
|
||||||
|
---
|
||||||
|
|
||||||
|
> `git diff a966928dead4cbf369ad244834ce9dbe988ac8ca wkt/address_logging_levels -- tractor/log.py`
|
||||||
|
|
||||||
|
Extend `at_least_level()` to resolve uppercase names first through
|
||||||
|
`CUSTOM_LEVELS`, then through `logging.getLevelNamesMapping()`.
|
||||||
|
Preserve integer inputs, zero-valued NOTSET, `KeyError` for unknown
|
||||||
|
names, and `Logger.isEnabledFor()` disable semantics. Update the
|
||||||
|
adapter's annotation to advertise its existing integer support.
|
||||||
|
|
||||||
|
> `git diff a966928dead4cbf369ad244834ce9dbe988ac8ca wkt/address_logging_levels -- tests/test_log_sys.py`
|
||||||
|
|
||||||
|
Add an isolated raw/adapted logger fixture and regressions for
|
||||||
|
standard names and aliases, lowercase names, custom CANCEL,
|
||||||
|
integer and NOTSET inputs, application-registered names, unknown
|
||||||
|
names, and logger-local/global disable controls.
|
||||||
|
|
||||||
|
Tractor's logging suite passes: **70 tests**. Before the source fix,
|
||||||
|
the new regressions produced **30 failures**, including `INFO`,
|
||||||
|
`NOTSET`, and application-registered names. Ruff and whitespace
|
||||||
|
checks also pass.
|
||||||
|
|
||||||
|
The original Piker consumer probe now passes **8/8**, including the
|
||||||
|
cache-hit case with `log.at_least_level('info')` retained. Import
|
||||||
|
assertions confirm it used the new Tractor worktree. The probe needed
|
||||||
|
a process-local overlay of existing Nix libraries to import Piker.
|
||||||
|
|
@ -0,0 +1,71 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-19T14:05:36Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 9a0f220f
|
||||||
|
scope: tests
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20261019T140536Z_9a0f220f_final_return_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
The human reported chart Ctrl-C causing search's final Return delivery
|
||||||
|
to raise TransportClosed in datad.ib, cancelling data services and then
|
||||||
|
pikerd's trading/sampler services. They asked whether the Piker branch
|
||||||
|
ib_connectivity_harden already remediates it and authorized a minimal
|
||||||
|
Tractor sibling-survival reproducer if no existing remediation applies:
|
||||||
|
|
||||||
|
> ok let's do it if we find no other redmediations per above.
|
||||||
|
|
||||||
|
The human explicitly selected capture of this test-only provenance.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Compared ib_connectivity_harden with the committed evaluation baseline:
|
||||||
|
efa72980's IB socket-disconnect propagation is already present; the
|
||||||
|
missing d126302f/c0dae89a changes concern idle arithmetic and sampler
|
||||||
|
cancellation-state access. Tractor ebf2258b hardens ordinary RPC response
|
||||||
|
sends, but the context final Return remains exposed. Inspected the
|
||||||
|
additional ipc_fault_regressions branch: its EOF/context-status changes
|
||||||
|
do not guard this final-result send.
|
||||||
|
|
||||||
|
Added a focused real-TCP, same-process dispatcher regression with an
|
||||||
|
ordinary RPC control and a context case. Explicit peer-close/EOF events
|
||||||
|
force the failure ordering. A sibling request/response checks service
|
||||||
|
survival; the failure path confirms premature sibling cancellation.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- tests/test_context_final_return_disconnect.py: reproducer and control.
|
||||||
|
- This provenance pair.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Ordinary RPC control passed.
|
||||||
|
- Context case failed with the reported TransportClosed caused by
|
||||||
|
ClosedResourceError, confirming collateral nursery cancellation.
|
||||||
|
- Final two-case run: 1 failed, 1 passed in 0.07s.
|
||||||
|
- Ruff passed; git diff --check passed.
|
||||||
|
- Test uses isolated local TCP, no broker, feed, Qt or asyncio bridge.
|
||||||
|
- Root actor and channels exit through their context managers.
|
||||||
|
- No production fix, staging, commit or ref changes.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
None observed in the generated test at capture time.
|
||||||
|
|
||||||
|
## Follow-up: identify the collateral sibling
|
||||||
|
|
||||||
|
The human requested the task repr and runtime metadata behind the
|
||||||
|
test-generated exception note. The cancellation handler now captures
|
||||||
|
the sibling task, Actor.aid (including PID), parent nursery, nursery
|
||||||
|
cancel scope and original trio.Cancelled repr while still unwinding.
|
||||||
|
The test attaches that snapshot to the escaping TransportClosed;
|
||||||
|
Tractor production reporting and exception handling are unchanged.
|
||||||
|
|
||||||
|
The updated two-case reproducer again yielded 1 passed, 1 failed in
|
||||||
|
0.07s, with the sibling identity and source invoke task in the note.
|
||||||
|
Ruff and git diff --check passed after the diagnostic change.
|
||||||
|
|
@ -0,0 +1,66 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-19T14:05:36Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 9a0f220f
|
||||||
|
diff_cmd: git diff --no-index /dev/null tests/test_context_final_return_disconnect.py
|
||||||
|
---
|
||||||
|
|
||||||
|
## Captured output
|
||||||
|
|
||||||
|
`ib_connectivity_harden` has no fix for this path. Its IB socket
|
||||||
|
disconnect handling is already in our evaluation; the two missing
|
||||||
|
commits fix idle-timer arithmetic and sampler cancellation-state
|
||||||
|
access.
|
||||||
|
|
||||||
|
I also checked Tractor’s related fault work. The existing
|
||||||
|
response-send fix covers ordinary RPCs, but leaves the `@context`
|
||||||
|
final-result send exposed. I’m building the reproducer in the
|
||||||
|
evaluated Tractor worktree, with isolated local IPC.
|
||||||
|
|
||||||
|
The reproducer hits the same failure with real loopback TCP:
|
||||||
|
**ordinary RPC passes; `@context` fails**, and the unrelated
|
||||||
|
sibling is confirmed cancelled before it can answer a request.
|
||||||
|
The two cases finish in 0.07 seconds, without IB, Qt or asyncio.
|
||||||
|
|
||||||
|
## Generated test
|
||||||
|
|
||||||
|
> `git diff --no-index /dev/null tests/test_context_final_return_disconnect.py`
|
||||||
|
|
||||||
|
The file is currently untracked. The pointer above displays the entire
|
||||||
|
generated artifact without duplicating it in this record.
|
||||||
|
|
||||||
|
finish_after_disconnect() is a real Tractor context endpoint that
|
||||||
|
publishes readiness and returns after an event. ordinary_result()
|
||||||
|
provides a non-context RPC control. The parameterized test invokes the
|
||||||
|
actual dispatcher with a real root actor and real loopback TCP channels.
|
||||||
|
It drains startup messages, closes the caller, observes callee EOF,
|
||||||
|
closes the callee channel, then releases the endpoint. A nursery sibling
|
||||||
|
must answer a subsequent request. On the observed TransportClosed it
|
||||||
|
also verifies collateral sibling cancellation and re-raises the fault.
|
||||||
|
|
||||||
|
The explicit event ordering isolates final-result delivery, without
|
||||||
|
mocked exceptions, sleeps, provider connections or process supervision.
|
||||||
|
|
||||||
|
## Final verification
|
||||||
|
|
||||||
|
Command environment: evaluation Piker py313/bin/python (Python 3.13.14,
|
||||||
|
pytest 9.0.2), importing Tractor from address_logging_levels at 9a0f220f.
|
||||||
|
VIRTUAL_ENV was removed; TRACTOR_SPAWN_METHOD and TRACTOR_LOGLEVEL unset.
|
||||||
|
LD_LIBRARY_PATH included the existing Nix GCC runtime library.
|
||||||
|
|
||||||
|
`python -m pytest tests/test_context_final_return_disconnect.py --tpt-proto=tcp --tb=short`
|
||||||
|
|
||||||
|
Result: 1 failed, 1 passed in 0.07s. Diagnostic --tb=short was used
|
||||||
|
after the ordinary full-trace run confirmed the dispatcher failure.
|
||||||
|
The context case raises TransportClosed caused by ClosedResourceError
|
||||||
|
at MsgpackTransport.send(), with the note:
|
||||||
|
The unrelated sibling was cancelled before it could answer the post-disconnect request.
|
||||||
|
|
||||||
|
`ruff check tests/test_context_final_return_disconnect.py`
|
||||||
|
|
||||||
|
All checks passed!
|
||||||
|
|
||||||
|
`git diff --check` passed. No production runtime edits were made.
|
||||||
|
|
@ -0,0 +1,61 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-19T14:54:31Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 0553fcf8
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20261019T145431Z_0553fcf8_ctx_return_fix_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
After committing the isolated regression on
|
||||||
|
wkt/ctx_final_return_disconnect, the human asked:
|
||||||
|
|
||||||
|
> committed, ok now a patch?
|
||||||
|
|
||||||
|
Earlier guidance requested runtime diagnostic information at the
|
||||||
|
production detection point, alongside the test's sibling metadata.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Added a narrow final-result delivery guard in _rpc._invoke(). A
|
||||||
|
TransportClosed while sending a successful context endpoint's final
|
||||||
|
Return now produces a metadata-rich warning rather than cancelling
|
||||||
|
the actor's shared service nursery. Actual endpoint transport errors
|
||||||
|
and cancellation retain the existing propagation path.
|
||||||
|
|
||||||
|
Extended the reproducer with a genuine endpoint-raised TransportClosed
|
||||||
|
control and an identity assertion, preventing broad suppression of
|
||||||
|
application failures. The formerly red context delivery case passes.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- tractor/runtime/_rpc.py: guard final response delivery and report
|
||||||
|
actor/task/context/nursery/channel metadata on peer disconnect.
|
||||||
|
- tests/test_context_final_return_disconnect.py: endpoint-error control.
|
||||||
|
- This provenance pair.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Ran from the Tractor worktree at committed base 0553fcf8 using
|
||||||
|
/home/goodboy/repos/piker/wkts/ib_downstream_pin_eval/py313/bin/python.
|
||||||
|
VIRTUAL_ENV was removed and LD_LIBRARY_PATH included the existing
|
||||||
|
Nix GCC runtime library; tests select isolated TCP addresses.
|
||||||
|
|
||||||
|
- pytest tests/test_context_final_return_disconnect.py tests/test_rpc.py
|
||||||
|
--tpt-proto=tcp: 10 passed in 0.93s.
|
||||||
|
- pytest tests/test_context_stream_semantics.py --tpt-proto=tcp:
|
||||||
|
65 passed in 59.80s.
|
||||||
|
- ruff check tractor/runtime/_rpc.py
|
||||||
|
tests/test_context_final_return_disconnect.py: passed.
|
||||||
|
- git diff --check: passed.
|
||||||
|
- No live broker/chart trial was run by the agent.
|
||||||
|
- All patch changes remain unstaged; no commit or ref move.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
None observed in this patch at capture time.
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-19T14:54:31Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 0553fcf8
|
||||||
|
diff_cmd: git diff HEAD -- tractor/runtime/_rpc.py tests/test_context_final_return_disconnect.py
|
||||||
|
---
|
||||||
|
|
||||||
|
## Captured response
|
||||||
|
|
||||||
|
I’ll patch the context final-result delivery path, then run the
|
||||||
|
reproducer and the related RPC/context tests.
|
||||||
|
|
||||||
|
## Generated patch
|
||||||
|
|
||||||
|
> `git diff HEAD -- tractor/runtime/_rpc.py`
|
||||||
|
|
||||||
|
Guard only chan.send(return_msg) after the context endpoint returns.
|
||||||
|
Catch normalized TransportClosed and warn with actor identity, task,
|
||||||
|
RPC/context ID, nursery, cancel scope, channel and transport error.
|
||||||
|
Keep endpoint execution and nursery-exit errors outside the guard.
|
||||||
|
|
||||||
|
> `git diff HEAD -- tests/test_context_final_return_disconnect.py`
|
||||||
|
|
||||||
|
Extend the real-TCP reproducer with an endpoint-raised TransportClosed
|
||||||
|
control. Verify its identity survives propagation and that swallowing
|
||||||
|
it would fail the test. Preserve the successful ordinary/context
|
||||||
|
cases' sibling responsiveness and RPC task cleanup assertions.
|
||||||
|
|
||||||
|
## Captured verification response
|
||||||
|
|
||||||
|
The patch turns the reproducer green. It catches
|
||||||
|
`TransportClosed` only around the final-result send and logs the
|
||||||
|
actor, task, RPC/context ID, nursery, cancel scope and channel.
|
||||||
|
|
||||||
|
An added control confirms that an endpoint’s own `TransportClosed`
|
||||||
|
still propagates with its original identity.
|
||||||
|
|
||||||
|
Checks passed: **10 RPC/reproducer cases + 65 context/stream
|
||||||
|
cases**, plus Ruff and whitespace checks.
|
||||||
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-20T19:36:19Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 0553fcf8
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20261020T193619Z_0553fcf8_review_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
Address three selected local Tuicr comments from
|
||||||
|
`address_logging_levels@wkt-ctx_final_return_disconnect/unstaged/0553fcf`.
|
||||||
|
The human requested detailed endpoint/nursery-owner task diagnostics,
|
||||||
|
an extra newline after the post-disconnect request prose, and warning
|
||||||
|
output consistent with runtime primitive pformat conventions. The `-r`
|
||||||
|
invocation authorizes local responses only; staging and commits remain
|
||||||
|
human-controlled.
|
||||||
|
|
||||||
|
Selected parent comment IDs:
|
||||||
|
- `337f29ca-73b2-4351-b150-d0160d67a2dc`
|
||||||
|
- `2c522ded-3bf1-4827-af8f-c27822d7c48d`
|
||||||
|
- `f00171c0-7a10-455f-a9e3-dd85bfa63df8`
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
The existing pformat_task_uid helper only names the current task. Kept
|
||||||
|
detailed arbitrary-task formatting local to the regression for now, and
|
||||||
|
reused existing primitive formatters in the runtime's warning path.
|
||||||
|
Preserved the narrow final-send suppression boundary and genuine endpoint
|
||||||
|
error propagation control. Captured a selected-path pre-review Git tree
|
||||||
|
without changing the real index, plus original comment/review-state data.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tests/test_context_final_return_disconnect.py`: task snapshots and note.
|
||||||
|
- `tractor/runtime/_rpc.py`: structured primitive warning reports.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
See the raw partner for the 10-pass modern-stack check, successful Ruff
|
||||||
|
and whitespace checks, expected-red diagnostic probe, and the separately
|
||||||
|
reproduced pre-existing Trio 0.29 cancellation-provenance failure.
|
||||||
|
Neither existing environment meets every current harness version bound:
|
||||||
|
the modern downstream interpreter has pytest 9.0.2; the worktree has an
|
||||||
|
older Trio. No dependency setup or lock-file changes were authorized.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
The captured pre-review work includes the runtime fix and endpoint-error
|
||||||
|
test control already under review. Existing comments, TODOs and review
|
||||||
|
markers were retained; this record describes only the new adjustments.
|
||||||
|
|
||||||
|
## Module-qualified call follow-up
|
||||||
|
|
||||||
|
After tooling commit `bf2bb1c9`, the human requested restoring the
|
||||||
|
existing `_pformat` module reference instead of a direct function import.
|
||||||
|
Removed that import and qualified the sibling, endpoint and owner task
|
||||||
|
calls, wrapping assignments to retain the source line-length convention.
|
||||||
|
The raw partner retains the original generation history. This formatting
|
||||||
|
correction changes no task-selection or disconnect behavior.
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-20T19:36:19Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 0553fcf8
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generated review adjustments
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tests/test_context_final_return_disconnect.py`
|
||||||
|
|
||||||
|
Added a test-local arbitrary-task formatter and snapshots of the endpoint
|
||||||
|
task and its nursery-owning task before the dispatcher wrapper exits.
|
||||||
|
The collateral-cancellation report includes actor identity, sibling,
|
||||||
|
endpoint and owner task fields, parent cancel-scope state and the original
|
||||||
|
cancellation repr. Added the requested blank line after the request prose.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/runtime/_rpc.py`
|
||||||
|
|
||||||
|
Replaced ad-hoc warning labels with Actor/Context/Channel pformat output,
|
||||||
|
the existing cancel-scope formatter and a nested transport-error report.
|
||||||
|
The TransportClosed guard still encloses only final-result transmission.
|
||||||
|
|
||||||
|
## Verification output
|
||||||
|
|
||||||
|
Modern downstream evaluation environment (Python 3.13.14, Trio 0.32.0,
|
||||||
|
pytest 9.0.2): regression plus RPC suite, 10 passed in 1.22s.
|
||||||
|
Standalone Ruff: All checks passed! Git whitespace check passed.
|
||||||
|
|
||||||
|
The worktree environment has Python 3.13.13, Trio 0.29.0 and pytest 9.1.0:
|
||||||
|
9 passed, 1 failed because the pre-existing test expects TransportClosed
|
||||||
|
in cancellation provenance, while this older Trio renders only Cancelled.
|
||||||
|
Executing the captured pre-review test in memory reproduced that assertion
|
||||||
|
failure; it was not caused by these review adjustments. No environment or
|
||||||
|
dependency was changed. The modern environment's pytest is below the
|
||||||
|
current harness reference's >=9.0.3 requirement; that limitation remains.
|
||||||
|
|
||||||
|
An in-memory probe removed only the final-send guard and reproduced the
|
||||||
|
expected TransportClosed failure. It verified the enhanced exception note
|
||||||
|
contains the blank separator, actor identity, three structured task
|
||||||
|
reports, task ownership fields and cancellation source. Source files were
|
||||||
|
unchanged by the probe. Initial scratch-probe validation errors were
|
||||||
|
corrected before the successful expected-red checks; they were not
|
||||||
|
repository test regressions.
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-21T14:53:44Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 0553fcf8
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20261021T145344Z_0553fcf8_task_format_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
The human requested moving the new regression-local Trio task formatter
|
||||||
|
into devx.pformat, scanning branches for earlier equivalents first, and
|
||||||
|
noted that log.pformat_task_uid already extracts the task ID. Follow-up:
|
||||||
|
the reviewed change was good enough apart from the function move; the
|
||||||
|
QA agent had already been alerted to the new order-bootstrap race.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Found no earlier detailed task formatter across 810 local refs and 18
|
||||||
|
worktrees. Extracted pformat_task into tooling and made the existing UID
|
||||||
|
helper accept an explicit task, preserving its established log output.
|
||||||
|
Added focused coverage that inspects a real child from its parent and
|
||||||
|
updated the cancellation regression to use the shared renderer.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/devx/pformat.py`: reusable pformat_task.
|
||||||
|
- `tractor/log.py`: optional explicit task for pformat_task_uid.
|
||||||
|
- `tests/devx/test_pformat.py`: real task-selection/ownership coverage.
|
||||||
|
- `tests/test_context_final_return_disconnect.py`: shared-tool callers.
|
||||||
|
|
||||||
|
## Review boundary and verification
|
||||||
|
|
||||||
|
Pre-move tree: `27e75d8b1cdfec1d96284d993e55ad3c51c3804c`.
|
||||||
|
HEAD remains `0553fcf88cd37b4d487eafb992071d4d1cc6e06f`.
|
||||||
|
The real index was unchanged at extraction start. Existing final-send
|
||||||
|
work and provenance are retained, including the earlier review record
|
||||||
|
that documents the then-local helper. Original Tuicr replies were not
|
||||||
|
rewritten; this extraction supersedes their local-helper description.
|
||||||
|
See the raw partner for environment details and focused verification.
|
||||||
|
|
||||||
|
Final verification repeated after import/line-wrap adjustments: 75
|
||||||
|
formatter/logger tests and 10 final-Return/RPC tests passed; Ruff,
|
||||||
|
whitespace and added-source 69-column checks passed. A read-only Tuicr
|
||||||
|
comparison found the saved session now contains only the first original
|
||||||
|
comment/reply pair, unlike the earlier three-pair receipt. This extraction
|
||||||
|
did not write session state or recreate the absent comments.
|
||||||
|
|
||||||
|
## Human edits
|
||||||
|
|
||||||
|
This extraction follows the human's requested final placement. Existing
|
||||||
|
logging TODOs, pre-review changes and task/review states are preserved.
|
||||||
|
|
@ -0,0 +1,49 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-21T14:53:44Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 0553fcf8
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
## Generated output
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/devx/pformat.py tractor/log.py`
|
||||||
|
|
||||||
|
Moved the regression-local task renderer into devx.pformat.pformat_task.
|
||||||
|
It accepts an explicit Trio task or defaults to the current task, and
|
||||||
|
reports identity, coroutine, parent nursery/owner and child nurseries.
|
||||||
|
The new renderer reuses log.pformat_task_uid for the short identity,
|
||||||
|
rather than independently extracting and slicing id(task).
|
||||||
|
Added a keyword-only optional task argument to that existing UID helper;
|
||||||
|
its current-task default, positional id_part and decimal head/tail output
|
||||||
|
remain unchanged. The full Task repr retains the full native identity.
|
||||||
|
Kept the UID import local to the richer renderer so log headers do not
|
||||||
|
gain a dependency on devx initialization. Existing UID/color TODOs remain.
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tests/test_context_final_return_disconnect.py tests/devx/test_pformat.py`
|
||||||
|
|
||||||
|
Removed the local formatter and switched its three consumers to the
|
||||||
|
shared tooling function. Added an actor-free live parent/child task test
|
||||||
|
that checks explicit-task selection, default current-task selection,
|
||||||
|
nursery ownership, coroutine identity, and unchanged head/tail log IDs.
|
||||||
|
The accepted final-result send guard is untouched by this extraction.
|
||||||
|
|
||||||
|
## Existing-work discovery
|
||||||
|
|
||||||
|
Read-only scan: 810 local branch/remote-tracking refs, 18 worktree path
|
||||||
|
inventories and task-formatting definitions, plus relevant all-ref history.
|
||||||
|
Only log.pformat_task_uid and this session's regression helper were found.
|
||||||
|
The dirty pformat.py in wkt/tipc_backend_wg_stack contains an unrelated
|
||||||
|
comment spelling correction; no other worktree was modified.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
Worktree Python 3.13.13 / pytest 9.1.0 / Trio 0.29.0: formatter and logger
|
||||||
|
suites, 75 passed. Downstream Python 3.13.14 / pytest 9.0.2 / Trio 0.32.0:
|
||||||
|
final-Return regression and RPC suite, 10 passed. The older pytest in the
|
||||||
|
downstream environment remains below the current harness reference's
|
||||||
|
version bound; neither environment was changed. Ruff passed. A source
|
||||||
|
line-length check prompted wrapping before final verification.
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-21T15:11:33Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 0553fcf8
|
||||||
|
scope: code
|
||||||
|
substantive: true
|
||||||
|
raw_file: 20261021T151133Z_0553fcf8_uid_move_prompt_io.raw.md
|
||||||
|
---
|
||||||
|
|
||||||
|
## Prompt
|
||||||
|
|
||||||
|
The human proposed moving pformat_task_uid into devx.pformat with the
|
||||||
|
new pformat_task and its unit test in a precursor commit, then placing
|
||||||
|
the final-result disconnect fix and matching tests in the next commit.
|
||||||
|
The request authorizes implementation and commit planning, not commits.
|
||||||
|
|
||||||
|
## Response summary
|
||||||
|
|
||||||
|
Moved the identity implementation into the formatting module, removed
|
||||||
|
the detailed formatter's reverse logging dependency, and preserved the
|
||||||
|
existing logging entry point through lazy forwarding. Retained the
|
||||||
|
existing TODO group and task-state markers. Extended focused identity
|
||||||
|
tests, verified the relocation and selected two dependency-ordered
|
||||||
|
whole-file commit boundaries with separate provenance.
|
||||||
|
|
||||||
|
## Files changed
|
||||||
|
|
||||||
|
- `tractor/devx/pformat.py`: canonical short and detailed task formatters.
|
||||||
|
- `tractor/log.py`: lazy forwarding entry point for existing log users.
|
||||||
|
- `tests/devx/test_pformat.py`: canonical/log entry-point equivalence.
|
||||||
|
|
||||||
|
## Verification and human edits
|
||||||
|
|
||||||
|
75 formatter/logger tests and 10 downstream final-Return/RPC tests passed.
|
||||||
|
Ruff and whitespace checks passed. Both existing environments are kept;
|
||||||
|
the downstream pytest-version caveat is disclosed in prior records.
|
||||||
|
Earlier raw prompt records are retained unchanged as generation history.
|
||||||
|
No staging, commits, history changes or Tuicr mutations were performed.
|
||||||
|
|
@ -0,0 +1,33 @@
|
||||||
|
---
|
||||||
|
created_at: 2026-10-21T15:11:33Z
|
||||||
|
generated_by:
|
||||||
|
harness: opencode
|
||||||
|
provider: openai
|
||||||
|
model: openai/gpt-6.1-sol
|
||||||
|
git_ref: 0553fcf8
|
||||||
|
diff_cmd: git diff HEAD~1..HEAD
|
||||||
|
---
|
||||||
|
|
||||||
|
> `git diff HEAD~1..HEAD -- tractor/devx/pformat.py tractor/log.py tests/devx/test_pformat.py`
|
||||||
|
|
||||||
|
Moved pformat_task_uid's implementation into devx.pformat alongside
|
||||||
|
pformat_task. The detailed renderer now calls the UID renderer directly;
|
||||||
|
it no longer imports logging. Kept the tractor.log entry point as a
|
||||||
|
lazy forwarding function to avoid eagerly initializing devx/debugger
|
||||||
|
modules while logging itself is being imported. Preserved the existing
|
||||||
|
current-task default and decimal head/tail ID output, plus explicit-task
|
||||||
|
selection added in the preceding extraction. Moved the existing TODO
|
||||||
|
group verbatim and did not change its checklist markers.
|
||||||
|
|
||||||
|
Extended the task test to compare the logging entry point and canonical
|
||||||
|
tooling renderer for current and selected child tasks in both ID modes.
|
||||||
|
The existing tests/devx/test_pformat.py module is reused.
|
||||||
|
|
||||||
|
Verification: formatter/logger suites 75 passed in 0.16s on the worktree
|
||||||
|
environment; final-Return/RPC suites 10 passed in 1.76s on the downstream
|
||||||
|
evaluation environment. Ruff and git diff --check passed. The downstream
|
||||||
|
pytest version remains 9.0.2; its current harness-version gap is recorded
|
||||||
|
in the earlier review/extraction provenance. No environment was changed.
|
||||||
|
|
||||||
|
Prepared the requested tooling-first, disconnect-fix-second boundary
|
||||||
|
selection. Planning itself does not execute checks, stage or commit.
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
# AI Prompt I/O Log
|
||||||
|
|
||||||
|
This directory tracks prompt inputs and model outputs for
|
||||||
|
AI-assisted development across harnesses.
|
||||||
|
|
||||||
|
## Policy
|
||||||
|
|
||||||
|
Prompt logging follows the [NLNet generative AI policy][nlnet-ai].
|
||||||
|
All substantive AI contributions are logged with:
|
||||||
|
|
||||||
|
- Original harness, and provider/model when known
|
||||||
|
- Timestamps
|
||||||
|
- The prompts that produced the output
|
||||||
|
- Unedited model output (`.raw.md` files)
|
||||||
|
|
||||||
|
[nlnet-ai]: https://nlnet.nl/foundation/policies/generativeAI/
|
||||||
|
|
||||||
|
## Usage
|
||||||
|
|
||||||
|
Entries are created by the `/prompt-io` skill or automatically via
|
||||||
|
`/commit-msg` integration.
|
||||||
|
|
||||||
|
Human contributors remain accountable for all code decisions.
|
||||||
|
AI-generated content is never presented as human-authored work.
|
||||||
|
|
@ -5,15 +5,82 @@ Unit tests for the `tractor.devx.pformat` render helpers.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
import trio
|
||||||
|
|
||||||
from tractor._exceptions import _mk_send_mte
|
from tractor._exceptions import _mk_send_mte
|
||||||
|
from tractor.log import pformat_task_uid as log_task_uid
|
||||||
from tractor.devx.pformat import (
|
from tractor.devx.pformat import (
|
||||||
pformat_boxed_tb,
|
pformat_boxed_tb,
|
||||||
pformat_caller_frame,
|
pformat_caller_frame,
|
||||||
|
pformat_task,
|
||||||
|
pformat_task_uid,
|
||||||
)
|
)
|
||||||
from tractor.msg._codec import _def_tractor_codec
|
from tractor.msg._codec import _def_tractor_codec
|
||||||
|
|
||||||
|
|
||||||
|
def test_pformat_task_selects_another_tasks_identity() -> None:
|
||||||
|
'''
|
||||||
|
Describe the selected task rather than the diagnostic caller.
|
||||||
|
|
||||||
|
The final-Return regression needs to snapshot an endpoint and its
|
||||||
|
nursery owner from another task. Hold a real child in an active
|
||||||
|
nursery and inspect it from its parent, checking both identities
|
||||||
|
and public nursery relationships without requiring an actor.
|
||||||
|
Preserve the log header's decimal head/tail ID convention while
|
||||||
|
allowing that existing helper to select an explicit task too.
|
||||||
|
|
||||||
|
'''
|
||||||
|
async def main() -> None:
|
||||||
|
'''
|
||||||
|
Keep the inspected child alive through all report assertions.
|
||||||
|
|
||||||
|
'''
|
||||||
|
async def child(
|
||||||
|
task_status: trio.TaskStatus[trio.lowlevel.Task],
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Publish the real task before waiting for cancellation.
|
||||||
|
|
||||||
|
'''
|
||||||
|
task_status.started(trio.lowlevel.current_task())
|
||||||
|
await trio.sleep_forever()
|
||||||
|
|
||||||
|
owner: trio.lowlevel.Task = trio.lowlevel.current_task()
|
||||||
|
owner_id: str = str(id(owner))
|
||||||
|
assert pformat_task_uid() == f'{owner.name}[{owner_id[-6:]}]'
|
||||||
|
assert pformat_task_uid('head') == (
|
||||||
|
f'{owner.name}[{owner_id[:6]}]'
|
||||||
|
)
|
||||||
|
assert log_task_uid() == pformat_task_uid()
|
||||||
|
assert log_task_uid('head') == pformat_task_uid('head')
|
||||||
|
async with trio.open_nursery() as nursery:
|
||||||
|
task: trio.lowlevel.Task = await nursery.start(child)
|
||||||
|
task_id: str = str(id(task))
|
||||||
|
task_uid: str = f'{task.name}[{task_id[-6:]}]'
|
||||||
|
report: str = pformat_task(task)
|
||||||
|
assert pformat_task_uid(task=task) == task_uid
|
||||||
|
assert pformat_task_uid('head', task=task) == (
|
||||||
|
f'{task.name}[{task_id[:6]}]'
|
||||||
|
)
|
||||||
|
assert log_task_uid(task=task) == task_uid
|
||||||
|
assert log_task_uid('head', task=task) == (
|
||||||
|
pformat_task_uid('head', task=task)
|
||||||
|
)
|
||||||
|
assert f'uid={task_uid!r}' in report
|
||||||
|
assert f'task={task!r}' in report
|
||||||
|
assert f'coro={task.coro!r}' in report
|
||||||
|
assert f'parent_nursery: {nursery!r}' in report
|
||||||
|
assert f'parent_task={owner!r}' in report
|
||||||
|
assert 'child_nurseries: ()' in report
|
||||||
|
assert f'child_nurseries: ({nursery!r},)' in (
|
||||||
|
pformat_task(owner)
|
||||||
|
)
|
||||||
|
assert pformat_task() == pformat_task(owner)
|
||||||
|
nursery.cancel_scope.cancel()
|
||||||
|
|
||||||
|
trio.run(main)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'box_tb',
|
'box_tb',
|
||||||
[True, False],
|
[True, False],
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,273 @@
|
||||||
|
'''
|
||||||
|
Isolate final-result delivery from sibling service lifetimes.
|
||||||
|
|
||||||
|
'''
|
||||||
|
from uuid import uuid4
|
||||||
|
|
||||||
|
import msgspec
|
||||||
|
import pytest
|
||||||
|
import tractor
|
||||||
|
import trio
|
||||||
|
|
||||||
|
from tractor.ipc import Channel
|
||||||
|
from tractor.devx import pformat as _pformat
|
||||||
|
from tractor.msg import Aid, StartAck, Started
|
||||||
|
from tractor.runtime import _rpc
|
||||||
|
|
||||||
|
|
||||||
|
@tractor.context
|
||||||
|
async def finish_after_disconnect(
|
||||||
|
ctx: tractor.Context,
|
||||||
|
peer_closed: trio.Event,
|
||||||
|
endpoint_finished: trio.Event,
|
||||||
|
endpoint_error: tractor.TransportClosed|None = None,
|
||||||
|
|
||||||
|
) -> str:
|
||||||
|
'''
|
||||||
|
Return or raise the supplied error after the caller disconnects.
|
||||||
|
|
||||||
|
'''
|
||||||
|
await ctx.started('ready')
|
||||||
|
await peer_closed.wait()
|
||||||
|
endpoint_finished.set()
|
||||||
|
if endpoint_error is not None:
|
||||||
|
raise endpoint_error
|
||||||
|
return 'finished'
|
||||||
|
|
||||||
|
|
||||||
|
async def ordinary_result(
|
||||||
|
peer_closed: trio.Event,
|
||||||
|
endpoint_finished: trio.Event,
|
||||||
|
|
||||||
|
) -> str:
|
||||||
|
'''
|
||||||
|
Exercise the already-hardened ordinary RPC final-result path.
|
||||||
|
|
||||||
|
'''
|
||||||
|
await peer_closed.wait()
|
||||||
|
endpoint_finished.set()
|
||||||
|
return 'finished'
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'context_endpoint, endpoint_transport_error',
|
||||||
|
[(False, False), (True, False), (True, True)],
|
||||||
|
ids=['ordinary-rpc', 'context', 'endpoint-transport-error'],
|
||||||
|
)
|
||||||
|
def test_final_return_disconnect_preserves_sibling(
|
||||||
|
reg_addr: tuple,
|
||||||
|
context_endpoint: bool,
|
||||||
|
endpoint_transport_error: bool,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
A departing caller must not cancel an unrelated service task.
|
||||||
|
|
||||||
|
Chart Ctrl-C made open_symbol_search finish before Tractor sent
|
||||||
|
its final Return. TransportClosed from that send escaped _invoke
|
||||||
|
and cancelled the shared service nursery, including IB feeds.
|
||||||
|
|
||||||
|
Use real loopback TCP channels and a real Actor/Context, invoking
|
||||||
|
_invoke directly to control the precise response-send ordering.
|
||||||
|
The caller consumes startup messages, closes its socket, and the
|
||||||
|
callee observes EOF and closes its channel before releasing the
|
||||||
|
successful endpoint. No transport exception is mocked or injected.
|
||||||
|
|
||||||
|
After _invoke completes, a sibling must still answer a request.
|
||||||
|
The ordinary-RPC case is a control for the existing response-send
|
||||||
|
hardening. The context case exposes the uncovered final Return.
|
||||||
|
Capture the sibling's task, actor, nursery and cancellation reprs
|
||||||
|
while unwinding so the failure identifies the collateral victim
|
||||||
|
and the cancellation source even after the runtime has exited.
|
||||||
|
Snapshot the endpoint task and its nursery's owning task before
|
||||||
|
they exit, retaining their coroutine and nursery ownership fields.
|
||||||
|
An endpoint-raised TransportClosed is a separate control: it
|
||||||
|
must retain its identity and current nursery propagation rather
|
||||||
|
than being suppressed as a final-result delivery failure.
|
||||||
|
This isolates response delivery; it does not exercise discovery,
|
||||||
|
process supervision, the asyncio bridge, or Qt cancellation.
|
||||||
|
|
||||||
|
'''
|
||||||
|
collateral_cancellations: list[tuple[trio.Cancelled, str]] = []
|
||||||
|
endpoint_error: tractor.TransportClosed|None = (
|
||||||
|
tractor.TransportClosed('endpoint transport failure')
|
||||||
|
if endpoint_transport_error else None
|
||||||
|
)
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
'''
|
||||||
|
Order peer EOF before endpoint completion without sleeps.
|
||||||
|
|
||||||
|
'''
|
||||||
|
peer_closed = trio.Event()
|
||||||
|
endpoint_finished = trio.Event()
|
||||||
|
invoke_finished = trio.Event()
|
||||||
|
ping = trio.Event()
|
||||||
|
pong = trio.Event()
|
||||||
|
sibling_stopped = trio.Event()
|
||||||
|
endpoint_task_info: str = ''
|
||||||
|
endpoint_owner_info: str = ''
|
||||||
|
|
||||||
|
async def sibling(
|
||||||
|
task_status: trio.TaskStatus = trio.TASK_STATUS_IGNORED,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Answer a post-disconnect request until explicitly stopped.
|
||||||
|
|
||||||
|
'''
|
||||||
|
task_status.started()
|
||||||
|
try:
|
||||||
|
await ping.wait()
|
||||||
|
pong.set()
|
||||||
|
await trio.sleep_forever()
|
||||||
|
except trio.Cancelled as exc:
|
||||||
|
if not pong.is_set():
|
||||||
|
assert endpoint_finished.is_set()
|
||||||
|
task: trio.lowlevel.Task = (
|
||||||
|
trio.lowlevel.current_task()
|
||||||
|
)
|
||||||
|
aid: Aid = actor.aid
|
||||||
|
nursery: trio.Nursery = task.parent_nursery
|
||||||
|
cancel_scope: trio.CancelScope = (
|
||||||
|
nursery.cancel_scope
|
||||||
|
)
|
||||||
|
sibling_info: str = _pformat.pformat_task(task)
|
||||||
|
scope_info: str = _pformat.pformat_cs(
|
||||||
|
cancel_scope,
|
||||||
|
var_name='services.cancel_scope',
|
||||||
|
)
|
||||||
|
runtime_info: str = (
|
||||||
|
f'Actor identity: {aid!r}\n'
|
||||||
|
f'\n'
|
||||||
|
f'Sibling task:\n{sibling_info}\n'
|
||||||
|
f'Endpoint task:\n{endpoint_task_info}\n'
|
||||||
|
f'Endpoint nursery owner:\n'
|
||||||
|
f'{endpoint_owner_info}\n'
|
||||||
|
f'{scope_info}\n'
|
||||||
|
f'Cancellation: {exc!r}\n'
|
||||||
|
)
|
||||||
|
collateral_cancellations.append(
|
||||||
|
(exc, runtime_info),
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
finally:
|
||||||
|
sibling_stopped.set()
|
||||||
|
|
||||||
|
with trio.fail_after(10):
|
||||||
|
async with tractor.open_root_actor(
|
||||||
|
registry_addrs=[reg_addr],
|
||||||
|
enable_transports=['tcp'],
|
||||||
|
loglevel='error',
|
||||||
|
) as actor:
|
||||||
|
listeners = await trio.open_tcp_listeners(
|
||||||
|
0,
|
||||||
|
host='127.0.0.1',
|
||||||
|
)
|
||||||
|
[listener] = listeners
|
||||||
|
async with listener:
|
||||||
|
port: int = listener.socket.getsockname()[1]
|
||||||
|
caller_stream = await trio.open_tcp_stream(
|
||||||
|
'127.0.0.1',
|
||||||
|
port,
|
||||||
|
)
|
||||||
|
callee_stream = await listener.accept()
|
||||||
|
|
||||||
|
async with caller_stream, callee_stream:
|
||||||
|
caller = Channel.from_stream(caller_stream)
|
||||||
|
callee = Channel.from_stream(callee_stream)
|
||||||
|
caller._aid = actor.aid
|
||||||
|
callee._aid = Aid(
|
||||||
|
name='departing-chart',
|
||||||
|
uuid=str(uuid4()),
|
||||||
|
)
|
||||||
|
cid: str = str(uuid4())
|
||||||
|
endpoint = (
|
||||||
|
finish_after_disconnect
|
||||||
|
if context_endpoint else ordinary_result
|
||||||
|
)
|
||||||
|
kwargs = {
|
||||||
|
'peer_closed': peer_closed,
|
||||||
|
'endpoint_finished': endpoint_finished,
|
||||||
|
}
|
||||||
|
if context_endpoint:
|
||||||
|
kwargs['endpoint_error'] = endpoint_error
|
||||||
|
|
||||||
|
async def invoke(
|
||||||
|
task_status: trio.TaskStatus,
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Run the actual RPC dispatcher to completion.
|
||||||
|
|
||||||
|
'''
|
||||||
|
nonlocal endpoint_task_info
|
||||||
|
nonlocal endpoint_owner_info
|
||||||
|
try:
|
||||||
|
await _rpc._invoke(
|
||||||
|
actor,
|
||||||
|
cid,
|
||||||
|
callee,
|
||||||
|
endpoint,
|
||||||
|
kwargs,
|
||||||
|
task_status=task_status,
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
endpoint_task: trio.lowlevel.Task = (
|
||||||
|
trio.lowlevel.current_task()
|
||||||
|
)
|
||||||
|
endpoint_task_info = (
|
||||||
|
_pformat.pformat_task(endpoint_task)
|
||||||
|
)
|
||||||
|
endpoint_owner_info = (
|
||||||
|
_pformat.pformat_task(
|
||||||
|
services.parent_task,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
invoke_finished.set()
|
||||||
|
|
||||||
|
async with trio.open_nursery() as services:
|
||||||
|
await services.start(sibling)
|
||||||
|
ctx = await services.start(invoke)
|
||||||
|
ack = await caller.recv()
|
||||||
|
assert isinstance(ack, StartAck)
|
||||||
|
assert ack.cid == cid
|
||||||
|
if context_endpoint:
|
||||||
|
started = await caller.recv()
|
||||||
|
assert isinstance(started, Started)
|
||||||
|
assert started.cid == cid
|
||||||
|
assert msgspec.msgpack.decode(
|
||||||
|
started.pld,
|
||||||
|
) == 'ready'
|
||||||
|
|
||||||
|
await caller.aclose()
|
||||||
|
with pytest.raises(tractor.TransportClosed):
|
||||||
|
await callee.recv()
|
||||||
|
await callee.aclose()
|
||||||
|
peer_closed.set()
|
||||||
|
|
||||||
|
await invoke_finished.wait()
|
||||||
|
assert endpoint_finished.is_set()
|
||||||
|
assert not sibling_stopped.is_set()
|
||||||
|
assert (callee, cid) not in actor._rpc_tasks
|
||||||
|
ping.set()
|
||||||
|
await pong.wait()
|
||||||
|
if context_endpoint:
|
||||||
|
assert ctx._result == 'finished'
|
||||||
|
services.cancel_scope.cancel()
|
||||||
|
|
||||||
|
try:
|
||||||
|
trio.run(main)
|
||||||
|
except tractor.TransportClosed as exc:
|
||||||
|
assert collateral_cancellations
|
||||||
|
cancelled, runtime_info = collateral_cancellations[0]
|
||||||
|
assert 'TransportClosed' in str(cancelled)
|
||||||
|
if endpoint_transport_error:
|
||||||
|
assert exc is endpoint_error
|
||||||
|
return
|
||||||
|
exc.add_note(
|
||||||
|
'The unrelated sibling was cancelled before it could '
|
||||||
|
'answer the post-disconnect request.\n'
|
||||||
|
f'\n'
|
||||||
|
f'{runtime_info}'
|
||||||
|
)
|
||||||
|
raise
|
||||||
|
assert not endpoint_transport_error, 'endpoint error suppressed'
|
||||||
|
|
@ -2,7 +2,9 @@
|
||||||
`tractor.log`-wrapping unit tests.
|
`tractor.log`-wrapping unit tests.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
from collections.abc import Callable, Iterator
|
||||||
import importlib
|
import importlib
|
||||||
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
|
@ -274,6 +276,204 @@ def test_add_log_level_pluggable():
|
||||||
delattr(log.StackLevelAdapter, name.lower())
|
delattr(log.StackLevelAdapter, name.lower())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(params=[False, True], ids=['raw', 'adapter'])
|
||||||
|
def level_guard(
|
||||||
|
request: pytest.FixtureRequest,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
) -> Iterator[tuple[logging.Logger, Callable[[int|str], bool]]]:
|
||||||
|
'''
|
||||||
|
Return an INFO(20) logger and a log-level enablement query.
|
||||||
|
|
||||||
|
`is_info_level_enabled(level)` returns True when the requested
|
||||||
|
log level meets the logger's current threshold and logging is
|
||||||
|
not disabled. It queries that decision; it does not emit a msg.
|
||||||
|
The initial INFO threshold permits INFO(20), CANCEL(22) and
|
||||||
|
WARNING(30), but blocks DEBUG(10) and NOTSET(0). Tests may change
|
||||||
|
the logger's threshold or disable flag to query those settings.
|
||||||
|
|
||||||
|
Each case allocates a fresh raw `logging.Logger` as `std_log`;
|
||||||
|
consuming tests do not supply a logger. `request.param` selects
|
||||||
|
only the returned query:
|
||||||
|
- False (`raw`): bind `tractor.log.at_least_level` to `std_log`.
|
||||||
|
- True (`adapter`): wrap that same logger in `StackLevelAdapter`
|
||||||
|
and return its `at_least_level` method. The raw logger never
|
||||||
|
supplies an `at_least_level` method.
|
||||||
|
|
||||||
|
Register only `std_log` under its name in
|
||||||
|
`Logger.manager.loggerDict` so `Logger.setLevel()` and
|
||||||
|
`logging.disable()` can invalidate its enabled-level cache.
|
||||||
|
`std_log.setLevel(...)` and changes to `std_log.disabled` affect
|
||||||
|
that logger, not other loggers. The registration is temporary;
|
||||||
|
`logging.disable()` changes the process-global cutoff. Restore
|
||||||
|
that cutoff and the registration after each case.
|
||||||
|
|
||||||
|
A consumer can use either query form in the same way::
|
||||||
|
|
||||||
|
def test_info_query(level_guard):
|
||||||
|
std_log, is_info_level_enabled = level_guard
|
||||||
|
assert std_log.level == logging.INFO
|
||||||
|
assert is_info_level_enabled('INFO')
|
||||||
|
assert not is_info_level_enabled('DEBUG')
|
||||||
|
|
||||||
|
'''
|
||||||
|
std_log = logging.Logger('level_guard_test', logging.INFO)
|
||||||
|
monkeypatch.setitem(
|
||||||
|
logging.Logger.manager.loggerDict,
|
||||||
|
std_log.name,
|
||||||
|
std_log,
|
||||||
|
)
|
||||||
|
orig_disable: int = logging.root.manager.disable
|
||||||
|
# Clear inherited global suppression so INFO queries can pass.
|
||||||
|
# NOTSET(0) removes logging.disable()'s positive-level cutoff.
|
||||||
|
logging.disable(logging.NOTSET)
|
||||||
|
try:
|
||||||
|
is_info_level_enabled: Callable[[int|str], bool]
|
||||||
|
if request.param:
|
||||||
|
guarded_log = log.StackLevelAdapter(std_log, {})
|
||||||
|
is_info_level_enabled = guarded_log.at_least_level
|
||||||
|
else:
|
||||||
|
is_info_level_enabled = partial(
|
||||||
|
log.at_least_level, std_log,
|
||||||
|
)
|
||||||
|
|
||||||
|
yield std_log, is_info_level_enabled
|
||||||
|
finally:
|
||||||
|
logging.disable(orig_disable)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
'level, enabled',
|
||||||
|
[
|
||||||
|
# String log levels are compared with the INFO(20) threshold.
|
||||||
|
('INFO', True),
|
||||||
|
('info', True),
|
||||||
|
('WARNING', True),
|
||||||
|
('warning', True),
|
||||||
|
('DEBUG', False),
|
||||||
|
('debug', False),
|
||||||
|
('ERROR', True),
|
||||||
|
('CRITICAL', True),
|
||||||
|
('WARN', True), # WARNING(30) alias, above INFO(20).
|
||||||
|
('FATAL', True), # CRITICAL(50) alias, above INFO(20).
|
||||||
|
('CANCEL', True), # Custom CANCEL(22), above INFO(20).
|
||||||
|
('cancel', True),
|
||||||
|
('TRANSPORT', False), # TRANSPORT(5), below INFO(20).
|
||||||
|
(logging.INFO, True), # 20 equals the INFO(20) threshold.
|
||||||
|
(logging.DEBUG, False), # 10 is below INFO(20).
|
||||||
|
(22, True), # CANCEL(22) is above INFO(20).
|
||||||
|
(0, False), # NOTSET(0) is below INFO(20).
|
||||||
|
('NOTSET', False),
|
||||||
|
('notset', False),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_at_least_level_names_and_integers(
|
||||||
|
level_guard,
|
||||||
|
level: int|str,
|
||||||
|
enabled: bool,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
String names of built-in log levels must not raise `KeyError`.
|
||||||
|
|
||||||
|
`tractor.log.at_least_level()` indexed `CUSTOM_LEVELS` for every
|
||||||
|
string query, so built-in logging levels such as 'INFO' raised
|
||||||
|
`KeyError` before testing whether logging was enabled. Query the
|
||||||
|
documented log levels and aliases, lowercase spellings, custom
|
||||||
|
levels and integer equivalents. Each result must match its
|
||||||
|
expected INFO(20) threshold comparison without changing that
|
||||||
|
threshold; NOTSET must resolve even though its value is zero.
|
||||||
|
|
||||||
|
See Python's `logging` docs for built-in level values:
|
||||||
|
https://docs.python.org/3/library/logging.html#logging-levels
|
||||||
|
|
||||||
|
'''
|
||||||
|
std_log, is_info_level_enabled = level_guard
|
||||||
|
assert is_info_level_enabled(level) is enabled
|
||||||
|
# A query must not reconfigure the INFO(20) logger.
|
||||||
|
assert std_log.level == logging.INFO
|
||||||
|
|
||||||
|
|
||||||
|
def test_at_least_level_registered_name(
|
||||||
|
level_guard,
|
||||||
|
monkeypatch: pytest.MonkeyPatch,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Query a custom log level registered with `logging.addLevelName()`.
|
||||||
|
|
||||||
|
`tractor.log.at_least_level()` raised `KeyError` when a string
|
||||||
|
log level was absent from `CUSTOM_LEVELS`, even when registered
|
||||||
|
by `logging.addLevelName()`. Register APP_LEVEL_GUARD(35), between
|
||||||
|
WARNING(30) and ERROR(40). Its lowercase query must pass at
|
||||||
|
INFO(20), and its uppercase query must fail at ERROR(40). Restore
|
||||||
|
the logging registrations afterward so later tests do not inherit
|
||||||
|
this custom level.
|
||||||
|
|
||||||
|
'''
|
||||||
|
monkeypatch.setattr(
|
||||||
|
logging, '_nameToLevel', logging._nameToLevel.copy(),
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
logging, '_levelToName', logging._levelToName.copy(),
|
||||||
|
)
|
||||||
|
# Custom 35 is above INFO(20), but below ERROR(40).
|
||||||
|
logging.addLevelName(35, 'APP_LEVEL_GUARD')
|
||||||
|
std_log, is_level_enabled = level_guard
|
||||||
|
assert is_level_enabled('app_level_guard') is True
|
||||||
|
std_log.setLevel(logging.ERROR)
|
||||||
|
assert is_level_enabled('APP_LEVEL_GUARD') is False
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('level', ['unknown_level_guard', '20', ''])
|
||||||
|
def test_at_least_level_unknown_name(
|
||||||
|
level_guard,
|
||||||
|
level: str,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Reject unknown log-level names with an explicit `KeyError`.
|
||||||
|
|
||||||
|
The old `CUSTOM_LEVELS` lookup raised `KeyError` with the
|
||||||
|
uppercase query as its key. Log-level name lookup must not
|
||||||
|
silently enable misspelled levels or treat numeric strings as
|
||||||
|
integer inputs. Each invalid name must still raise `KeyError`
|
||||||
|
carrying its uppercase spelling, including an empty name.
|
||||||
|
|
||||||
|
'''
|
||||||
|
std_log, is_info_level_enabled = level_guard
|
||||||
|
with pytest.raises(KeyError) as excinfo:
|
||||||
|
is_info_level_enabled(level)
|
||||||
|
assert excinfo.value.args == (level.upper(),)
|
||||||
|
assert std_log.level == logging.INFO
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize('level', ['info', 'cancel', logging.INFO, 22])
|
||||||
|
@pytest.mark.parametrize('suppression', ['logger', 'global'])
|
||||||
|
def test_at_least_level_disable_controls(
|
||||||
|
level_guard,
|
||||||
|
level: int|str,
|
||||||
|
suppression: str,
|
||||||
|
):
|
||||||
|
'''
|
||||||
|
Log-level name resolution must honor logging disable controls.
|
||||||
|
|
||||||
|
Earlier guards checked only `Logger.getEffectiveLevel()` and
|
||||||
|
rendered payloads despite disable controls. Prime a positive
|
||||||
|
enabled-level result, then disable the logger or all logging.
|
||||||
|
Built-in INFO and custom CANCEL string names, plus their integer
|
||||||
|
equivalents, must return False even though the logger's effective
|
||||||
|
INFO(20) threshold remains unchanged. Query once before disabling
|
||||||
|
to ensure a cached True result cannot bypass suppression.
|
||||||
|
|
||||||
|
'''
|
||||||
|
std_log, is_info_level_enabled = level_guard
|
||||||
|
assert is_info_level_enabled(level) is True
|
||||||
|
if suppression == 'logger':
|
||||||
|
std_log.disabled = True
|
||||||
|
else:
|
||||||
|
logging.disable(logging.CRITICAL)
|
||||||
|
|
||||||
|
assert is_info_level_enabled(level) is False
|
||||||
|
assert std_log.getEffectiveLevel() == logging.INFO
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
'suppression',
|
'suppression',
|
||||||
[
|
[
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,14 @@ import sys
|
||||||
import textwrap
|
import textwrap
|
||||||
import traceback
|
import traceback
|
||||||
|
|
||||||
from trio import CancelScope
|
from trio import (
|
||||||
|
CancelScope,
|
||||||
|
Nursery,
|
||||||
|
)
|
||||||
|
from trio.lowlevel import (
|
||||||
|
Task,
|
||||||
|
current_task,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def add_div(
|
def add_div(
|
||||||
|
|
@ -219,6 +226,70 @@ def pformat_caller_frame(
|
||||||
return tb_str
|
return tb_str
|
||||||
|
|
||||||
|
|
||||||
|
# TODO IDEAs:
|
||||||
|
# -[ ] move to `.devx.pformat`?
|
||||||
|
# -[ ] do per task-name and actor-name color coding
|
||||||
|
# -[ ] unique color per task-id and actor-uuid
|
||||||
|
def pformat_task_uid(
|
||||||
|
id_part: str = 'tail',
|
||||||
|
*,
|
||||||
|
task: Task|None = None,
|
||||||
|
|
||||||
|
) -> str:
|
||||||
|
'''
|
||||||
|
Render a task's name and a decimal head/tail slice of its ID.
|
||||||
|
|
||||||
|
Default to the current task, or select an explicit `task` when
|
||||||
|
rendering another task's identity from diagnostic tooling.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if task is None:
|
||||||
|
task = current_task()
|
||||||
|
tid: str = str(id(task))
|
||||||
|
if id_part == 'tail':
|
||||||
|
tid_part: str = tid[-6:]
|
||||||
|
else:
|
||||||
|
tid_part: str = tid[:6]
|
||||||
|
|
||||||
|
return f'{task.name}[{tid_part}]'
|
||||||
|
|
||||||
|
|
||||||
|
def pformat_task(
|
||||||
|
task: Task|None = None,
|
||||||
|
|
||||||
|
) -> str:
|
||||||
|
'''
|
||||||
|
Render a Trio task's identity, execution and nursery ownership.
|
||||||
|
|
||||||
|
Default to the current task. An explicit `task` lets a caller
|
||||||
|
inspect a sibling or nursery owner without selecting itself.
|
||||||
|
Capture the report while that task is alive to retain its active
|
||||||
|
`Task.parent_nursery` and `Task.child_nurseries` relationships.
|
||||||
|
|
||||||
|
'''
|
||||||
|
if task is None:
|
||||||
|
task = current_task()
|
||||||
|
task_uid: str = pformat_task_uid(task=task)
|
||||||
|
nursery: Nursery|None = task.parent_nursery
|
||||||
|
parent_task: Task|None = (
|
||||||
|
nursery.parent_task if nursery is not None else None
|
||||||
|
)
|
||||||
|
child_nurseries: tuple[Nursery, ...] = tuple(
|
||||||
|
task.child_nurseries,
|
||||||
|
)
|
||||||
|
return (
|
||||||
|
f'<Task(\n'
|
||||||
|
f' |_name: {task.name!r}\n'
|
||||||
|
f' uid={task_uid!r}\n'
|
||||||
|
f' task={task!r}\n'
|
||||||
|
f' coro={task.coro!r}\n'
|
||||||
|
f' |_parent_nursery: {nursery!r}\n'
|
||||||
|
f' parent_task={parent_task!r}\n'
|
||||||
|
f' |_child_nurseries: {child_nurseries!r}\n'
|
||||||
|
f')>\n'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def pformat_cs(
|
def pformat_cs(
|
||||||
cs: CancelScope,
|
cs: CancelScope,
|
||||||
var_name: str = 'cs',
|
var_name: str = 'cs',
|
||||||
|
|
|
||||||
|
|
@ -106,11 +106,20 @@ def at_least_level(
|
||||||
level: int|str,
|
level: int|str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
'''
|
'''
|
||||||
Predicate to test if a given level is active.
|
Return whether `log.isEnabledFor()` permits `level`.
|
||||||
|
|
||||||
|
Accept integer log levels or their case-insensitive string names.
|
||||||
|
`CUSTOM_LEVELS` takes precedence over names registered with
|
||||||
|
`logging.addLevelName()`, including the built-in logging levels.
|
||||||
|
Unknown names raise `KeyError` with the uppercased name.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
if isinstance(level, str):
|
if isinstance(level, str):
|
||||||
level: int = CUSTOM_LEVELS[level.upper()]
|
name: str = level.upper()
|
||||||
|
level = CUSTOM_LEVELS.get(name)
|
||||||
|
# Resolve stdlib registrations only on a CUSTOM_LEVELS miss.
|
||||||
|
if level is None:
|
||||||
|
level = logging.getLevelNamesMapping()[name]
|
||||||
|
|
||||||
return log.isEnabledFor(level)
|
return log.isEnabledFor(level)
|
||||||
|
|
||||||
|
|
@ -138,7 +147,7 @@ class StackLevelAdapter(LoggerAdapter):
|
||||||
|
|
||||||
def at_least_level(
|
def at_least_level(
|
||||||
self,
|
self,
|
||||||
level: str,
|
level: int|str,
|
||||||
) -> bool:
|
) -> bool:
|
||||||
return at_least_level(
|
return at_least_level(
|
||||||
log=self,
|
log=self,
|
||||||
|
|
@ -323,26 +332,20 @@ def add_log_level(
|
||||||
add_log_level('IO', 21, 'purple')
|
add_log_level('IO', 21, 'purple')
|
||||||
|
|
||||||
|
|
||||||
# TODO IDEAs:
|
|
||||||
# -[ ] move to `.devx.pformat`?
|
|
||||||
# -[ ] do per task-name and actor-name color coding
|
|
||||||
# -[ ] unique color per task-id and actor-uuid
|
|
||||||
def pformat_task_uid(
|
def pformat_task_uid(
|
||||||
id_part: str = 'tail'
|
id_part: str = 'tail',
|
||||||
):
|
*,
|
||||||
|
task: trio.lowlevel.Task|None = None,
|
||||||
|
|
||||||
|
) -> str:
|
||||||
'''
|
'''
|
||||||
Return `str`-ified unique for a `trio.Task` via a combo of its
|
Forward the logging entry point to devx.pformat's UID renderer.
|
||||||
`.name: str` and `id()` truncated output.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
task: trio.Task = trio.lowlevel.current_task()
|
# Import lazily: devx initialization itself imports tractor.log.
|
||||||
tid: str = str(id(task))
|
from .devx.pformat import pformat_task_uid as render_task_uid
|
||||||
if id_part == 'tail':
|
|
||||||
tid_part: str = tid[-6:]
|
|
||||||
else:
|
|
||||||
tid_part: str = tid[:6]
|
|
||||||
|
|
||||||
return f'{task.name}[{tid_part}]'
|
return render_task_uid(id_part, task=task)
|
||||||
|
|
||||||
|
|
||||||
_curr_actor_no_exc = partial(
|
_curr_actor_no_exc = partial(
|
||||||
|
|
|
||||||
|
|
@ -743,7 +743,38 @@ async def _invoke(
|
||||||
f'\n'
|
f'\n'
|
||||||
f'{pretty_struct.pformat(return_msg)}\n'
|
f'{pretty_struct.pformat(return_msg)}\n'
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
await chan.send(return_msg)
|
await chan.send(return_msg)
|
||||||
|
except TransportClosed as ipc_err:
|
||||||
|
# The endpoint completed successfully. Failure to
|
||||||
|
# deliver its Return to a departed peer must not
|
||||||
|
# cancel sibling RPCs in Actor._service_tn.
|
||||||
|
# Keep this guard outside `await coro` so an
|
||||||
|
# endpoint's own transport errors still propagate.
|
||||||
|
actor_info: str = actor.pformat(privates=True)
|
||||||
|
ctx_info: str = ctx.pformat(
|
||||||
|
extra_fields={'scope_nursery': tn},
|
||||||
|
indent='',
|
||||||
|
)
|
||||||
|
scope_info: str = _pformat.pformat_cs(
|
||||||
|
rpc_ctx_cs,
|
||||||
|
var_name='ctx._scope',
|
||||||
|
)
|
||||||
|
chan_info: str = chan.pformat(privates=True)
|
||||||
|
ipc_info: str = _pformat.nest_from_op(
|
||||||
|
input_op='x=>',
|
||||||
|
text=repr(ipc_err),
|
||||||
|
)
|
||||||
|
log.warning(
|
||||||
|
'Final context result delivery failed: '
|
||||||
|
'peer disconnected\n'
|
||||||
|
f'\n'
|
||||||
|
f'{actor_info}\n'
|
||||||
|
f'{ctx_info}\n'
|
||||||
|
f'{scope_info}\n'
|
||||||
|
f'{chan_info}\n'
|
||||||
|
f'{ipc_info}\n'
|
||||||
|
)
|
||||||
|
|
||||||
# NOTE: this happens IFF `ctx._scope.cancel()` is
|
# NOTE: this happens IFF `ctx._scope.cancel()` is
|
||||||
# called by any of,
|
# called by any of,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue