Commit Graph

111 Commits (c0692f32b5b2fb1c452ad238665d4151dceb3ff1)

Author SHA1 Message Date
Gud Boi c0692f32b5 Add `Address.rebind_from_sockname` opt-out
Gate `Endpoint.start_listener()`s `getsockname()`-vs-`.addr`
reconciliation on a new per-addr-type `ClassVar[bool]`, set
`True` on both `TCPAddress` and `UDSAddress` so existing
behaviour is bit-for-bit unchanged.

That reconciliation exists ONLY to learn a kernel-assigned
port from a `port=0` tcp bind (its own comment says so). The
incoming `tipc` backend (gh #378) has no late-binding
analogue AND its `getsockname()` answers a `TIPC_ADDR_ID`
port-id rather than the name-seq it published — rebinding
from that would swap a dialable service name for an
un-dialable, un-reconstructable port id.

So opting out is semantically right rather than a hack.

(this patch was generated in some part by `claude-code` using `claude-opus-5` (`anthropic`))
2026-09-01 19:28:17 -04:00
Gud Boi 299e044d49 Move network APIs to lazy `tractor.net`
Move bindspace, tunnel and WireGuard lifecycles out of actor
discovery and expose them through one lazy public package.

Deats,
- keep `import tractor` free of multiaddr, pyroute2 and WG impls
- move network-focused tests under `tests/net`
- update IPC, spawn, docs and multihost callers to the new API
- pin `CURRENT_NETNS` through the calling thread's procfs link

Prompt-IO: ai/prompt-io/opencode/20260830T025201Z_b1f6ade8_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-09-01 18:10:53 -04:00
Gud Boi 3a737f960b Expose tunnel namespaces on IPC endpoints
Make `TCPAddress` and `UDSAddress` explicitly satisfy
`Address.namespace`, then retain each original listener declaration
beside its peeled, resolved transport address.

Deats,
- remove `TunnelledAddress`'s attribute fallback
- add required `Endpoint.declared_addr` metadata
- report declaration namespaces in endpoint/server formatting
- preserve concrete `Endpoint.addr` for transport reflection
- cover plain and tunneled namespace visibility

Prompt-IO: ai/prompt-io/opencode/20260822T032520Z_d35c802b_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-09-01 17:56:13 -04:00
Gud Boi 0d95a9e63c Emit canonical tagged addresses
- Make `TCPAddress.unwrap()` emit `('tcp', host, port)` and
  `UDSAddress.unwrap()` emit `('unix', path)` while retaining the
  compatibility readers from the preceding change.

- Pass concrete TCP fields to Trio, compose multiaddrs from tagged
  values, and let `SpawnSpec` carry protocol-specific tuple shapes
  for validation by `wrap_address()`.

- Compare runtime, registry, bind, and tunnel addresses through
  canonical serialized forms and cover both TCP and UDS operation.

Prompt-IO: ai/prompt-io/opencode/20260820T033108Z_ba07e09d_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-09-01 15:47:28 -04:00
Gud Boi c857ad09f2 Decode tagged transport addresses
- Define canonical `tcp` and `unix` tuple shapes while retaining
  legacy pair aliases as the emitted `UnwrappedAddress`.

- Dispatch tagged tuple/list payloads explicitly, accept `uds` as a
  Unix input alias, and preserve legacy TCP, UDS, and native IPv6
  readers.

- Cover tag aliases, msgpack-style lists, legacy payloads, and IPv6
  socket addresses before switching writers.

Prompt-IO: ai/prompt-io/opencode/20260820T033107Z_ba07e09d_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-09-01 15:47:28 -04:00
Gud Boi ffb87d351e Peel tunnels before `Endpoint` binding
Carry tunnel declarations through listener configuration, then strip
them immediately before constructing transport endpoints.

Also allocate random listener addresses from a contacted registry's
overlay, and prove a real TCP listener never stores the wrapper while
the source declaration retains its bindspace metadata.

Prompt-IO: ai/prompt-io/opencode/20260819T213145Z_f81fc5e5_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-09-01 15:21:18 -04:00
Gud Boi b517f7f523 Peel tunnels before `Channel` connects
Retain tunnel annotations through address declaration, then hand only
the bindable overlay to exact-type transport lookup and dialing.

Broaden `Channel.from_addr()` and `_connect_chan()` inputs accordingly,
and cover plain plus tunnelled TCP dispatch arguments.

Prompt-IO: ai/prompt-io/opencode/20260819T213144Z_f81fc5e5_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-09-01 15:21:18 -04:00
Gud Boi 48844aa4ac Clarify bounded IPC frame publication
Older review threads left partial-frame scheduling, send-lock ownership,
deadline-only stream destruction and cancellation precedence unclear in
both transport tests and source comments.

Document exact sender/parent ordering, name send events explicitly and
explain why stream alignment controls sibling reuse. Clarify private
context controls, overrun relay failure and transport shield boundaries.

Review: PR #481 (goodboy)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-25 14:04:48 -04:00
Gud Boi bf46f5cc5e Clarify pointer and IPC cancellation contracts
Source prose left the stalled transport peer ambiguous, omitted why a
local namespace pointer retains its object and described cancellation
as interrupting a frame write which is now shielded.

Identify remote-peer and bounded-cancel behavior, document process-local
pointer caching, and explain the shield completion checkpoint which
makes startup cancellation protocol-safe on a connected channel.

Review: PR #481 (goodboy)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-5012942328

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-25 01:38:17 -04:00
Gud Boi 23e26b5b32 Bound `Portal.cancel_actor()` frame sends
A cancel RPC could stall forever in complete-frame transport
shielding before the peer received it, bypassing the outer ack
timeout and blocking graceful supervision.

- thread one absolute deadline from `Portal.cancel_actor()` through
  the private `Start` publication path
- force-close a partial-frame stream before releasing its send lock
- keep ordinary sends unbounded and preserve pending cancellation
- document the current `Start -> StartAck -> CancelAck` exchange and
  link the dedicated `Cancel` msg follow-up in #506
- cover partial publication and the shared send/ack timeout budget

Prompt-IO: ai/prompt-io/opencode/20260821T023537Z_ae6f2ac3_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-21 15:03:35 -04:00
Gud Boi 086962ca36 Preserve cancellation over shielded `send_all()` errors
The frame-publication shield added in `88a23449` checks for
pending cancellation only after a successful write. If actor
teardown closes the stream first, `ClosedResourceError` escaped
as `TransportClosed` and could defeat the caller's cancel scope.

Check for pending cancellation on the transport-error path before
normalizing the close. Genuine stream errors retain their existing
translation when no cancellation is active.

The cancellation-first path can swap which nested debugger
intermediary renders as the immediate source vs. relay. Keep
assertions over both actor levels while accepting either valid role.

Prompt-IO: ai/prompt-io/opencode/20260820T143845Z_559fd0f1_prompt_io.md

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-21 15:03:34 -04:00
Gud Boi 643e1c861b Complete IPC frames before sender cancellation
Cancellation inside `send_all()` can publish a partial frame. Closing
the actor-wide stream preserved framing but destroyed every context
on the channel and replaced primary errors with `TransportClosed`.

Deats,
- shield complete frame publication, then deliver pending
  cancellation
- keep the shared channel reusable after context-local cancellation
- absorb transport closure while reporting an unshippable overrun
- cover mid-frame cancellation and failed overrun error shipment

This deliberately defers cancellation until the current frame write
resolves; channel teardown remains the fallback for broken peers.

Prompt-IO: ai/prompt-io/opencode/20260819T234824Z_557065d8_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-21 15:03:34 -04:00
Gud Boi ecf89bfaac Close interrupted `MsgTransport.send()` streams
`SendStream.send_all()` can raise `trio.Cancelled` after writing an
arbitrary prefix of the four-byte length header and payload. The
peer can no longer distinguish a following msg boundary.

Close the stream under a shield before propagating cancellation so
callers can not append another msg to an indeterminate byte stream.

Caught-during: review remediation
Found-via: prospective P2 cancellation review

Review: PR #481 (opencode)
https://github.com/goodboy/tractor/pull/481#pullrequestreview-4956692120

Prompt-IO: ai/prompt-io/opencode/20260818T193001Z_bf06b4f8_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-21 15:03:34 -04:00
Gud Boi 93322405a1 Restore IPC transport style conventions
Bring the transport helpers back in line with project style:

- restore single-quote strings and docstrings;
- drop the oversized helper divider and simplify comments;
- keep guarded `match` dispatch so a missing `socket.AF_UNIX`
  remains safe when `HAS_UDS` is false.

Also, format the `HAS_UDS` conjunction with the project's
multiline branch convention.

Review: PR #475 (goodboy)
https://github.com/goodboy/tractor/pull/475

Prompt-IO: ai/prompt-io/opencode/20260817T231825Z_359fe75c_prompt_io.md

(this patch was generated in some part by `opencode` using `gpt-5.6-sol` (`openai`))
2026-08-17 19:37:44 -04:00
Gud Boi 40be587ce2 Keep `HAS_UDS` false on Windows
Modern Windows Python can expose `AF_UNIX`, so `trio.has_unix`
alone can register the UDS backend even though its credential and
lifecycle paths remain POSIX-only.

Gate `HAS_UDS` on `sys.platform` and make the Windows import smoke
step assert the TCP-only capability contract.

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-17 12:09:52 -04:00
Gud Boi 75385e448d Derive transport registries from one list
The five transport/address lookup maps each hand-guarded `uds`
with its own `if HAS_UDS:` block (3 in `ipc/_types`, 2 in
`discovery/_addr`) — easy to let drift so a backend half-registers
(known by address but not by key, listed but unroutable, &c).

- build one `_msg_transports` / `_address_protos` list per module
  (TCP always, UDS only when `HAS_UDS`), then DERIVE every map
  from it via each backend's ClassVars (`codec_key`,
  `address_type`, `proto_key`). Adding a backend now touches one
  list, and the maps can't disagree.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-08-14 23:17:09 -04:00
