Modular transports via a tractor.ipc subpkg! #24

Open
goodboy wants to merge 53 commits from leslies_extra_appendix into shm_apis

That’s right, we’re finally not just using TCP!

Bo


This originally started as a appendix PR to #17 and was thus named,

Refinements around multiple IPC transport protos (an appendix to #17)

Now this PR will replace #17 since the history is to be fast forwarded onto the structural_dynamics_of_flow branch originally started by @guille (with much thanks!)

The original description was,

A variety of simplifying-refinements and fixes to the core runtime due to issues discovered as part of making our entire test suite work over both TCP and UDS sockets!


PR land plan

I’d like to see this landed before the original upstream branch (#10) which contains the new (and much more experimental) high-perf shared-mem .ipc._ringbug stuff from @guille.

  • that branch should get rebased onto this one eventually and landed after all the linux-specific additions have been worked out in detail.

So our first common commit between this and #10 should be,

  • 9b21615 Break out transport protocol and tcp specifics into their own submodules

From the original history I’ve dropped the following commits that can instead go with #10:

  • bab265b Important RingBuffBytesSender fix on non batched mode!
  • 010874b Catch trio cancellation on RingBuffReceiver bg eof listener task
  • a010ab Add direct read method on EventFD
  • be7fc89 Add direct ctx managers for RB channels
  • 2a9a786 Improve test_ringbuf test
  • be818a7 Switch tractor.ipc.MsgTransport.stream type to trio.abc.Stream
  • ba353bf Better encapsulate RingBuff ctx managment methods

Amendments to tractor.ipc and ._addr vs. original #17

  • Much simplification to new ._addr.Address primitives,

    • use a type alias UnwrappedAddress,
    • drop all the Address type-var inheritance,
    • drop Address.open_stream(), never used.
    • make UDSAddress.unwrap() to a tuple[str, str] of the (._filedir, ._filename) pair, effectively the (Path.parent, Path.name) components of a (new) .sockpath: Path.
    • rename class vars to .proto_key and unwrapped_type.
    • redo namespace as .bindspace to both better reflect the socket.bind() argument significance as well as avoid collision with the linux semantics of “network namespaces” (which i’d like to utilize eventually!).
    • add .def_bindspace attrs to each concrete type.
  • Changes to UDSAddress.bindspace: Path semantic,

    • use a new _state.get_rt_dir() as the default directory-value representing (in the same vein as a net-proto ip-address) the “basis of the bind space”, that is for socket-files which will be created under that directory-node.
      • This is more equivalent to ports available to be bound per routing-proto address such that file-names like ports only have collision for a given address-part.
      • That is, filename (equivalent of port numbers) only can experience a file-name collision when the files are under the same parent directory (in the mount-namespace on linux) for the process.
    • do appropriate type-matching in wrap_address() and .from_addr() to match.
    • use the PID and if available Actor.aid info in the socket file name to indicate that process is the likely .listen()er/server for that socket/file.
    • mk .close_listener() sync.
  • moved the _addr.preferred_transport to _state._def_tpt_proto: TransportProtocolKey = 'tcp'

  • extend ipc._uds for peer-credentials reading from the OS,

    • add a new open_unix_socket_w_passcred() which ensures with sock.setsockopt() that the server side can always read the connecting client’s pid.
    • add a get_peer_info() helper to read that^
    • always return UDSAddresss with the .maybe_pid set from the peer on the raddr for groking/introspection/repr purposes!
  • new deps introduced,

    • bidict for a 2way map ._address_types: [str, Type[Address]].

Changes to surrounding core components

  • add and use a new Actor.aid: Aid:

    • switch Channel.aid/uid to it^
    • make a new optional Aid.pid: int field for getting any peer’s process id much more easily!
    • always render the .uuid field from a new _addr.mk_uuid().
  • add real __repr__ = pformat representations to,

    • Actor,
    • Channel,
    • MsgPackTransport (well ish, it’s a start),
  • move Actor._do_handshake() to Channel method.

  • proper/rigorous SpawnSpec handling in Actor._from_parent() such that we actually raise an explicit internal error on type-mismatches (and not some weird downstream error..).

  • add some new runtime excs,

    • RuntimeFailure for out of our control init issues.
    • ActorFailure now from that for failed spawns.
  • add a new .devx.pformat.pformat_exc() factored from the body of RemoteActorError.pformat() and used for TransportClosed.

  • factor out breakpoint() blocking to a _root.maybe_block_bp(); should be factored to .devx maybe ya?


Full test suite multi-tpt support with --tpt-proto=uds|tcp

I was able to augment the entire test to opt into multi-transport-protocols via a new accumulative --tpt-proto flag which by default is always just stored as ['tcp'] as before.

Now any opting in test/fixture can pass it to the enable_transports: list[str] argument exposed by open_root_actor() to test against that backend; currently we haven’t tried to test multiple protos together yet though!

brief test harness updates summary

new conftest fixture content,

  • a new tpt_protos: list[str] rendered from CLI input args.
  • tpt_proto: str which just takes the first of the above for now.
  • reg_addr now correctly generates a random root-host-singleton-registry value per test session/run.
  • masked draft-idea for a set_script_runtime_args().

test suite tweaks for UDS,

  • test_advanced_faults.py adjustments for receiving non-KBIs since the socket “closure”/“breakage” will be relayed by the OS immediately.
    • also matching against the new TransportClosed.src_exc field for the raised underlying trio exceptions.

a new test_root_runtime.py suite with,

  • test_only_one_root_actor to verify we error on opening a 2nd root from an actor tree.
  • test_implicit_root_via_first_nursery which just verifies implicit root/runtime booting from the first opened .open_nursery().

New --dev deps introduced

  • psutil for proc introspection within the discovery suite.

Follow up refinements I’d like to maybe see before #17 lands,

As in after this is merged into #17, I’d like to see these additional changes considered,


GH issues landing #17 will maybe close!

### That's right, we're finally not just using TCP! Bo --- This originally started as a appendix PR to #17 and was thus named, > Refinements around multiple IPC transport protos (an appendix to #17) Now this PR will replace #17 since the history is to be fast forwarded onto the `structural_dynamics_of_flow` branch originally started by @guille (with much thanks!) The original description was, >A variety of simplifying-refinements and fixes to the core > runtime due to issues discovered as part of making our entire test > suite work over both TCP and UDS sockets! --- PR land plan ------------ I'd like to see this landed before the original upstream branch (#10) which contains the new (and much more experimental) high-perf shared-mem `.ipc._ringbug` stuff from @guille. - [ ] that branch should get rebased onto this one eventually and landed after all the linux-specific additions have been worked out in detail. So our first common commit between this and #10 should be, - 9b21615 Break out transport protocol and tcp specifics into their own submodules From the original history I've dropped the following commits that can instead go with #10: - [x] bab265b Important RingBuffBytesSender fix on non batched mode! - [x] 010874b Catch trio cancellation on RingBuffReceiver bg eof listener task - [x] a010ab Add direct read method on EventFD - [x] be7fc89 Add direct ctx managers for RB channels - [x] 2a9a786 Improve test_ringbuf test - [x] be818a7 Switch `tractor.ipc.MsgTransport.stream` type to `trio.abc.Stream` - [x] ba353bf Better encapsulate RingBuff ctx managment methods --- Amendments to `tractor.ipc` and `._addr` vs. original #17 --------------------------------------------------------- - Much simplification to new `._addr.Address` primitives, - use a type alias `UnwrappedAddress`, - drop all the `Address` type-var inheritance, - drop `Address.open_stream()`, never used. - make `UDSAddress.unwrap()` to a `tuple[str, str]` of the `(._filedir`, `._filename)` pair, effectively the `(Path.parent, Path.name)` components of a (new) `.sockpath: Path`. - rename class vars to `.proto_key` and `unwrapped_type`. - redo `namespace` as `.bindspace` to both better reflect the `socket.bind()` argument significance as well as avoid collision with the linux semantics of "network namespaces" (which i'd like to utilize eventually!). - add `.def_bindspace` attrs to each concrete type. - Changes to `UDSAddress.bindspace: Path` semantic, - use a new `_state.get_rt_dir()` as the default directory-value representing (in the same vein as a net-proto ip-address) the "basis of the bind space", that is for socket-files which will be created *under* that directory-node. * This is more equivalent to ports available to be bound per routing-proto address such that file-names like ports only have collision for a given address-part. * That is, filename (equivalent of port numbers) only can experience a file-name collision when the files are under the same parent directory (in the mount-namespace on linux) for the process. - do appropriate type-matching in `wrap_address()` and `.from_addr()` to match. - use the PID and if available `Actor.aid` info in the socket file name to indicate that process is the likely `.listen()`er/server for that socket/file. - mk `.close_listener()` sync. - moved the `_addr.preferred_transport` to `_state._def_tpt_proto: TransportProtocolKey = 'tcp'` - extend `ipc._uds` for peer-credentials reading from the OS, - add a new `open_unix_socket_w_passcred()` which ensures with `sock.setsockopt()` that the server side can always read the connecting client's pid. - add a `get_peer_info()` helper to read that^ - always return `UDSAddress`s with the `.maybe_pid` set from the peer on the `raddr` for groking/introspection/repr purposes! - new deps introduced, - `bidict` for a 2way map `._address_types: [str, Type[Address]]`. --- Changes to surrounding core components -------------------------------------- - add and use a new `Actor.aid: Aid`: - switch `Channel.aid/uid` to it^ - make a new optional `Aid.pid: int` field for getting any peer's process id much more easily! - always render the `.uuid` field from a new `_addr.mk_uuid()`. - add real `__repr__ = pformat` representations to, - `Actor`, - `Channel`, - `MsgPackTransport` (well ish, it's a start), - move `Actor._do_handshake()` to `Channel` method. - proper/rigorous `SpawnSpec` handling in `Actor._from_parent()` such that we actually raise an explicit internal error on type-mismatches (and not some weird downstream error..). - add some new runtime excs, - `RuntimeFailure` for out of our control init issues. - `ActorFailure` now from that for failed spawns. - add a new `.devx.pformat.pformat_exc()` factored from the body of `RemoteActorError.pformat()` and used for `TransportClosed`. - factor out `breakpoint()` blocking to a `_root.maybe_block_bp()`; should be factored to `.devx` maybe ya? --- Full test suite multi-tpt support with `--tpt-proto=uds|tcp` ------------------------------------------------------------ I was able to augment the entire test to opt into multi-transport-protocols via a new accumulative `--tpt-proto` flag which by default is always just stored as `['tcp']` as before. Now any opting in test/fixture can pass it to the `enable_transports: list[str]` argument exposed by `open_root_actor()` to test against that backend; currently we **haven't tried to test multiple protos together yet though!** #### brief test harness updates summary new `conftest` fixture content, - a new `tpt_protos: list[str]` rendered from CLI input args. - `tpt_proto: str` which just takes the first of the above for now. - `reg_addr` now correctly generates a random root-host-singleton-registry value per test session/run. - masked draft-idea for a `set_script_runtime_args()`. test suite tweaks for UDS, - `test_advanced_faults.py` adjustments for receiving non-KBIs since the socket "closure"/"breakage" will be relayed by the OS immediately. - also matching against the new `TransportClosed.src_exc` field for the raised underlying `trio` exceptions. a new `test_root_runtime.py` suite with, - `test_only_one_root_actor` to verify we error on opening a 2nd root from an actor tree. - `test_implicit_root_via_first_nursery` which just verifies implicit root/runtime booting from the first opened `.open_nursery()`. New `--dev` deps introduced - `psutil` for proc introspection within the discovery suite. --- Follow up refinements I'd like to maybe see before #17 lands, ------------- As in after this is merged into #17, I'd like to see these additional changes considered, - [x] if we can move each concrete protocol's `Address` into its `.ipc/_tcp/uds` module. - as of - [ ] consider where we want the final destination of `._addr` since there's going to be extensions necessary as we start solving the multi-addresss idea and composition of tpt protos from addresses as kinda requested in https://github.com/goodboy/tractor/pull/367 with further follow up as we research the libp2p spec-docs: * https://github.com/libp2p/specs/blob/master/addressing/README.md#addressing-in-libp2p-- * https://github.com/multiformats/multiaddr - [ ] factor the transport serving into a new `TransportServer` (singleton?-)wrapper which keeps track of all connections (`SocketStream`s wrapped by `MsgTransport`) per protocol and peer! - [ ] as part of this removing the `.open/close_listener()` meths from per-tpt addrs-types and instead offering the api at module level for each proto? --- GH issues landing #17 will maybe close! ------------- - [ ] https://github.com/goodboy/tractor/issues/109 - [ ] https://github.com/goodboy/tractor/issues/136
goodboy added 35 commits 2025-04-06 20:18:12 +00:00
9de192390a Rework/simplify transport addressing
A few things that can fundamentally change,

- UDS addresses now always encapsulate the local and remote pid such
  that it denotes each side's process much like a TCP *port*.
  |_ `.__init__()` takes a new `maybe_pid: int`.
  |_ this required changes to the `.ipc._uds` backend which will come in
     an subsequent commit!
  |_ `UDSAddress.address_type` becomes a `tuple[str, int]` just like the
      TCP case.
  |_ adjust `wrap_address()` to match.
- use a new `_state.get_rt_dir() -> Path` as the default location for
  UDS socket file: now under `XDG_RUNTIME_DIR'/tractor/` subdir by
  default.
- re-implement `USDAddress.get_random()` to use both the local
  `Actor.uid` (if available) and at least the pid for its socket file
  name.

Removals,
- drop the loop generated `_default_addrs`, simplify to just
  `_default_lo_addrs` for per-transport default registry addresses.
  |_ change to `_address_types: dict[str, Type[Address]]` instead of
     separate types `list`.
  |_ adjust `is_wrapped_addr()` to just check `in _addr_types.values()`.
- comment out `Address.open_stream()` it's unused and i think the wrong
  place for this API.

Renames,
- from `AddressTypes` -> `UnwrappedAddress`, since it's a simple type
  union and all this type set is, is the simple python data-structures
  we encode to for the wire.
  |_ see note about possibly implementing the `.[un]wrap()` stuff as
     `msgspec` codec `enc/dec_hook()`s instead!

Additions,
- add a `mk_uuid()` to be used throughout the runtime including for
  generating the `Aid.uuid` part.
- tons of notes around follow up refinements!
2c11d1d44a Implement peer-info tracking for UDS streams
Such that any UDS socket pair is represented (and with the recent
updates to) a `USDAddress` via a similar pair-`tuple[str, int]` as TCP
sockets, a pair of the `.filepath: Path` & the peer proc's `.pid: int`
which we read from the underlying `socket.socket` using
`.set/getsockopt()` calls

Impl deats,
- using the Linux specific APIs, we add a `get_peer_info()` which reads
  the `(pid, uid, gid)` using the `SOL_SOCKET` and `SOL_PEECRED` opts to
  `sock.getsockopt()`.
  |_ this presumes the client has been correspondingly configured to
     deliver the creds via a `sock.setsockopt(SOL_SOCKET, SO_PASSCRED,
     1)` call - this required us to override `trio.open_unix_socket()`.
- override `trio.open_unix_socket()` as per the above bullet to ensure
  connecting peers always transmit "credentials" options info to the
  listener.
- update `.get_stream_addrs()` to always call `get_peer_info()` and
  extract the peer's pid for the `raddr` and use `os.getpid()` for
  `laddr` (obvi).
  |_ as part of the new impl also `log.info()` the creds-info deats and
    socket-file path.
  |_ handle the oddity where it depends which of `.getpeername()` or
    `.getsockname()` will return the file-path; i think it's to do with
    who is client vs. server?

Related refinements,
- set `.layer_key: int = 4` for the "transport layer" ;)
- tweak some typing and multi-line unpacking in `.ipc/_tcp`.
23acd0f4cb Adjust imports to use new `UnwrappedAddress`
For those mods where it's just a type-alias (name) import change.
6a5ccc2425 Allocate bind-addrs in subactors
Previously whenever an `ActorNursery.start_actor()` call did not receive
a `bind_addrs` arg we would allocate the default `(localhost, 0)` pairs
in the parent, for UDS this obviously won't work nor is it ideal bc it's
nicer to have the actor to be a socket server (who calls
`Address.open_listener()`) define the socket-file-name containing their
unique ID info such as pid, actor-uuid etc.

As such this moves "random" generation of server addresses to the
child-side of a subactor's spawn-sequence when it's sin-`bind_addrs`;
i.e. we do the allocation of the `Address.get_random()` addrs inside
`._runtime.async_main()` instead of `Portal.start_actor()` and **only
when** `accept_addrs`/`bind_addrs` was **not provided by the spawning
parent**.

Further this patch get's way more rigorous about the `SpawnSpec`
processing in the child inside `Actor._from_parent()` such that we
handle any invalid msgs **very loudly and pedantically!**

Impl deats,
- do the "random addr generation" in an explicit `for` loop (instead of
  prior comprehension) to allow for more detailed typing of the layered
  calls to the new `._addr` mod.
- use a `match:/case:` for process any invalid `SpawnSpec` payload case
  where we can instead receive a `MsgTypeError` from the `chan.recv()`
  call in `Actor._from_parent()` to raise it immediately instead of
  triggering downstream type-errors XD
  |_ as per the big `#TODO` we prolly want to take from other callers
     of `Channel.recv()` (like in the `._rpc.process_messages()` loop).
  |_ always raise `InternalError` on non-match/fall-through case!
  |_ add a note about not being able to use `breakpoint()` in this
     section due to causality of `SpawnSpec._runtime_vars` not having
     been processed yet..
  |_ always return a third element from `._from_rent()` eventually to be
     the `preferred_transports: list[str]` from the spawning rent.
- use new `._addr.mk_uuid()` and pass to new `Actor.__init__(uuid: str)`
  for all actor creation (including in all the mods tweaked here).
- Move to new type-alias-name `UnwrappedAddress` throughout.
e904af679b Add a big boi `Channel.pformat()/__repr__()`
Much like how `Context` has been implemented, try to give tons of high
level details on all the lower level encapsulated primitives, namely the
`.msgstream/.transport` and any useful runtime state.

B)

