IPC and logging#
Under every portal, context and stream sits a per-peer
Channel: a msgpack-typed messaging link wrapping
one OS transport connection. Transports are pluggable per actor
via enable_transports=['tcp' | 'uds'] — TCP is the default,
UDS (unix domain sockets) gives you port-less, same-host IPC with
kernel-provided peer credentials for free — and exactly one
transport may currently be enabled per actor.
Where Channel sits in the runtime stack.#
Addresses are “unwrapped” tuples at the API edges:
('host', port) for TCP, filesystem-path pairs for UDS. For
the full layering story — transport protocols, the IPC server,
address types and the msg loop — see
Anatomy of the runtime.
Channel#
- class tractor.Channel(transport=None)[source]#
An inter-process channel for communication between (remote) actors.
Wraps a
MsgStream: transport + encoding IPC connection.Currently we only support
trio.SocketStreamfor transport (aka TCP) and themsgpackinterchange format via themsgspeccodec libary.- Parameters:
transport (MsgTransport | None)
- property aid: Aid#
Peer actor’s ID.
- apply_codec(codec)[source]#
Temporarily override the underlying IPC msg codec for dynamic enforcement of messaging schema.
- Parameters:
codec (MsgCodec)
- Return type:
None
- async send(payload, hide_tb=False)[source]#
Send a coded msg-blob over the underlying IPC transport.
This fn raises TransportClosed on comms failures and is normally handled by higher level runtime machinery for the expected-graceful cases, normally ephemercal (re/dis)connects.
Deprecated since version 0.1.0a6: Channel.uid warns; use Channel.aid which carries
richer (optional) identity fields beyond the legacy
(name, uuid) pair.
Note
You rarely construct a Channel yourself — the runtime
hands them out via Portal.chan
and Context.chan. Treat the
send/recv surface as advanced API: normal apps should speak
MsgStream instead.
Choosing a transport#
'''
Demonstrate an actor tree which talks over unix-domain-socket
(UDS) transport instead of the default TCP: pass
`enable_transports=['uds']` when opening the root and every
subactor inherits the preference.
Every channel address is a filesystem socket path (no TCP port
in sight!) and, as a kernel-provided bonus, the peer's pid is
exchanged for free via `SO_PEERCRED`.
'''
import os
import trio
import tractor
async def report_addr() -> str:
'''
Return this actor's own accept (bind) addr + pid.
'''
actor = tractor.current_actor()
addr: tuple = actor.accept_addr
pid: int = os.getpid()
return f'{actor.name}@{addr} pid={pid}'
async def main() -> None:
async with tractor.open_nursery(
enable_transports=['uds'],
) as an:
portal = await an.start_actor(
'uds_child',
enable_modules=[__name__],
)
# the channel's remote addr is a `UDSAddress`: a
# filesystem socket path, NOT a (host, port) pair!
raddr = portal.chan.raddr
assert raddr.proto_key == 'uds'
# NOTE, `.sockpath` is the *shared listener* socket file
# (named for the root registrar) this channel rode in
# on, NOT a per-child path; the child-specific identity
# we get for free is the kernel-reported peer pid (via
# `SO_PEERCRED`).
print(
f'portal chan tpt proto: {raddr.proto_key!r}\n'
f'listener sock file: {raddr.sockpath}\n'
f'kernel-reported peer pid: {raddr.maybe_pid}\n'
)
# ask the child for its OWN distinct bind addr: another
# socket-file path under the runtime dir.
print(f'child says: {await portal.run(report_addr)}')
await portal.cancel_actor()
if __name__ == '__main__':
trio.run(main)
Logging#
tractor.log provides the structured, colorized console
logging used across the runtime — with actor-name + task-aware
record headers and extra log levels below logging.DEBUG
('transport', 'runtime', 'cancel', 'devx') for
spelunking the runtime itself. Use it for your app too: it’s
already distributed-system aware.
- tractor.log.get_logger(name=None, pkg_name='tractor', _root_name=None, logger=None, subsys_spec=None, mk_sublog=True, _strict_debug=False)[source]#
Return the tractor-library root logger or a sub-logger for name if provided.
When name is left null we try to auto-detect the caller’s mod.__name__ and use that as a the sub-logger key. This allows for example creating a module level instance like,
log = tractor.log.get_logger(_root_name='mylib')
and by default all console record headers will show the caller’s (of any log.<level>()-method) correct sub-pkg’s + py-module-file.
- tractor.log.get_console_log(level=None, logger=None, **kwargs)[source]#
Get a tractor-style logging instance: a Logger wrapped in a StackLevelAdapter which injects various concurrency-primitive (process, thread, task) fields and enables a StreamHandler that writes on stderr using colorlog formatting.
Yeah yeah, i know we can use logging.config.dictConfig(). You do it.
- Parameters:
- Return type:
Note
The TRACTOR_LOGLEVEL env var overrides any caller-passed
loglevel (e.g. to open_root_actor()) so you can crank
console verbosity without touching code; subactors inherit
the root’s level by default.
See also
Anatomy of the runtime for the transport/server internals, Discovery and the registrar for how channel addresses get registered and found, and Typed messaging: tractor.msg for the codec layer every channel speaks.