Gud Boi 72441124c0 Fix Windows `import tractor` at the UDS root
The prior round gated UDS in four modules but `import tractor`
still crashed on Windows: `tractor.ipc._uds` does `from socket
import AF_UNIX` at module top, and several modules in the import
graph (`discovery._api`, `spawn._reap`, `discovery._multiaddr`,
`_testing.addr`) import `_uds` unconditionally. Instead of
guarding every importer, fix the root and collapse the per-module
probes to one capability flag.

- in `ipc/_uds.py`, guard the lone `AF_UNIX` import so the module
  stays importable everywhere; expose `HAS_UDS = trio.has_unix`
  as the single source of truth (the same predicate that gates
  `trio.open_unix_socket()`).
- `ipc/_types.py`, `discovery/_addr.py` and `ipc/_server.py` now
  import `UDSAddress`/`MsgpackUDSStream`/`HAS_UDS` directly and
  gate the transport + address registries on `HAS_UDS`; drop the
  duplicated `getattr(socket,'AF_UNIX')` / `platform.system()`
  probes, the dead `HAS_AF_UNIX` conjunct, and the import-time
  `log.warning()` spam.
- `devx/_stackscope.py` `enable_stack_on_sig()` early-returns
  when `sig is None`, so a missing `SIGUSR1` (Windows) degrades
  to a no-op instead of a `TypeError` from `getsignal()` /
  `signal()`.