Impl deats,
- adjust `.from_addr()` to only call `._addr.wrap_address()` when we
  detect `addr` is unwrapped.
- add another `log.runtime()` using the new `.__repr__()` in
  `Channel.from_addr()`.
- change to `UnwrappedAddress` as in prior commits.
8040ae6994 Adjust lowlevel-tb hiding logic for `MsgStream`
Such that whenev the `self._ctx.chan._exc is trans_err` we suppress.
I.e. when the `Channel._exc: Exception|None` error **is the same as**
set by the `._rpc.process_messages()` loop (that is, set to the
underlying transport layer error), we suppress the lowlevel tb,
otherwise we deliver the full tb since likely something at the lowlevel
that we aren't detecting changed/signalled/is-relevant!
a28659c3cd Handle broken-pipes from `MsgpackTransport.send()`
Much like we already do in the `._iter_packets()` async-generator which
delivers to `.recv()` and `async for`, handle the `''[Errno 32] Broken
pipe'` case that can show up with unix-domain-socket usage.

Seems like the cause is due to how fast the socket can be torn down
during a registry addr channel ping where,
- the sending side can break the connection faster then the pong side
  can prep its handshake msg,
- the pong side tries to send it's handshake pkt via
  `.SocketStream.send_all()` after the breakage and then raises
  `trio.BrokenResourceError`.
