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`))
wkt/wg_pyroute2_read
Gud Boi 2026-08-29 23:56:21 -04:00
parent b1f6ade840
commit 3e5a699df6
41 changed files with 427 additions and 120 deletions

View File

@ -0,0 +1,47 @@
---
model: gpt-5.6-sol
service: opencode
session: tractor-net-api-extraction
timestamp: 2026-08-30T02:52:01Z
git_ref: b1f6ade8
scope: code
substantive: true
raw_file: 20260830T025201Z_b1f6ade8_prompt_io.raw.md
---
## Prompt
After rebasing the WireGuard Layer C stack, the human questioned the
placement of bindspace APIs under `tractor.discovery`, considered new
public package names, selected `tractor.net`, and authorized the
implementation.
## Response summary
Extracted bindspace and tunnel implementations into a lazy
`tractor.net` package. Moved public multiaddr, bindspace, tunnel, and
WireGuard symbols out of `tractor.discovery`, updated internal imports,
tests, examples, and documentation, and added cold-import regressions
that keep optional networking dependencies off the root import path.
Current-netns attachment pins `/proc/thread-self/ns/net` so calls from
non-leader threads retain the caller's namespace rather than the process
leader's.
## Files changed
- `tractor/net/` - lazy public network API and implementation modules.
- `tractor/discovery/` - retain actor discovery and internal address parsing.
- `tractor/__init__.py` - expose `tractor.net` lazily.
- `tests/net/` - network declaration and lifecycle coverage.
- `tests/test_lazy_imports.py` - enforce the lazy package boundary.
- `examples/multihost/wg_lan/` - use the public network package.
- `docs/` - document network APIs separately from actor discovery.
## Human edits
The human rejected `tractor.discovery` as the long-term public home,
considered tunnel- and namespace-specific alternatives, and selected
the broader `tractor.net` boundary because bindspaces may include plain
netns, WireGuard, VRF, veth, and later network resources. The agent
applied the resulting source changes; no direct manual edits were
observed.

View File

@ -0,0 +1,29 @@
---
model: gpt-5.6-sol
service: opencode
timestamp: 2026-08-30T02:52:01Z
git_ref: b1f6ade8
diff_cmd: git diff HEAD~1..HEAD
---
# Raw output - extract public network APIs
The human questioned whether bindspace and WireGuard lifecycle APIs
belonged under actor discovery, selected the proposed `tractor.net`
boundary, and authorized implementation.
## Generated code
> `git diff HEAD~1..HEAD -- tractor/net tractor/discovery tractor/__init__.py`
Move bindspace and tunnel implementations into a lazy public network
package. Keep actor discovery focused on registry and lookup behavior,
while exposing bindspace, tunnel, WireGuard, and multiaddr declarations
through `tractor.net` without loading optional networking dependencies
during `import tractor`.
> `git diff HEAD~1..HEAD -- tests/net tests/test_lazy_imports.py tests/ipc examples/multihost/wg_lan docs`
Move network-focused tests to `tests/net`, update internal and public
imports, verify lazy symbol resolution and removed discovery exports,
and document the new package boundary.

View File

@ -47,7 +47,7 @@ Here is a small example from piker,
We should take whatever common API is needed to support this and
distill it into a
```python
tractor.discovery.parse_endpoints(
tractor.net.parse_endpoints(
) -> dict[
str,
list[Address]

View File

@ -209,7 +209,7 @@ Observed protocol-name lists, for writing the `match`:
### 3.3 pure codecs + explicit verification
Port #482 §2's pure helpers into
`tractor/discovery/_tunnel.py`, keeping the impure probe cleanly
`tractor/net/_tunnel.py`, keeping the impure probe cleanly
separated until layer B:
```python

View File

@ -53,6 +53,7 @@ Most-used names at a glance:
core
context
discovery
net
errors
msg
trionics

View File

@ -84,6 +84,7 @@ already distributed-system aware.
.. seealso::
:doc:`/explain/architecture` for the transport/server
internals, :doc:`/api/discovery` for how channel addresses
get registered and found, and :doc:`/api/msg` for the codec
layer every channel speaks.
internals, :doc:`/api/net` for network declarations,
:doc:`/api/discovery` for how channel addresses get registered
and found, and :doc:`/api/msg` for the codec layer every channel
speaks.

62
docs/api/net.rst 100644
View File

@ -0,0 +1,62 @@
Network declarations and lifecycles
===================================
``tractor.net`` provides address composition, bindspace declarations,
and tunnel configuration. The package is lazy: importing
``tractor`` or ``tractor.net`` does not load multiaddr, WireGuard, or
pyroute2 implementation modules until a public symbol is used.
Multiaddr helpers
-----------------
.. currentmodule:: tractor.net
.. autofunction:: mk_maddr
.. autofunction:: parse_maddr
.. autofunction:: parse_endpoints
Bindspaces
----------
.. autoclass:: BindspaceSpec
.. autoclass:: BindspaceRef
.. autoclass:: Bindspace
.. autofunction:: attach_netns
.. autofunction:: open_netns
.. autofunction:: open_bindspace
Tunnels and WireGuard
---------------------
.. autoclass:: TunnelledAddress
.. autoclass:: WGTunnelSpec
.. autoclass:: WGInterfaceConfig
.. autoclass:: WGPeerConfig
.. autofunction:: parse_wg_maddr
.. autofunction:: mk_wg_maddr
.. autofunction:: strip_tunnels
.. autofunction:: tunnels_of
.. autofunction:: open_wg_iface
.. autofunction:: open_wg_bindspace
.. autofunction:: read_wg_pubkey
.. autofunction:: read_wg_peers
.. autofunction:: verify_wg_peer

View File

@ -264,7 +264,7 @@ terminology is retired: it's *registrar*/*registry* everywhere now
substitute "registrar" and you're up to date.
.. note::
Multihoming nerds: ``tractor.discovery`` also ships
Multihoming nerds: ``tractor.net`` ships
libp2p-style *multiaddr* helpers — ``mk_maddr()`` and
``parse_maddr()`` — for describing transport endpoints as
structured strings.

View File

@ -116,7 +116,7 @@ ping -c1 10.0.11.1 # from B
```bash
python -c "
from tractor.discovery import mb_pubkey
from tractor.net import mb_pubkey
key = open('wg_pub.key').read().strip()
print(mb_pubkey(key))
"
@ -166,7 +166,7 @@ Four corrections, all from
## next
Layer A's `TunnelledAddress` and native maddr parser plus Layer B's
explicit pyroute2 verification now live in `tractor.discovery`. Next,
add `open_bindspace()` `@acm`s which create/tear down the iface and
netns.
The `TunnelledAddress`, native maddr parser, bindspace lifecycle, and
explicit pyroute2 verification APIs live in `tractor.net`. Root actor
bindspace integration remains future work; callers compose these
lifecycles explicitly for now.

View File

@ -10,7 +10,7 @@ from __future__ import annotations
import tractor
import trio
from tractor.discovery import (
from tractor.net import (
TunnelledAddress,
mk_maddr,
parse_wg_maddr,

View File

@ -8,7 +8,7 @@ from __future__ import annotations
import tractor
import trio
from tractor.discovery import (
from tractor.net import (
TunnelledAddress,
parse_wg_maddr,
verify_wg_peer,

View File

@ -18,7 +18,7 @@ from tractor.devx import dump_on_hang
from tractor.trionics import collapse_eg
from tractor._testing import tractor_test
from tractor.discovery._addr import wrap_address
from tractor.discovery._multiaddr import mk_maddr
from tractor.net import mk_maddr
import trio

View File

@ -24,7 +24,7 @@ from tractor.discovery import _api
from tractor.discovery._addr import (
wrap_address,
)
from tractor.discovery._multiaddr import mk_maddr
from tractor.net import mk_maddr
from tractor.ipc import _connect_chan
from tractor._testing.addr import get_rando_addr

View File

@ -7,7 +7,7 @@ from __future__ import annotations
import pytest
import trio
from tractor.discovery import (
from tractor.net import (
TunnelledAddress,
WGTunnelSpec,
tunnels_of,

View File

@ -6,7 +6,7 @@ from __future__ import annotations
import trio
from tractor.discovery import (
from tractor.net import (
BindspaceRef,
TunnelledAddress,
WGTunnelSpec,

View File

@ -0,0 +1 @@
'''Network declaration and lifecycle tests.'''

View File

@ -13,7 +13,7 @@ import msgspec
import pytest
import trio
from tractor.discovery import (
from tractor.net import (
Bindspace,
BindspaceOwnership,
BindspaceRef,
@ -23,7 +23,7 @@ from tractor.discovery import (
open_bindspace,
open_netns,
)
from tractor.discovery import _bindspace
from tractor.net import _bindspace
from tractor.msg import ProcessLocal
@ -261,10 +261,11 @@ def test_open_bindspace_attaches_current_netns() -> None:
'''
The unnamed spec must borrow and pin the caller's current netns.
Open `/proc/self/ns/net`, prove the yielded bindspace records its
stable inode and borrowed ownership, then exit the context and
prove the exact descriptor was closed without altering the
namespace itself.
Opening `/proc/self/ns/net` could pin the thread-group leader's
namespace when this context runs from another thread. Prove the
implementation selects `/proc/thread-self/ns/net`, records the
calling thread's stable inode and borrowed ownership, then closes
the exact descriptor without altering the namespace itself.
'''
async def main() -> int:
@ -276,6 +277,9 @@ def test_open_bindspace_attaches_current_netns() -> None:
kind='netns',
)
assert spec.key is CURRENT_NETNS
assert _bindspace._THREAD_NETNS == Path(
'/proc/thread-self/ns/net'
)
async with open_bindspace(spec) as bindspace:
namespace_fd: int|None = bindspace.namespace_fd
assert namespace_fd is not None

View File

@ -1,7 +1,6 @@
'''
Multiaddr construction, parsing, and round-trip tests for
`tractor.discovery._multiaddr.mk_maddr()` and
`tractor.discovery._multiaddr.parse_maddr()`.
`tractor.net.mk_maddr()` and `tractor.net.parse_maddr()`.
'''
from pathlib import Path
@ -10,20 +9,20 @@ from types import SimpleNamespace
import pytest
from multiaddr import Multiaddr
from tractor.discovery import (
from tractor.net import (
TunnelledAddress,
WGTunnelSpec,
mb_pubkey,
mk_wg_maddr,
mk_maddr,
parse_endpoints,
parse_maddr,
parse_wg_maddr,
tunnels_of,
)
from tractor.ipc._tcp import TCPAddress
from tractor.ipc._uds import UDSAddress
from tractor.discovery._multiaddr import (
mk_maddr,
parse_maddr,
parse_endpoints,
_tpt_proto_to_maddr,
_maddr_to_tpt_proto,
)

View File

@ -14,7 +14,7 @@ from __future__ import annotations
import msgspec
import pytest
from tractor.discovery import (
from tractor.net import (
BindspaceRef,
TunnelledAddress,
WGTunnelSpec,

View File

@ -7,7 +7,7 @@ from __future__ import annotations
import msgspec
import pytest
from tractor.discovery import (
from tractor.net import (
WGInterfaceConfig,
WGPeerConfig,
)

View File

@ -13,7 +13,7 @@ from typing import BinaryIO
import pytest
import trio
from tractor.discovery import (
from tractor.net import (
Bindspace,
BindspaceRef,
BindspaceSpec,
@ -23,7 +23,7 @@ from tractor.discovery import (
open_wg_bindspace,
open_wg_iface,
)
from tractor.discovery import _tunnel
from tractor.net import _tunnel
_LOCAL_KEY: str = 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA='

View File

@ -13,13 +13,13 @@ from typing import (
import pytest
import trio
from tractor.discovery import (
from tractor.net import (
read_wg_peers,
read_wg_pubkey,
verify_wg_peer,
WGTunnelSpec,
)
from tractor.discovery import _tunnel
from tractor.net import _tunnel
pyroute2: Any = pytest.importorskip('pyroute2')

View File

@ -44,8 +44,8 @@ def test_lazy_to_asyncio_package_api():
Before the lazy conversion, package import side effects exposed
`to_asyncio` to `dir()` and wildcard imports. Exercise those APIs
in cold interpreters so this test proves normal `import tractor`
leaves `asyncio` unloaded, while discovery and wildcard access
still advertise and resolve the public submodule.
leaves `asyncio` unloaded, while introspection and wildcard
access still advertise and resolve the public submodule.
'''
cold = run_cold_import(
@ -102,6 +102,12 @@ def test_cold_import_budget():
'bidict',
'colorlog',
'multiaddr',
'multibase',
'pyroute2',
'tractor.discovery._multiaddr',
'tractor.net',
'tractor.net._bindspace',
'tractor.net._tunnel',
'wrapt',
)
code = (
@ -136,6 +142,123 @@ def test_cold_import_budget():
)
def test_lazy_net_package_api():
'''
Keep the public network package cold until symbol access.
The old discovery re-exports imported bindspace, tunnel,
multiaddr and optional dependencies while initializing a package.
Import `tractor.net` in a clean interpreter, inspect its public
surface, and prove no implementation or optional dependency was
loaded. Then resolve one symbol from each backing module and
prove the facade caches each value while preserving boundaries.
'''
modules: tuple[str, ...] = (
'tractor.net._bindspace',
'tractor.net._tunnel',
'tractor.discovery._multiaddr',
'multiaddr',
'multibase',
'pyroute2',
)
cold: dict[str, object] = run_cold_import(
'import json, sys; import tractor.net as net; '
f'names = {modules!r}; '
'print(json.dumps({'
'"public": all(name in dir(net) for name in net.__all__), '
'"loaded": [name for name in names if name in sys.modules]'
'}))'
)
assert cold == {
'public': True,
'loaded': [],
}
resolved: dict[str, object] = run_cold_import(
'import json, sys; import tractor.net as net; '
'bindspace = net.BindspaceSpec; '
'bindspace_cached = net.BindspaceSpec is bindspace; '
'maddr = net.mk_maddr; '
'maddr_cached = net.mk_maddr is maddr; '
'tunnel = net.WGTunnelSpec; '
'tunnel_cached = net.WGTunnelSpec is tunnel; '
'print(json.dumps({'
'"bindspace_cached": bindspace_cached, '
'"maddr_cached": maddr_cached, '
'"tunnel_cached": tunnel_cached, '
'"bindspace_module": bindspace.__module__, '
'"maddr_module": maddr.__module__, '
'"tunnel_module": tunnel.__module__, '
'"multiaddr_loaded": "multiaddr" in sys.modules, '
'"pyroute2_loaded": "pyroute2" in sys.modules'
'}))'
)
assert resolved == {
'bindspace_cached': True,
'maddr_cached': True,
'tunnel_cached': True,
'bindspace_module': 'tractor.net._bindspace',
'maddr_module': 'tractor.discovery._multiaddr',
'tunnel_module': 'tractor.net._tunnel',
'multiaddr_loaded': False,
'pyroute2_loaded': False,
}
def test_net_root_export_and_old_discovery_surface():
'''
Publish networking only from its approved namespace.
Before extraction, unshipped network names and implementation
modules lived under `tractor.discovery`. Exercise root attribute
and wildcard access in clean interpreters, proving `tractor.net`
is discoverable and cached without loading implementations. Also
prove the old exports are absent and their modules no longer
resolve, preventing accidental compatibility aliases.
'''
root: dict[str, object] = run_cold_import(
'import json, sys, tractor; '
'advertised = "net" in dir(tractor); '
'net = tractor.net; '
'print(json.dumps({'
'"advertised": advertised, '
'"cached": tractor.net is net, '
'"module": net.__name__, '
'"bindspace_loaded": '
'"tractor.net._bindspace" in sys.modules, '
'"tunnel_loaded": "tractor.net._tunnel" in sys.modules'
'}))'
)
assert root == {
'advertised': True,
'cached': True,
'module': 'tractor.net',
'bindspace_loaded': False,
'tunnel_loaded': False,
}
old: dict[str, object] = run_cold_import(
'import importlib.util, json; '
'import tractor.discovery as discovery; '
'old_names = ("Bindspace", "TunnelledAddress", '
'"mk_maddr", "parse_maddr", "parse_endpoints"); '
'old_modules = ("tractor.discovery._bindspace", '
'"tractor.discovery._tunnel"); '
'print(json.dumps({'
'"exports": [name for name in old_names '
'if hasattr(discovery, name)], '
'"modules": [name for name in old_modules '
'if importlib.util.find_spec(name) is not None]'
'}))'
)
assert old == {
'exports': [],
'modules': [],
}
def test_lazy_annotation_names_resolve():
'''
Resolve annotations without importing optional dependencies.

View File

@ -24,7 +24,7 @@ import trio
import tractor
from tractor import _child
from tractor.devx import _proctitle
from tractor.discovery._bindspace import (
from tractor.net._bindspace import (
Bindspace,
BindspaceRef,
BindspaceSpec,

View File

@ -18,6 +18,7 @@
tractor: structured concurrent ``trio``-"actors".
"""
from types import ModuleType as _ModuleType
from ._clustering import (
open_actor_cluster as open_actor_cluster,
@ -82,6 +83,7 @@ __all__: tuple[str, ...] = tuple(
for name in globals()
if not name.startswith('_')
) + (
'net',
'to_asyncio',
)
@ -92,21 +94,18 @@ def __dir__() -> list[str]:
def __getattr__(name: str):
'''
PEP 562 lazy sub-module loading, presently only for
`.to_asyncio` which (transitively) imports `asyncio`
itself: a non-trivial multi-ms chunk of the eager
`import tractor` cost (gh #470) unneeded by
`trio`-only apps.
PEP 562 lazy public sub-package loading.
Any `tractor.to_asyncio.<attr>` access (or a
`from tractor import to_asyncio`) still works, the
sub-mod is simply imported on first-access instead
of at pkg-import time.
`tractor.to_asyncio` transitively imports `asyncio`, while
`tractor.net` owns optional network dependencies. Neither is
needed by most applications merely importing the root package.
'''
if name == 'to_asyncio':
if name in ('net', 'to_asyncio'):
from importlib import import_module
return import_module('.to_asyncio', __name__)
module: _ModuleType = import_module(f'.{name}', __name__)
globals()[name] = module
return module
raise AttributeError(
f'module {__name__!r} has no attribute {name!r}'

View File

@ -55,7 +55,6 @@ from .discovery._addr import (
mk_uuid,
wrap_address,
)
from .discovery._tunnel import strip_tunnels
from .trionics import (
is_multi_cancelled,
collapse_eg,
@ -505,6 +504,8 @@ async def open_root_actor(
# XXX INSTEAD, bind random addrs using the same tpt
# proto if not already provided.
if not tpt_bind_addrs:
from .net._tunnel import strip_tunnels
for addr in ponged_addrs:
bindable_addr: Address = strip_tunnels(addr)
tpt_bind_addrs.append(

View File

@ -15,48 +15,8 @@
# along with this program. If not, see <https://www.gnu.org/licenses/>.
'''
Discovery (protocols) API for automatic addressing
and location management of (service) actors.
Actor discovery and registrar implementation package.
NOTE: this ``__init__`` only eagerly imports the lightweight
``._multiaddr`` and ``._tunnel`` submodules for public re-exports.
Heavier submodules like ``._addr`` and ``._api`` are NOT imported
here to avoid circular imports; use direct module paths for those.
Network declarations and helpers are public from `tractor.net`.
'''
from ._bindspace import (
Bindspace as Bindspace,
BindspaceKind as BindspaceKind,
BindspaceLifecycle as BindspaceLifecycle,
BindspaceOwnership as BindspaceOwnership,
BindspaceRef as BindspaceRef,
BindspaceSpec as BindspaceSpec,
CURRENT_NETNS as CURRENT_NETNS,
attach_netns as attach_netns,
open_bindspace as open_bindspace,
open_netns as open_netns,
)
from ._multiaddr import (
parse_endpoints as parse_endpoints,
parse_maddr as parse_maddr,
mk_maddr as mk_maddr,
)
from ._tunnel import (
TunnelledAddress as TunnelledAddress,
TunnelSpec as TunnelSpec,
WGTunnelSpec as WGTunnelSpec,
WGInterfaceConfig as WGInterfaceConfig,
WGPeerConfig as WGPeerConfig,
WGRole as WGRole,
mb_pubkey as mb_pubkey,
mk_wg_maddr as mk_wg_maddr,
open_wg_bindspace as open_wg_bindspace,
open_wg_iface as open_wg_iface,
parse_wg_maddr as parse_wg_maddr,
read_wg_peers as read_wg_peers,
read_wg_pubkey as read_wg_pubkey,
strip_tunnels as strip_tunnels,
tunnels_of as tunnels_of,
verify_wg_peer as verify_wg_peer,
wg8_pubkey as wg8_pubkey,
)

View File

@ -42,7 +42,7 @@ from ..ipc._uds import (
if TYPE_CHECKING:
# ONLY type-annots, the eager import costs ~4.5ms
# of `import tractor` wall-time (gh #470).
from ._tunnel import (
from tractor.net._tunnel import (
TunnelledAddress,
)
from ..runtime._runtime import Actor
@ -237,8 +237,9 @@ def is_wrapped_addr(addr: any) -> bool:
# XXX NOTE, a `TunnelledAddress` is genuinely "wrapped" but is
# deliberately NOT in `_address_types`: it has no
# `MsgTransport` of its own (a tunnel is transparent to
# `socket(2)`), so it gets no proto-key entry. See `._tunnel`.
from ._tunnel import TunnelledAddress
# `socket(2)`), so it gets no proto-key entry. See
# `tractor.net._tunnel`.
from tractor.net._tunnel import TunnelledAddress
return (
type(addr) in _address_types.values()
or
@ -333,7 +334,7 @@ def wrap_address(
# multiaddr-format string, e.g.
# '/ip4/127.0.0.1/tcp/1616'
case str() if addr.startswith('/'):
from tractor.discovery._multiaddr import (
from tractor.net import (
parse_maddr,
)
return parse_maddr(addr)

View File

@ -38,7 +38,7 @@ if TYPE_CHECKING:
# `import tractor` path (gh #470).
from multiaddr import Multiaddr
from tractor.discovery._addr import Address
from tractor.discovery._tunnel import (
from tractor.net._tunnel import (
TunnelledAddress,
)
else:
@ -71,7 +71,7 @@ def mk_maddr(
'''
from multiaddr import Multiaddr
from ._tunnel import (
from tractor.net._tunnel import (
TunnelledAddress,
mk_wg_maddr,
)
@ -130,7 +130,7 @@ def parse_maddr(
# fails. Pre-checking the raw string would misclassify valid
# values such as `/unix/tmp/wg/service.sock`.
if '/wg/' in maddr_str:
from ._tunnel import _wg_proto_code
from tractor.net._tunnel import _wg_proto_code
_wg_proto_code()
raise
proto_names: list[str] = [
@ -156,7 +156,7 @@ def parse_maddr(
)
case _ if 'wg' in proto_names:
from ._tunnel import parse_wg_maddr
from tractor.net._tunnel import parse_wg_maddr
return parse_wg_maddr(maddr)
case _:

View File

@ -46,10 +46,6 @@ from tractor.discovery._addr import (
Address,
UnwrappedAddress,
)
from tractor.discovery._tunnel import (
TunnelledAddress,
strip_tunnels,
)
from tractor.log import get_logger
from tractor._exceptions import (
MsgTypeError,
@ -63,6 +59,9 @@ from tractor.msg import (
if TYPE_CHECKING:
from ._transport import MsgTransport
from tractor.net._tunnel import TunnelledAddress
else:
TunnelledAddress = Any
log = get_logger()
@ -190,6 +189,8 @@ class Channel:
**kwargs
) -> Channel:
from tractor.net._tunnel import strip_tunnels
if not is_wrapped_addr(addr):
addr = wrap_address(addr)

View File

@ -68,7 +68,7 @@ from ._transport import MsgTransport
if TYPE_CHECKING:
from ..discovery._tunnel import TunnelledAddress
from ..net._tunnel import TunnelledAddress
from ..runtime._runtime import Actor
from ..runtime._supervise import ActorNursery
@ -1091,7 +1091,7 @@ async def _serve_ipc_eps(
`.cancel_server()` is called.
'''
from ..discovery._tunnel import strip_tunnels
from ..net._tunnel import strip_tunnels
try:
listen_tn: Nursery

View File

@ -37,7 +37,6 @@ from trio import (
from tractor.msg import MsgCodec
from tractor.log import get_logger
from tractor.discovery._multiaddr import mk_maddr
from tractor.ipc._transport import (
MsgTransport,
MsgpackTransport,
@ -235,6 +234,8 @@ class MsgpackTCPStream(MsgpackTransport):
@property
def maddr(self) -> Multiaddr:
from tractor.net import mk_maddr
return mk_maddr(self.raddr)
def connected(self) -> bool:

View File

@ -63,7 +63,6 @@ from trio._highlevel_open_unix_stream import (
from tractor.msg import MsgCodec
from tractor.log import get_logger
from tractor.discovery._multiaddr import mk_maddr
from tractor.ipc._transport import (
MsgpackTransport,
)
@ -611,6 +610,8 @@ class MsgpackUDSStream(MsgpackTransport):
@property
def maddr(self) -> Multiaddr|str:
from tractor.net import mk_maddr
if not self.raddr:
return '<unknown-peer>'

View File

@ -0,0 +1,75 @@
# tractor: structured concurrent "actors".
# Copyright 2018-eternity Tyler Goodlet.
'''
Network declarations, bindspaces and tunnels.
Public symbols are imported and cached on first access so importing
this package does not load optional network dependencies.
'''
from importlib import import_module
_SYMBOL_MODULES: dict[str, str] = {
'Bindspace': '._bindspace',
'BindspaceKind': '._bindspace',
'BindspaceLifecycle': '._bindspace',
'BindspaceOwnership': '._bindspace',
'BindspaceRef': '._bindspace',
'BindspaceSpec': '._bindspace',
'CURRENT_NETNS': '._bindspace',
'attach_netns': '._bindspace',
'open_bindspace': '._bindspace',
'open_netns': '._bindspace',
'TunnelledAddress': '._tunnel',
'TunnelSpec': '._tunnel',
'WGTunnelSpec': '._tunnel',
'WGInterfaceConfig': '._tunnel',
'WGPeerConfig': '._tunnel',
'WGRole': '._tunnel',
'mb_pubkey': '._tunnel',
'mk_wg_maddr': '._tunnel',
'open_wg_bindspace': '._tunnel',
'open_wg_iface': '._tunnel',
'parse_wg_maddr': '._tunnel',
'read_wg_peers': '._tunnel',
'read_wg_pubkey': '._tunnel',
'strip_tunnels': '._tunnel',
'tunnels_of': '._tunnel',
'verify_wg_peer': '._tunnel',
'wg8_pubkey': '._tunnel',
'mk_maddr': '..discovery._multiaddr',
'parse_maddr': '..discovery._multiaddr',
'parse_endpoints': '..discovery._multiaddr',
}
__all__: tuple[str, ...] = tuple(_SYMBOL_MODULES)
def __dir__() -> list[str]:
'''
Advertise the complete lazy public API.
'''
return sorted(set(globals()) | set(__all__))
def __getattr__(name: str) -> object:
'''
Import and cache one public network symbol on first access.
'''
try:
module_name: str = _SYMBOL_MODULES[name]
except KeyError:
raise AttributeError(
f'module {__name__!r} has no attribute {name!r}'
) from None
value: object = getattr(
import_module(module_name, __name__),
name,
)
globals()[name] = value
return value

View File

@ -51,7 +51,7 @@ BindspaceOwnership: TypeAlias = Literal[
]
_NETNS_RUN_DIR: Path = Path('/var/run/netns')
_SELF_NETNS: Path = Path('/proc/self/ns/net')
_THREAD_NETNS: Path = Path('/proc/thread-self/ns/net')
CURRENT_NETNS: Final[None] = None
@ -124,7 +124,7 @@ class BindspaceSpec(
Serializable declaration of one requested bindspace.
For a netns spec, `.key = CURRENT_NETNS` selects the calling
process's current namespace without a named-path lookup.
thread's current namespace without a named-path lookup.
'''
kind: BindspaceKind
@ -277,7 +277,7 @@ async def _pin_netns(
'''
key: str|None = spec.key
namespace_path: Path = (
_SELF_NETNS
_THREAD_NETNS
if key is CURRENT_NETNS
else _NETNS_RUN_DIR / key
)

View File

@ -19,7 +19,8 @@ Tunnelled addresses: an `Address` that rides *inside* a tunnel.
A tunnel (`wg`, and later plain ip-in-udp, `veth`-in-netns, ..) is
**not** a `MsgTransport`. Its data plane is transparent to the
application's `socket(2)`, so it never gets its own entry in
`._addr._address_types` nor a `MsgpackTransport` impl. Instead it
`tractor.discovery._addr._address_types` nor a `MsgpackTransport`
impl. Instead it
*annotates* an existing L4 addr, and this module carries that
annotation beside it.
@ -102,7 +103,7 @@ from ._bindspace import (
if TYPE_CHECKING:
from multiaddr import Multiaddr
from ._addr import (
from ..discovery._addr import (
Address,
UnwrappedAddress,
)
@ -1055,7 +1056,7 @@ def parse_wg_maddr(
]
match overlay_names:
case [('ip4' | 'ip6'), 'tcp']:
from ._multiaddr import parse_maddr
from ..discovery._multiaddr import parse_maddr
overlay: Address|TunnelledAddress = parse_maddr(
str(overlay_ma)
)
@ -1161,7 +1162,7 @@ def mk_wg_maddr(
f'/wg/{mb_pubkey(addr.tunnel.peer_pubkey)}'
)
from ._multiaddr import mk_maddr
from ..discovery._multiaddr import mk_maddr
overlay_ma: Multiaddr = mk_maddr(addr.overlay)
return (
bearer_ma

View File

@ -65,7 +65,7 @@ from ..spawn import _spawn
if TYPE_CHECKING:
import multiprocessing as mp
from ..discovery._bindspace import Bindspace
from ..net._bindspace import Bindspace
# from ..ipc._server import IPCServer
from ..ipc import IPCServer
from ..spawn._spawn import ProcessType

View File

@ -54,7 +54,7 @@ from ._spawn import (
if TYPE_CHECKING:
from tractor.discovery._bindspace import Bindspace
from tractor.net._bindspace import Bindspace
from tractor.ipc import (
_server,
)

View File

@ -50,7 +50,7 @@ from tractor.msg import types as msgtypes
if TYPE_CHECKING:
from tractor.discovery._bindspace import Bindspace
from tractor.net._bindspace import Bindspace
from tractor.ipc import (
_server,
Channel,

View File

@ -57,7 +57,7 @@ from ._spawn import (
if TYPE_CHECKING:
from tractor.discovery._bindspace import Bindspace
from tractor.net._bindspace import Bindspace
from tractor.ipc import (
_server,
)

View File

@ -61,7 +61,7 @@ from ..runtime._supervise import (
if TYPE_CHECKING:
from ..discovery._addr import UnwrappedAddress
from ..discovery._bindspace import Bindspace
from ..net._bindspace import Bindspace
from ..runtime._portal import Portal