- add a `windows-latest` CI leg (UDS excluded; informational via
  `continue-on-error` while support matures) plus an `import
  tractor` smoke step as the hard signal for the import fix.

Because `_uds` is importable everywhere `UDSAddress` stays a real
class, so `isinstance()` checks and `wrap_address()` no longer
`AttributeError` on no-UDS hosts; actual socket use stays gated
on `has_unix`.

Review: https://github.com/goodboy/tractor/pull/475
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-08-14 23:12:01 -04:00
Gud Boi 0cbb850645 Make UDS + `SIGUSR1` optional for Windows
Windows (and any CPython that doesn't expose `socket.AF_UNIX`)
can't import the UDS transport backend nor `signal.SIGUSR1`, so
the unconditional imports break `import tractor` outright on
those hosts. Guard the platform-specific bits behind capability
probes and fall back to a TCP-only runtime when the UDS backend
is unavailable.

- across `discovery/_addr.py`, `ipc/_server.py` and
  `ipc/_types.py`, gate on `getattr(socket, 'AF_UNIX', None)` +
  `platform.system()` and import `UDSAddress` /
  `MsgpackUDSStream` only when supported, leaving the names as
  `None` otherwise.
- register the `'uds'` key in `_address_types`, its
  default-loopback addr, and the transport lookup maps only when
  the backend actually loads, so TCP keeps working standalone.
- in `devx/_stackscope.py`, import `SIGUSR1` conditionally and
  set it to `None` on Windows.

Rebased onto the post-reorg tree where `_addr.py` now lives
under `tractor/discovery/`; adapt the relocated imports to the
package's `..ipc._uds` / `..ipc._tcp` paths (the original
single-dot paths would silently disable UDS on POSIX) and drop a
duplicated `TYPE_CHECKING` block and dead `import logging` left
by the move.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-08-14 22:35:40 -04:00
Gud Boi f75c9cdeab Traverse `BaseExceptionGroup` peer-close errors
`_peer_closed_errno()` followed cause and context links but did not
descend through grouped exceptions. A reset below a group could
therefore escape as `trio.BrokenResourceError` instead of the
normalized `TransportClosed` boundary.

Walk the exception tree with cycle protection, requiring every
group branch to represent peer closure before normalization. Extend
the `MsgpackTransport.send()` regression to prove all-transport and
mixed-failure behavior.

Review: PR #480 (goodboy)
https://github.com/goodboy/tractor/pull/480

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-14 18:54:56 -04:00
Gud Boi 584ea4e9ad Document stable macOS UDS operation
The remediation grew beyond the original no-autobind fix, leaving
public docs and nearby comments describing connect-only discovery,
XDG-only socket paths, raw readiness probes, and old cleanup naming.

Document typed registrar probing, occupied-address rejection, Darwin's
short runtime directory, platform-aware socket cleanup, sentinel
readiness, and subprocess output draining. Add the GH #473 bugfix
fragment and update regression rationale without changing task states.

Review: PR #480 (goodboy)
https://github.com/goodboy/tractor/pull/480

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-14 10:37:19 -04:00
Gud Boi 1a9ce915f3 Harden inbound actor handshakes
Registry probes need short retry deadlines, but applying their
one-second budget to every portal and child connection can terminate a
valid delayed actor with no client retry path.