89993a4e3a Even more `tractor._addr.Address` simplifying
Namely reducing the duplication of class-fields and `TypeVar`s used
for parametrizing the `Address` protocol type,
- drop all of the `TypeVar` types and just stick with all concrete addrs
  types inheriting from `Address` only.
- rename `Address.name_key` -> `.proto_key`.
- rename `Address.address_type` -> `.unwrapped_type`
- rename `.namespace` -> `.bindspace` to better reflect that this "part"
  of the address represents the possible "space for binding endpoints".
 |_ also linux already uses "namespace" to mean the `netns` and i'd
   prefer to stick with their semantics for that.
- add `TCPAddress/UDSAddress.def_bindspace` values.
- drop commented `.open_stream()` method; never used.
- simplify `UnwrappedAdress` to just a `tuple` of union types.
- add logging to `USDAddress.open_listener()` for now.
- adjust `tractor.ipc/_uds/tcp` transport to use new addr field names.
46e775ce6d Add a `MsgpackTransport.pformat()`
And map `.__repr__/__str__` to it. Also adjust to new
`Address.proto_key` and add a #TODO for a `.get_peers()`.
1cb2337c7c Add an `Actor.pformat()`
And map `.__repr__/__str__` to it and add various new fields to fill it
out,
- drop `self.uid` as var and instead add `Actor._aid: Aid` and proxy to
  it for the various `.name/.uid/.pid` properties as well as a new
  `.aid` field.
 |_ the `Aid.pid` addition is also included.