Give ordinary pre-registration handshakes an independent ten-second
deadline. Normalize raw `msgspec.DecodeError` frames to
`TransportClosed` so malformed peers cannot cancel the shared IPC
nursery with decoder internals.

Cover malformed frames and the ordinary-vs-probe timeout distinction.

Review: PR #480 (goodboy)
https://github.com/goodboy/tractor/pull/480

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-14 10:34:59 -04:00
Gud Boi 0b63af020e Retry transient registrar handshakes
A loaded runner can accept a registry transport while delaying its
actor handshake beyond the first one-second attempt. Treating that
timeout as final classifies a healthy daemon as occupied and cascades
into unrelated discovery failures.

Retry connected handshake failures on fresh channels under a shared
three-second budget, while returning immediately for truly absent
listeners. Bound every connect-plus-handshake attempt, use incremental
backoff, and retain the one-second unauthenticated server limit.

Also bound shielded probe-channel cleanup to 200ms and cover the real
timeout, reconnection, backoff, fresh-channel, and stalled-close paths.

Review: PR #480 (goodboy)
https://github.com/goodboy/tractor/pull/480

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-13 23:40:12 -04:00
Gud Boi 5724c0516a Probe registrar capability in actor handshakes
Transport connect alone can select a foreign, stalled, or ordinary
actor endpoint as the registry. On macOS UDS this also exercises a
fragile connect-and-bail path before every root election.

Extend `Aid` with backward-compatible probe and registrar capability
fields, require a bounded typed handshake, and classify addresses as
absent, occupied, or confirmed registrars. Reject occupied endpoints
instead of binding over them.

Also,
- close `_connect_chan()` in `finally`
- bypass normal peer tracking for election probes
- preserve legacy registrar handshakes with unknown capability
- cover foreign listeners and idle registrar peer state

Review: PR #480 (goodboy)
https://github.com/goodboy/tractor/pull/480

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-13 20:04:48 -04:00
Gud Boi 340d506940 Bound generated UDS socket paths
Actor names and custom runtime subdirs can otherwise produce unsafe
or overlong pathname sockets after moving Darwin's bindspace to
`/tmp`.

Deats,
- hash unsafe or over-budget actor names while retaining `@pid.sock`
- share deterministic naming with post-kill socket cleanup
- enforce Linux and Darwin `sun_path` byte budgets
- restore non-Darwin dir validation and mode `0700`
- validate every nested Darwin runtime-dir component

Review: PR #480 (goodboy)
https://github.com/goodboy/tractor/pull/480

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-13 18:40:55 -04:00
Gud Boi 64e820e18e Normalize peer resets in `.send()`
A raw UDS readiness client can disconnect before the actor handshake.
Darwin reports the first server write as `ECONNRESET`, wrapped in
`trio.BrokenResourceError`; letting it escape cancels the daemon's
shared IPC nursery and makes later roots elect themselves registrar.

Walk the exception chain for `EPIPE` or `ECONNRESET` and translate
either into the existing `TransportClosed` boundary. Also handle
argument-less resource errors without raising `IndexError`.

Review: PR #480 (goodboy)
https://github.com/goodboy/tractor/pull/480

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-13 18:30:44 -04:00
Gud Boi bceca74eb7 Fix UDS addr corruption sans-autobind (macOS)
`MsgpackUDSStream.get_stream_addrs()` matches the
`(peername, sockname)` pair by type to find the listener's
fs-path, but the `(str, str)` arm unconditionally takes
`peername`: on platforms without linux's
`SO_PASSCRED`-triggered autobind (macOS!) the accept side's
`getpeername()` is `''`, so every accepted conn gets garbage
`Path('')` laddr/raddr structs.

Proven on linux by disabling `SO_PASSCRED` (no autobind ->
same `''` shape as darwin): the `uds_transport_actor_tree.py`
example reports `listener sock file: .` pre-fix and the real
registry sockpath post-fix.

- pick the non-empty name in the `(str, str)` arm: `peername`
  on the connect side, `sockname` on the accept side; raise
  `ValueError` on an (unexpected) empty pair.
- document the linux-autobind origin of the `bytes` arms
  which the original impl noted as "unclear".
- `start_listener()`: create the bindspace dir with
  `parents=True, exist_ok=True` (nested custom `filedir`s +
  racing actors).
- example docstring: peer-pid comes via `SO_PEERCRED` on
  linux but `LOCAL_PEERPID` on macOS.

May not be the (only) macOS crasher for GH #473 — it is
non-fatal on the linux sim — but with stderr surfacing now
in place the next macOS CI run pins any remaining layer.

Prompt-IO: ai/prompt-io/claude/20260702T155006Z_65bf9df5_prompt_io.md

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-08-13 15:46:56 -04:00
Gud Boi 7987f6b8d7 Keep lazy annotations runtime-resolvable
Provide import-free runtime aliases for annotation-only actor and
multiaddr types so `typing.get_type_hints()` remains usable without
eagerly loading optional dependencies.