Other improvements,
- flip to a sync call to `Address.close_listener()`.
- track the `async_main()` parent task as `Actor._task`.
- add exception logging around failure to bind due to already-in-use
  when calling `add.open_listener()` in `._stream_forever()`; sometimes
  the error might be overridden by something else during the
  runtime-failure unwind..
0c60914cc4 Factor `breakpoint()` blocking into `@acm`
Call it `maybe_block_bp()` can wrap the `open_root_actor()` body with
it. Main reason is to guarantee we can bp inside actor runtime bootup as
needed when debugging internals! Prolly should factor this to another
module tho?

ALSO, ensure we RTE on recurrent entries to `open_root_actor()` from
within an existing tree! There was actually `test_spawning` test somehow
getting away with this!? Should never be possible or allowed!
dc68ea4118 Start protoyping multi-transport testing
Such that we can run (opting-in) tests on both TCP and UDS backends and
ensure the `reg_addr` fixture and various timeouts are adjusted
accordingly.

Impl deats,
- add a new `tpc_proto` CLI option and fixture to allow choosing which
  "transport protocol" will be used in the test suites (either globally
  or contextually).
- rm `_reg_addr` instead opting for a `_rando_port` which will only be
  used for `reg_addr`s which are net-tpt-protos.
- rejig `reg_addr` fixture to set a ideally session-unique `testrun_reg_addr`
  based on the `tpt_proto` setting making appropriate calls to `._addr`
  APIs as needed.
- refine `daemon` fixture a bit with typing, `tpt_proto` timings, and
  stderr capture.
- in `test_discovery` do a ton of type-annots, add `debug_mode` fixture
  opt ins, augment `spawn_and_check_registry()` with `psutil.Process`
  passing for introspection (when things go wrong..).
9f837161ea More `._addr` boxing refinements
The more I think about it, it seems @guille's orig approach of
unwrapping UDS socket-file addresses to strings (or `Path`) is making
the most sense. I had originally thought that pairing it with the
listening side's pid would add clarity (and it definitely does for
introspection/debug/logging) but since we don't end up passing that pid
to the eventual `.connect()` call on the client side, it doesn't make
much sense to wrap it for the wire just to discard.. Further, the
`tuple[str, int]` makes `wrap_address()` break for TCP since it will
always match on uds first.

So, on that note this patch refines a few things in prep for going back
to that original `UnwrappedAddress` as `str` type though longer run
i think the more "builtin approach" would be to add `msgspec` codec
hooks for these types to avoid all the `.wrap()`/`.unwrap()` calls
throughout the runtime.

Down-low deats,
- add `wrap_address()` doc string, detailed (todo) comments and handle
  the `[None, None]` case that can come directly from
  `._state._runtime_vars['_root_mailbox']`.
- buncha adjustments to `UDSAddress`,
  - add a `filedir`, chng `filepath` -> `filename` and mk `maybe_pid` optional.
  - the intent `filedir` is act as the equivalent of the host part in a network proto's
    socket address and when it's null use the `.def_bindspace = get_rt_dir()`.
  - always ensure the `filedir / filename` is an absolute path and
    expose it as a new `.sockpath: Path` property.
  - mk `.is_valid` actually verify the `.sockpath` is in the valid
    `.bindspace: namely just checking it's in the expected dir.
  - add pedantic `match:`ing to `.from_addr()` such that we error on
    unexpected `type(addr)` inputs and otherwise parse any `sockpath:
    Path` inputs using a new `unwrap_sockpath()` which simply splits an
    abs file path to dir, file-name parts.
  - `.unwrap()` now just `str`-ifies the `.sockpath: Path`
  - adjust `.open/close_listener()` to use `.sockpath`.
35acc5a3d5 UDS: translate file dne to connection-error
For the case where there's clearly no socket file created/bound
obviously the `trio.socket.connect()` call will raise
`FileNotFoundError`, so just translate this to
a builtin-`ConnectionError` at the transport layer so we can report the
guilty `UDSAddress`.
dd3e918cfe Mv `Actor._do_handshake()` to `Channel`, add `.aid`
Finally.. i've been meaning todo this for ages since the
actor-id-swap-as-handshake is better layered as part of the IPC msg-ing
machinery and then let's us encapsulate the connection-time-assignment
of a remote peer's `Aid` as a new `Channel.aid: Aid`. For now we
continue to offer the `.uid: tuple[str, str]` attr (by delegating to the
`.uid` field) since there's still a few things relying on it in the
runtime and ctx layers

Nice bonuses from this,
- it's very easy to get the peer's `Aid.pid: int` from anywhere in an
  IPC ctx by just reading it from the chan.
- we aren't saving more then the wire struct-msg received.

Also add deprecation warnings around usage to get us moving on porting
the rest of consuming runtime code to the new attr!
eeed5fd7f1 Mv to `Channel._do_handshake()` in `open_portal()`
As per the method migration in the last commit. Also adjust all `.uid`
usage to the new `.aid`.
c2705cce68 Change some low-hanging `.uid`s to `.aid`
Throughout `_context` and `_spawn` where it causes no big disruption.
Still lots to work out for things like how to pass `--uid
<tuple-as-str>` to spawned subactors and whether we want a diff name for
the minimum `tuple` required to distinguish a subactor pre-process-ID
allocation by the OS.
a528d45a30 Some more log message tweaks
- aggregate the `MsgStream.aclose()` "reader tasks" stats content into a
  common `message: str` before emit.
- tweak an `_rpc.process_messages()` emit per new `Channel.__repr__()`.
7d537e60cc Repair weird spawn test, start `test_root_runtime`
There was a very strange legacy test
`test_spawning.test_local_arbiter_subactor_global_state` which was
causing unforseen hangs/errors on the UDS tpt and looking deeper this
test was already doing root-actor things that should never have been
valid XD

So rework that test to properly demonstrate something of value
(i guess..) and add a new suite which start more rigorously auditing our
`open_root_actor()` permitted usage.

For the old test,
- since the main point of this test seemed to be the ability to invoke
  the same function in both the parent and child actor (using the very
  legacy `ActorNursery.run_in_actor()`.. due to be deprecated) rename it
  to `test_run_in_actor_same_func_in_child`,
- don't re-enter `.open_root_actor()` since that's invalid usage (tested
  in new suite see below),
- adjust some `spawn()` arg/var naming and ensure we only return in the
  child.

For the new suite add tests for,
- ensuring the implicit `open_root_actor()` call under `open_nursery()`.
- double open of `open_root_actor()` from within the same process tree
  both from a root and sub.

Intro some new `_exceptions` used in the new suite,
- a top level `RuntimeFailure` for generically expressing faults not of
  our own doing that prevent successful operation; this is what we now
  (changed in this commit) raise on attempts to open a 2nd root.
- mk `ActorFailure` derive from the former; it's already used from
  `._spawn` when subprocs fail to boot.
69fbe49d37 s/`._addr.preferred_transport`/`_state._def_tpt_proto`
Such that the "global-ish" setting (actor-local) is managed with the
others per actor-process and type it as a `Literal['tcp', 'uds']` of the
currently support protocol keys.

Here obvi `_tpt` is some kinda shorthand for "transport" and `_proto` is
for "protocol" Bp

Change imports and refs in all dependent modules.

Oh right, and disable UDS in `wrap_address()` for the moment while
i figure out how to avoid the unwrapped type collision..
2d6b3922a6 Unwrap `UDSAddress` as `tuple[str, str]`, i.e. sin pid
Since in hindsight the real analog of a net-proto's "bindspace"
(normally its routing layer's addresses-port-set) is more akin to the
"location in the file-system" for a UDS socket file (aka the file's
parent directory) determines whether or not the "port" (aka it's
file-name) collides with any other.

So the `._filedir: Path` is like the allocated "address" and,
the `._filename: Path|str` is basically the "port",

at least in my mind.. Bp

Thinking about fs dirs like a "host address" means you can get
essentially the same benefits/behaviour of say an (ip)
addresses-port-space but using the (current process-namespace's)
filesys-tree. Note that for UDS sockets in particular the
network-namespace is what would normally isolate so called "abstract
sockets" (i.e. UDS sockets that do NOT use file-paths by setting `struct
sockaddr_un.sun_path = 'abstract', see `man unix`); using directories is
even easier and definitely more explicit/readable/immediately-obvious as
a human-user.