Correct `_address_types` to its actual `dict` shape and cover the
affected discovery and transport APIs.

Caught-during: review remediation
Found-via: `/run-tests` test_lazy_annotation_names_resolve

Review: PR #478 (goodboy)
https://github.com/goodboy/tractor/pull/478#pullrequestreview-4922213201

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-12 20:52:15 -04:00
Gud Boi d1d3fc58bb Lazy-import optional 3rd-party deps (gh #470)
Move every import-time-only-by-accident dep off the eager
`import tractor` path so cold child-actor boots only pay for
what they actually use:

- `bidict` -> `TYPE_CHECKING` in `discovery._addr`
  (annotation-only; `_address_types` is a plain `dict`
  literal).
- `multiaddr` -> `TYPE_CHECKING` + fn-local imports in
  `discovery._multiaddr.mk_maddr()`/`parse_maddr()`; also
  `TYPE_CHECKING` the `Multiaddr` annots in `ipc._tcp`/`._uds`
  (adds future-annots to `._multiaddr`).
- `colorlog` -> fn-local in `log.get_console_log()`.
- `pdbp` + `wrapt` -> fn-local in
  `devx._frame_stack.hide_runtime_frames()`/`api_frame()`.
- `platformdirs` -> fn-local in `runtime._state.get_rt_dir()`.

Still eager (documented follow-ups),
- `pdbp` via `devx.debug._repl` class-bases
  (`PdbREPL(pdbp.Pdb)`) + the module-lvl `@pdbp.hideframe` in
  `._tty_lock`; needs a `._repl` restructure.
- `platformdirs` via the `UDSAddress.def_bindspace: ClassVar`
  class-body eval of `get_rt_dir()`; needs an `Address`-proto
  rework.
- `stackscope` is already fn-local; `setproctitle` is not
  imported anywhere.

Prompt-IO: ai/prompt-io/claude/20260702T155626Z_65bf9df5_prompt_io.md
(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-08-12 19:57:28 -04:00
Gud Boi 935c8cf656 Guard receive-path transport rendering
Skip raw packet, decoded message, peer, and channel formatting when
transport logging is disabled. Keep message processing and wire
reads outside the guards so logging controls never affect IPC flow.

Also narrow the remaining pretty-struct TODO to require a
non-raising formatter with native-repr fallback.

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-12 13:48:40 -04:00
Gud Boi 84ec895150 Drop resolved channel log-guard TODO
Remove the stale `Channel.from_addr()` design note now that its
`at_least_level()` guard avoids inactive pretty-rendering work.

(this patch was generated in some part by `opencode` using
`gpt-5.6-sol` (`openai`))
2026-08-12 12:51:43 -04:00
root 0e11ff7e9d Guard hot-path log calls to avoid payload rendering when disabled
Wrap `log.transport()` in `Channel.send()` and `log.runtime()` in
`PldRx.decode_pld()` with `log.at_least_level()` checks so that
expensive `pformat(payload)` / `repr(msg)` / `repr(pld)` calls are
skipped entirely when the respective log level is not active.

Previously the f-string arguments were eagerly evaluated before being
passed to the log method, even though `StackLevelAdapter.log()` would
then discard the message internally via its own `isEnabledFor()` check.
On high-frequency IPC paths this caused `pformat` to dominate CPU
usage (~60-70 %) as reported in #455.

This also restores the full diagnostic output (msg type, decoded
payload) that was temporarily commented out in 0373164 as a stopgap.

Resolves #455
2026-08-11 20:41:20 -04:00
mahmoud 148a098ca6 avoid format on the hot send path 2026-08-11 20:41:20 -04:00
Gud Boi 51326f4b7a Fix uds `get_random` reaper-regex break (+2 nits)
Copilot's 2nd-pass review (PR #468) caught a regression I landed in
the uds fix: `UDSAddress.get_random()` put the per-call token AFTER
`@{pid}` (`no_runtime_*@{pid}.{token}.sock`), which breaks the
`tractor._testing._reap` matcher
`^(?P<name>.+)@(?P<pid>\d+)\.sock$` — so no-runtime orphan socks
stopped matching and never got reaped/attributed. Move the token
INTO the name (`{prefix}.{token}@{pid}.sock`) so the canonical
`@{pid}.sock` suffix stays intact for both that regex and the
`spawn._reap` reconstruction; also bump the token to 8 hex chars.

Also two robustness nits from the same review,
- `tests.conftest._measure_sustained_headroom()`: guard `frac <= 0`
  before `1./frac` — a 0/parked-core freq read would
  `ZeroDivisionError`, get swallowed by the broad `except` into a
  1.0 (no-throttle), defeating the probe on the exact broken box it
  should flag; read 0 as max throttle.
- `scripts/cpu-perf-check`: mark the burn procs `daemon=True` and
  wrap sampling in `try/finally` so a Ctrl-C / error reaps them
  instead of leaving stray CPU hogs.

Regressed-by: 09c50f49 (uds no-runtime token placed after `@pid`)
Found-via: Copilot review #4595803812 (`_testing._reap` regex)
Review: PR #468 (Copilot)
https://github.com/goodboy/tractor/pull/468#pullrequestreview-4595803812

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-06-29 18:51:26 -04:00
Gud Boi 09c50f495d Uniquify no-runtime UDS `get_random()` paths
W/o a live runtime `get_random()` named UDS socks purely by
`(prefix, pid)`, so two calls in one proc returned the SAME
`no_runtime_*@{pid}.sock` — the 2nd `.bind()` then tripped
`EADDRINUSE`. Append a per-call `uuid4().hex[:6]` token so
each call yields a distinct sockpath.

This fixes the 3 `tests.discovery.test_tpt_bind_addrs` uds
failures (one registrar + disjoint-bind, two non-registrar
binds) where `reg_addr` and a "random" bind addr aliased —
the uds CI job's only red, surfaced when this branch rebased
onto the newer base that carries those tests.

Scoped to the no-runtime branch ON PURPOSE: the runtime
`{name}@{pid}` convention stays deterministic so
`spawn._reap.unlink_uds_bind_addrs()` can still reconstruct
+ unlink a SIGKILL'd subactor's sock (#454).

Deats,
- `_uds.py`: add `uuid4` import + token in the no-runtime
  branch
- `_testing.addr`: tighten `get_rando_addr()` docstring re the
  intra-proc per-call token (not just pid-keyed namespacing)

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-06-29 18:08:50 -04:00
Gud Boi 6255c17db9 Use `is not None` check for peer-connect `event`
Matches the explicit `dict.pop(uid, None)` contract one
line above; same semantics as the prior truthy check.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code

(cherry picked from commit 0e3e008b0c)
(cherry picked from commit 13ed668512)
2026-06-24 18:19:16 -04:00
Gud Boi c797bcb783 Address Copilot review nits on PR #462
All 5 flagged items were valid (4 real bugs + 1 dead assert),

- fix an inverted `sys.version_info < (3, 14)` guard in
  `ipc._linux` — the "`cffi` has no 3.14 support" import note now
  fires on 3.14+ (where it applies) instead of on older pys.
- use `os.environ.get('PYTHON_COLORS')` in the `sync_bp` example
  so it doesn't `KeyError` when run outside the test harness.
- correct `dump_task_tree()`'s docstring: the `/tmp` + `/dev/tty`
  tee is gated on `write_file`/`write_tty`, not "unconditional".
- tidy the `ActorTooSlowError` message spacing in `cancel_actor`.
- replace a tautological `applied is True or applied is False` in
  `test_patches` with `isinstance(applied, bool)` (the value is
  order-dependent across the module).

Review: PR #462 (copilot-pull-request-reviewer)
https://github.com/goodboy/tractor/pull/462#pullrequestreview-4527179852

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-06-18 13:37:55 -04:00
Gud Boi 0c64e76bf9 Fix shutdown deadlock on UDS unlink race
Wrap `os.unlink()` in `close_listener()` with a `FileNotFoundError`
guard — under concurrent pytest sessions the sock-file can already be
reaped. Without this the raise aborts `_serve_ipc_eps`'s finally before
`_shutdown.set()`, deadlocking `wait_for_shutdown()` on
`actor.cancel()`.

Also,
- close each endpoint independently in the finally so one raise doesn't
  strand the rest.
- always signal `_shutdown.set()` regardless of remaining ep count.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code

(cherry picked from commit 2ee44a6fdd)
2026-06-17 17:39:44 -04:00
Gud Boi 0b6a7aa1a9 Fix `SharedMemory` under `subint_forkserver`
Implements the resolution described in c99d475d's
`subint_forkserver_mp_shared_memory_issue.md` (now
updated with the resolution post-mortem). Two-part
fix that side-steps `mp.resource_tracker` entirely
rather than try to make it fork-safe — turns out
that's both simpler AND more correct given tractor
already SC-manages allocation lifetimes.

Deats,
- `tractor/ipc/_mp_bs.py::disable_mantracker()`: drop the
  `platform.python_version_tuple()[:-1] >= ('3', '13')` branch — patches
  now run unconditionally:
  * monkey-patch `mp.resource_tracker. _resource_tracker` to a no-op
    `ManTracker` subclass (empty `register` / `unregister`
    / `ensure_running`).
  * return `partial(SharedMemory, track=False)` for the per-allocation
    opt-out.
  * belt + suspenders: even if something dodges the wrapper, the
    singleton can't talk to the inherited (broken) parent fd.

- `tractor/ipc/_shm.py::open_shm_list()`: drop the 3.13+ conditional
  skip of the unlink-callback; install a `try_unlink()` wrapper that
  swallows `FileNotFoundError` (sibling-already-cleaned race in
  shared-key setups). Without `mp.resource_tracker` doing it for us, we
  own the unlink — `actor. lifetime_stack` is the right place since
  tractor already controls actor lifecycle.

- `tests/test_shm.py`: uncomment-out `subint_forkserver` from the
  module-level skip- list (tests pass now). Inline comment cross-refs
  the two `_mp_bs` / `_shm` workarounds.

- `ai/conc-anal/subint_forkserver_mp_shared_memory_ issue.md`: heavy
  rewrite — flips status from "open / unresolvable in tractor" to
  "resolved, kept as decision record". Adds Resolution section, "Why
  this is the right call" rationale (mp tracker is widely criticized;
  tractor already owns lifecycle), trade-offs (crash-leaked segments,
  lost mp leak warning), verification (7 passed under both
  `subint_forkserver` and `trio` backends), and upstream issue links

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code

(cherry picked from commit aa3e230926)
(factored: dropped subint_forkserver conc-anal doc update)
2026-06-17 17:39:44 -04:00
Gud Boi 4b63b7f1cc Handle py3.14+ incompats as test skips
Since we're devving subints we require the 3.14+ stdlib API
and a couple compiled libs don't support it yet, namely:
- `cffi`, which we're only using for the `.ipc._linux` eventfd
  stuff (now factored into `hotbaud` anyway).
- `greenback`, which requires `greenlet` which doesn't seem to be
  wheeled yet
  * on nixos the sdist build was failing due to lack of `g++` which
    i don't care to figure out rn since we don't need `.devx` stuff
    immediately for this subints prototype.
  * [ ] we still need to adjust any dependent suites to skip.

Adjust `test_ringbuf` to skip on import failure.

Also project wide,
- pin us to py 3.13+ in prep for last-2-minor-version policy.
- drop `msgspec>=0.20.0`, the first release with py3.14 support.

(cherry picked from commit d2ea8aa2de)
2026-06-17 17:39:44 -04:00
Gud Boi 70dc60a199 Bump UDS `listen()` backlog 1 -> 128 for multi-actor unreg
A backlog of 1 caused `ECONNREFUSED` when multiple
sub-actors simultaneously connect to deregister from
a remote-daemon registrar. Now matches the TCP
transport's default backlog (~128).

Also,
- add cross-ref comments between
  `_uds.close_listener()` and `async_main()`'s
  `parent_is_reg` deregistration path explaining
  the UDS socket-file lifecycle

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-04-14 19:54:15 -04:00
Gud Boi 75b07c4b7c Show trailing bindspace-path-div in `repr(UDSAddress)` 2026-04-14 19:54:14 -04:00
Gud Boi c3d6cc9007 Rename `discovery._discovery` to `._api`
Adjust all imports to match.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-04-14 19:54:14 -04:00
Gud Boi 926e861f52 Use upstream `py-multiaddr` for `._multiaddr`
Drop the NIH (notinventedhere) custom parser (`parse_maddr()`,
`iter_prot_layers()`, `prots`/`prot_params` tables) which was never
called anywhere in the codebase.

Replace with a thin `mk_maddr()` factory that wraps the upstream
`multiaddr.Multiaddr` type, dispatching on `Address.proto_key` to build
spec-compliant paths.

Deats,
- `'tcp'` addrs detect ipv4 vs ipv6 via stdlib
  `ipaddress` (resolves existing TODO)
- `'uds'` addrs map to `/unix/{path}` per the
  multiformats protocol registry (code 400)
- fix UDS `.maddr` to include full sockpath
  (previously only used `filedir`, dropped filename)
- standardize protocol names: `ipv4`->`ip4`,
  `uds`->`unix`
- `.maddr` properties now return `Multiaddr` objs
  (`__str__()` gives the canonical path form so all
  existing f-string/log consumers work unchanged)
- update `MsgTransport` protocol hint accordingly

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-04-14 19:54:14 -04:00
Gud Boi 9ec2749ab7 Rename `Arbiter` -> `Registrar`, mv to `discovery._registry`
Move the `Arbiter` class out of `runtime._runtime` into its
logical home at `discovery._registry` as `Registrar(Actor)`.
This completes the long-standing terminology migration from
"arbiter" to "registrar/registry" throughout the codebase.

Deats,
- add new `discovery/_registry.py` mod with `Registrar`
  class + backward-compat `Arbiter = Registrar` alias.
- rename `Actor.is_arbiter` attr -> `.is_registrar`;
  old attr now a `@property` with `DeprecationWarning`.
- `_root.py` imports `Registrar` directly for
  root-actor instantiation.
- export `Registrar` + `Arbiter` from `tractor.__init__`.
- `_runtime.py` re-imports from `discovery._registry`
  for backward compat.

Also,
- update all test files to use `.is_registrar`
  (`test_local`, `test_rpc`, `test_spawning`,
  `test_discovery`, `test_multi_program`).
- update "arbiter" -> "registrar" in comments/docstrings
  across `_discovery.py`, `_server.py`, `_transport.py`,
  `_testing/pytest.py`, and examples.
- drop resolved TODOs from `_runtime.py` and `_root.py`.

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-04-02 17:59:13 -04:00
Gud Boi cc42d38284 Mv core mods to `runtime/`, `spawn/`, `discovery/` subpkgs
Restructure the flat `tractor/` top-level private mods
into (more nested) subpackages:

- `runtime/`: `_runtime`, `_portal`, `_rpc`, `_state`,
  `_supervise`
- `spawn/`: `_spawn`, `_entry`, `_forkserver_override`,
  `_mp_fixup_main`
- `discovery/`: `_addr`, `_discovery`, `_multiaddr`

Each subpkg `__init__.py` is kept lazy (no eager
imports) to avoid circular import issues.

Also,
- update all intra-pkg imports across ~35 mods to use
  the new subpkg paths (e.g. `from .runtime._state`
  instead of `from ._state`)

(this patch was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-04-02 17:59:13 -04:00
Gud Boi 52e8fb43ee Tighten UDS addr validation and sockname prefixes
- add `is_valid` and `sockpath.resolve()` asserts in
  `get_rando_addr()` for the `'uds'` case plus an
  explicit `UDSAddress` type annotation.
- rename no-runtime sockname prefixes from
  `'<unknown-actor>'`/`'root'` to
  `'no_runtime_root'`/`'no_runtime_actor'` with a proper
  if/else branch in `UDSAddress.get_random()`.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-13 21:10:52 -04:00
Gud Boi 5ea721683b Use `.aid.uid` to avoid deprecation warns
I started getting annoyed by all the warnings from `pytest` during work
on macos suport in CI, so this replaces all `Actor.uid`/`Channel.uid`
accesses with `.aid.uid` (or `.aid.reprol()` for log msgs) across the
core runtime and IPC subsystems to avoid the noise.

This also provides incentive to start the adjustment to all
`.uid`-holding/tracking internal `dict`-tables/data-structures to
instead use `.msg.types.Aid`. Hopefully that will come a (vibed?) follow
up shortly B)

Deats,
- `._context`: swap all `self._actor.uid`, `self.chan.uid`,
  and `portal.actor.uid` refs to `.aid.uid`; use
  `.aid.reprol()` for log/error formatting.
- `._rpc`: same treatment for `actor.uid`, `chan.uid` in
  log msgs and cancel-scope handling; fix `str(err)` typo
  in `ContextCancelled` log.
- `._runtime`: update `chan.uid` -> `chan.aid.uid` in ctx
  cache lookups, RPC `Start` msg, registration and
  cancel-request handling; improve ctxc log formatting.
- `._spawn`: replace all `subactor.uid` with
  `.aid.uid` for child-proc tracking, IPC peer waiting,
  debug-lock acquisition, and nursery child dict ops.
- `._supervise`: same for `subactor.uid` in cancel and
  portal-wait paths; use `actor.aid.uid` for error dict.
- `._state`: fix `last.uid` -> `last.aid.uid` in
  `current_actor()` error msg.

Also,
- `._chan`: make `Channel.aid` a proper `@property` backed
  by `._aid` so we can add validation/typing later.
- `.log`: use `current_actor().aid.uuid` instead of
  `.uid[1]` for actor-uid log field.
- `.msg.types`: add TODO comment for `Start.aid` field
  conversion.

(this commit msg was generated in some part by [`claude-code`][claude-code-gh])
[claude-code-gh]: https://github.com/anthropics/claude-code
2026-03-13 21:10:52 -04:00
Gud Boi daae196048 Warn if `.ipc._uds.get_peer_pid()` returns null 2026-03-08 19:17:16 -04:00
Gud Boi a7e74acdff Doc `getsockopt()` args (for macOS)
Per the questionable `copilot` review which is detailed for follow up in
https://github.com/goodboy/tractor/issues/418. These constants are
directly linked from the kernel sources fwiw.
2026-03-08 19:17:16 -04:00
Gud Boi 521fb97fe9 Support UDS on macos (for realz)
Though it was a good (vibed) try by @dnks, the previous "fix" was not
actually adding unix socket support but merely sidestepping a crash due
to `get_peer_info()`'s impl never going to work on MacOS (and it was
never intended to).

This patch instead solves the underlying issue by implementing a new
`get_peer_pid()` helper which does in fact retrieve the peer's PID in
a more generic/cross-platform way (:fingers_crossed:); much thanks to
the linked SO answer for this solution!

Impl deats,
- add `get_peer_pid()` and call it from
  `MsgpackUDSStream.get_stream_addrs()` when we detect a non-'linux'
  platform, OW use the original soln: `get_stream_addrs()`.
- add a new case for the `match (peername, sockname)` with a
  `case (str(), str()):` which seems to at least work on macos.
- drop all the `LOCAL_PEERCRED` dynamic import branching since it was
  never needed and was never going to work.
2026-03-08 19:17:16 -04:00