As such this reworks all the necessary `UDSAddress` meths,
- `.unwrap()` now returns a `tuple(str(._filedir, str(._filename))`,
- `wrap_address()` now matches UDS on a 2nd tuple `str()` element,
- `.get_root()` no longer passes `maybe_pid`.

AND adjusts `MsgpackUDSStream` to,
- use the new `unwrap_sockpath()` on the `socket.get[sock/peer]name()`
  output before passing directly as `UDSAddress.__init__(filedir, filename)`
  instead of via `.from_addr()`.
- also pass `maybe_pid`s to init since no longer included in the
  unwrapped-type form.
0a1ac80fee Support multiple IPC transports in test harness!
Via a new accumulative `--tpt-proto` arg you can select which
`tpt_protos: list[str]`-fixture protocol keys will be delivered to
opting in tests!

B)

Also includes,
- CLI quote handling/stripping.
- default of 'tcp'.
- only support one selection per session at the moment (until we figure
  out how we want to support multiples, either simultaneously or
  sequentially).
- draft a (masked) dynamic-`metafunc` parametrization in the
  `pytest_generate_tests()` hook.
- first proven and working use in the `test_advanced_faults`-suite (and
  thus its underlying
  `examples/advanced_faults/ipc_failure_during_stream.py` script)!
 |_ actually needed this to prove that the suite only has 2 failures on
    'uds' seemingly due to low-level `trio` error semantics translation
    differences to do with with calling `socket.close()`..

On a very nearly related topic,
- draft an (also commented out) `set_script_runtime_args()` fixture idea
  for a std way of `partial`-ling in runtime args to `examples/`
  scripts-as-modules defining a `main()` which would proxy to
  `tractor.open_nursery()`.
46ffc80d89 Handle unconsidered fault-edge cases for UDS
In `tests/test_advanced_faults.py` that is.
Since instead of zero-responses like we'd expect from a network-socket
we actually can get a few differences from the OS when "everything IPC
is known"

XD

Namely it's about underlying `trio` exceptions versus how we wrap them
and how we expect to box them. A `TransportClosed` boxing improvement
is coming in follow up btw to make this all work!

B)
d59954ade3 Improve `TransportClosed.__repr__()`, add `src_exc`
By borrowing from the implementation of `RemoteActorError.pformat()`
which is now factored into a new `.devx.pformat_exc()` and re-used for
both error types while maintaining the same func-sig. Obviously delegate
`RemoteActorError.pformat()` to the new helper accordingly and keeping
the prior `body` generation from `.devx.pformat_boxed_tb()` as before.

The new helper allows for,
- passing any of a `header|message|body: str` which are all combined in
  that order in the final output.
- getting the `exc.message` as the default `message` part.
- generating an objecty-looking "type-name" header to be rendered by
  default when `header` is not overridden.
- "first-line-of `message`" processing which we split-off and then
  re-inject as a `f'<{type(exc).__name__}( {first} )>'` top line header.
- an optional `tail: str = '>'` to "close the object"-look only added
  when `with_type_header: bool = True`.

Adjustments to `TransportClosed` around this include,
- replacing the init `cause` arg for a `src_exc` which is now always
  assigned to a same named instance var.
- displaying that new `.src_exc` in the `body: str` arg to the
  `.devx.pformat.pformat_exc()` call so you can always see the
  underlying (normally `trio`) source error.
- just make it inherit from `Exception` not `trio.BrokenResourceError`
  to avoid handlers catching `TransportClosed` as the former
  particularly in testing when we want to sometimes to distinguish them.
goodboy changed title from Refinements around multiple IPC transport protos, and appendix for #17 to Refinements around multiple IPC transport protos (an appendix to #17) 2025-04-06 20:33:07 +00:00
goodboy reviewed 2025-04-06 22:24:41 +00:00
tractor/_addr.py Outdated
@ -48,2 +97,4 @@
# Lke, what use does this have besides a noop and if it's not
# valid why aren't we erroring on creation/use?
@property
def is_valid(self) -> bool:
Poster
Owner

i’m not really sure how useful this predicate is tbh.

i'm not really sure how useful this predicate is tbh.
goodboy reviewed 2025-04-06 22:28:22 +00:00
tractor/_addr.py Outdated
@ -157,3 +267,1 @@
async def open_listener(self, **kwargs) -> trio.SocketListener:
listeners = await trio.open_tcp_listeners(
async def open_listener(
Poster
Owner

i think maybe we should consider moving this to MsgpackTransport as well?

If it provides an interface around SocketStream-like instances (for each peer pair) i think it makes more sense to maybe do something like,

await MsgpackUDSStream.open_listener(addr=UDSAddress.get_random()) instead of pulling weird Address()-instance-internal state?

i think maybe we should consider moving this to `MsgpackTransport` as well? If it provides an interface around `SocketStream`-like instances (for each peer pair) i think it makes more sense to maybe do something like, `await MsgpackUDSStream.open_listener(addr=UDSAddress.get_random())` instead of pulling weird `Address()`-instance-internal state?
goodboy reviewed 2025-04-06 22:34:05 +00:00
tractor/_addr.py Outdated
@ -176,3 +285,1 @@
trio.SocketStream,
trio.SocketListener
]):
def unwrap_sockpath(
Poster
Owner

just a helper for any UDSAddress..sockpath: Path style partitioning to dir, filename.

just a helper for any `UDSAddress..sockpath: Path` style partitioning to dir, filename.
goodboy reviewed 2025-04-06 22:40:12 +00:00
tractor/_root.py Outdated
@ -67,3 +147,3 @@
# defaults are above
arbiter_addr: tuple[AddressTypes]|None = None,
arbiter_addr: tuple[UnwrappedAddress]|None = None,
Poster
Owner

REALLY need to drop this!

XD

i’m hoping to get to a follow up registry-sys rework this week in a follow up PR!

REALLY need to drop this! XD i'm hoping to get to a follow up registry-sys rework this week in a follow up PR!
goodboy reviewed 2025-04-06 22:41:04 +00:00
tractor/_root.py Outdated
@ -61,0 +72,4 @@
# save an indent level?
#
@acm
async def maybe_block_bp(
Poster
Owner

NOTE most of the diff in this file is just the higher indent of open_root_actor() due to factoring this bp blocking stuff out!

NOTE most of the diff in this file is just the higher indent of `open_root_actor()` due to factoring this bp blocking stuff out!
goodboy reviewed 2025-04-06 22:42:02 +00:00
@ -202,3 +202,1 @@
self.uid = (
name,
uid or str(uuid.uuid4())
self._aid = msgtypes.Aid(
Poster
Owner

Notice we always stash the pid now!

Notice we always stash the pid now!
goodboy reviewed 2025-04-06 22:49:38 +00:00
@ -1199,0 +1342,4 @@
if (
'[Errno 98] Address already in use'
in
oserr.args#[0]
Poster
Owner

drop that #[0] bit..

drop that `#[0]` bit..
goodboy reviewed 2025-04-06 22:51:03 +00:00
@ -146,0 +152,4 @@
_rtdir: Path = Path(os.environ['XDG_RUNTIME_DIR'])
def get_rt_dir(
Poster
Owner

used for our UDSAddress.def_bindspace: path

used for our `UDSAddress.def_bindspace: path`
goodboy requested review from guille 2025-04-06 23:01:51 +00:00
guille approved these changes 2025-04-07 01:44:19 +00:00
goodboy force-pushed leslies_extra_appendix from 44fd1ecc41 to 8fd7d1cec4 2025-04-07 02:33:15 +00:00 Compare
goodboy changed title from Refinements around multiple IPC transport protos (an appendix to #17) to Modular transports via a `tractor.ipc` subpkg! 2025-04-07 02:58:56 +00:00
goodboy changed title from Modular transports via a `tractor.ipc` subpkg! to Modular transports via a `tractor.ipc` subpkg! 2025-04-07 03:00:41 +00:00
goodboy changed target branch from structural_dynamics_of_flow to shm_apis 2025-04-07 03:00:42 +00:00
Poster
Owner

Wow it seems i can’t update the pull_from branch here!?

that’s like nuts..

I guess if it can’t be done i’m going to have to open a new PR.. or just land this branch i guess..

Wow it seems i can't update the `pull_from` branch here!? that's like nuts.. I guess if it can't be done i'm going to have to open a new PR.. or just land this branch i guess..
goodboy added 1 commit 2025-04-08 00:41:50 +00:00
800c99ac41 Move concrete `Address`es to each tpt module
That is moving from `._addr`,
- `TCPAddress` to `.ipc._tcp`
- `UDSAddress` to `.ipc._uds`

Obviously this requires adjusting a buncha stuff in `._addr` to avoid
import cycles (the original reason the module was not also included in
the new `.ipc` subpkg) including,

- avoiding "unnecessary" imports of `[Unwrapped]Address` in various modules.
  * since `Address` is a protocol and the main point is that it **does
    not need to be inherited** per
    (https://typing.python.org/en/latest/spec/protocol.html#terminology)
    thus I removed the need for it in both transport submods.
  * and `UnwrappedAddress` is a type alias for tuples.. so we don't
    really always need to be importing it since it also kinda obfuscates
    what the underlying pairs are.
- not exporting everything in submods at the `.ipc` top level and
  importing from specific submods by default.
- only importing various types under a `if typing.TYPE_CHECKING:` guard
  as needed.
goodboy force-pushed leslies_extra_appendix from 800c99ac41 to c9e9a3949f 2025-04-08 14:10:12 +00:00 Compare
goodboy added 1 commit 2025-04-11 03:18:50 +00:00
c208bcbb1b Factor actor-embedded IPC-tpt-server to `ipc` subsys
Primarily moving the `Actor._serve_forever()`-task-as-method and
supporting actor-instance attributes to a new `.ipo._server` sub-mod
which now encapsulates,
- the coupling various `trio.Nursery`s (and their independent lifetime mgmt)
  to different `trio.serve_listener()`s tasks and `SocketStream`
  handler scopes.
- `Address` and `SocketListener` mgmt and tracking through the idea of
  an "IPC endpoint": each "bound-and-active instance" of a served-listener
  for some (varied transport protocol's socket) address.
- start and shutdown of the entire server's lifetime via an `@acm`.
- delegation of starting/stopping tpt-protocol-specific `trio.abc.Listener`s
  to the corresponding `.ipc._<proto_key>` sub-module (newly defined
  mod-top-level instead of `Address` method) `start/close_listener()`
  funcs.

Impl details of the `.ipc._server` sub-sys,
- add new `IPCServer`, allocated with `open_ipc_server()`, and which
  encapsulates starting multiple-transport-proto-`trio.abc.Listener`s
  from an input set of `._addr.Address`s using,
  |_`IPCServer.listen_on()` which internally spawns tasks that delegate to a new
    `_serve_ipc_eps()`, a rework of what was (effectively)
    `Actor._serve_forever()` and which now,
    * allocates a new `IPCEndpoint`-struct (see below) for each
      address-listener pair alongside the specified
      listener-serving/stream-handling `trio.Nursery`s provided by the
      caller.
    * starts and stops each transport (socket's) listener by calling
      `IPCEndpoint.start/close_listener()` which in turn delegates to
      the underlying `inspect.getmodule(IPCEndpoint.addr)` backend tpt
      module's equivalent impl.
    * tracks all created endpoints in a `._endpoints: list[IPCEndpoint]`
      which is further exposed through public properties for
      introspection of served transport-protocols and their addresses.
  |_`IPCServer._[parent/stream_handler]_tn: Nursery`s which are either
     allocated (in which case, as the same instance) or provided by the
     caller of `open_ipc_server()` such that the same nursery-cancel-scope
     controls offered by `trio.serve_listeners(handler_nursery=)` are
     offered where the `._parent_tn` is used to spawn `_serve_ipc_eps()`
     tasks, and `._stream_handler_tn` is passed verbatim as `handler_nursery`.
- a new `IPCEndpoint`-struct (as mentioned) which wraps each
  transport-proto's address + listener + allocated-supervising-nursery
  to encapsulate the "lifetime of a server IPC endpoint" such that
  eventually we can track and managed per-protocol/address/`.listen_on()`-call
  scoped starts/stops/restarts for the purposes of filtering/banning
  peer traffic.
  |_ also included is an unused `.peer_tpts` table which we can
    hopefully use to replace `Actor._peers` in a `Channel`-tracking
    transport-proto-aware way!

Surrounding changes to `.ipc.*` primitives to match,
- make `[TCP|UDS]Address` types `msgspec.Struct(frozen=True)` and thus
  drop any-and-all `addr._host =` style mutation throughout.
  |_ as such also drop their `.__init__()` and `.__eq__()` meths.
  |_ UDS tweaks to field names and thus `.__repr__()`.
- move `[TCP|UDS]Address.[start/close]_listener()` meths to be mod-level
  equiv `start|close_listener()` funcs.
- just hard code the `.ipc._types._key_to_transport/._addr_to_transport`
  table entries instead of all the prior fancy dynamic class property
  reading stuff (remember, "explicit is better then implicit").

Modified in `._runtime.Actor` internals,
- drop the `._serve_forever()` and `.cancel_server()`, methods and
  `._server_down` waiting logic from `.cancel_soon()`
- add `.[_]ipc_server` which is opened just after the `._service_n` and
  delegate to it for any equivalent publicly exposed instance
  attributes/properties.
Poster
Owner

This is now replaced by an equiv PR from the branch from #17 onto shm_apis,

https://github.com/goodboy/tractor/pull/375

Once i move desired content from the descr here to there i will be closing this one.

This is now replaced by an equiv PR from the branch from #17 onto `shm_apis`, https://github.com/goodboy/tractor/pull/375 Once i move desired content from the descr here to there i will be closing this one.
This pull request can be merged automatically.
You are not authorized to merge this pull request.
You can also view command line instructions.

Step 1:

From your project repository, check out a new branch and test the changes.
git checkout -b leslies_extra_appendix shm_apis
git pull origin leslies_extra_appendix

Step 2:

Merge the changes and update on Gitea.
git checkout shm_apis
git merge --no-ff leslies_extra_appendix
git push origin shm_apis
Sign in to join this conversation.
No reviewers
No Label
No Milestone
No project
No Assignees
2 Participants
Notifications
Due Date
The due date is invalid or out of range. Please use the format 'yyyy-mm-dd'.

No due date set.

Dependencies

No dependencies set.

Reference: goodboy/tractor#24
There is no content yet.