Rate this Page

Host#

A Host manages a collection of spawned procs and provides bidirectional routing between them. It serves as the entry point for external connections and coordinates message delivery across local and remote actors.

Overview#

The Host struct maintains two key channel endpoints:

  • Frontend address: accepts connections from external clients

  • Backend address: receives messages from spawned procs

Both endpoints feed into a unified routing layer that can deliver messages to either the service proc (running within the host) or to spawned procs.

                      ┌────────────┐
                  ┌───▶  proc *,1  │
                  │ #1└────────────┘
                  │
  ┌──────────┐    │   ┌────────────┐
  │   Host   │◀───┼───▶  proc *,2  │
 *└──────────┘#   │ #2└────────────┘
                  │
                  │   ┌────────────┐
                  └───▶  proc *,3  │
                    #3└────────────┘

Where:

  • * is the host’s frontend address (frontend_addr)

  • # is the host’s backend address (backend_addr)

  • #1, #2, #3 are the per-proc backend channels

  • Each proc is direct-addressed via the host - its id is “proc at * named N

Structure#

pub struct Host<M> {
    procs: HashSet<String>,
    frontend_addr: ChannelAddr,
    backend_addr: ChannelAddr,
    router: DialMailboxRouter,
    manager: M,
    service_proc: Proc,
    local_proc: Proc,
    frontend_rx: Option<ChannelRx<MessageEnvelope>>,
}

Fields:

  • procs: Stores proc names to avoid creating duplicates

  • frontend_addr: The address external clients connect to

  • backend_addr: The address spawned procs use to send messages back to the host

  • router: A DialMailboxRouter for prefix-based routing to spawned procs

  • manager: A ProcManager implementation that handles proc lifecycle

  • service_proc: The host’s local proc for system-level actors

  • local_proc: The host’s local proc for user-level actors

  • frontend_rx: Channel receiver for external connections (consumed during startup)

Creating a Host#

The Host::new constructor takes a ProcManager and a channel address to serve:

impl<M: ProcManager> Host<M> {
    pub async fn new(manager: M, addr: ChannelAddr) -> Result<Self, HostError> {
        let (frontend_addr, frontend_rx) = channel::serve(addr)?;
        let (backend_addr, backend_rx) = channel::serve(
            ChannelAddr::any(manager.transport())
        )?;

        let router = DialMailboxRouter::new();

        let service_proc_id = ProcId::Direct(
            frontend_addr.clone(),
            "service".to_string()
        );
        let service_proc = Proc::new(service_proc_id.clone(), router.boxed());

        let local_proc_id = ProcId::Direct(
            frontend_addr.clone(),
            "local".to_string()
        );
        let local_proc = Proc::new(local_proc_id.clone(), router.boxed());

        let host = Host {
            procs: HashSet::new(),
            frontend_addr,
            backend_addr,
            router,
            manager,
            service_proc,
            local_proc,
            frontend_rx: Some(frontend_rx),
        };

        let _backend_handle = host.forwarder().serve(backend_rx);

        Ok(host)
    }
}

Understanding channel::serve()#

channel::serve() is the universal “bind and listen” operation across different transport types. It:

  • Takes a ChannelAddr (which can be a wildcard like ChannelAddr::any())

  • Binds a server/listener on that address

  • Returns a tuple of:

    • The actual bound address (resolved from wildcards)

    • A receiver (ChannelRx<MessageEnvelope>) for incoming messages

This is why both calls in Host::new capture the returned address:

let (frontend_addr, frontend_rx) = channel::serve(addr)?;
let (backend_addr, backend_rx) = channel::serve(ChannelAddr::any(manager.transport()))?;

The returned address is the actual bound address you can give to others to connect to. For example, when you pass ChannelAddr::Tcp(127.0.0.1:0):

  • Input: “bind to localhost on any available port”

  • Output: (ChannelAddr::Tcp(127.0.0.1:54321), rx) - the OS-assigned port

See Channel Addresses and Transmits and Receives for more on channel semantics.

The Service Proc and Local Proc#

The host creates two procs identified by ProcId::Direct:

Service Proc:

let service_proc_id = ProcId::Direct(
    frontend_addr.clone(),
    "service".to_string()
);
let service_proc = Proc::new(service_proc_id, router.boxed());

Local Proc:

let local_proc_id = ProcId::Direct(
    frontend_addr.clone(),
    "local".to_string()
);
let local_proc = Proc::new(local_proc_id, router.boxed());

Both procs:

  • Live within the host process

  • Use ProcId::Direct(frontend_addr, name) as their identity

  • Forward outbound messages through the DialMailboxRouter

  • The service proc hosts system-level actors that manage proc lifecycle and coordination

  • The local proc hosts user-level actors

See ProcId variants for the distinction between Ranked and Direct addressing.

Routing Architecture#

The host implements bidirectional routing using a specialized ProcOrDial router (see ProcOrDial Router). Both the frontend and backend receivers are served by this router:

Backend receiver (from spawned procs):

let _backend_handle = host.forwarder().serve(backend_rx);

Frontend receiver (from external clients):

Some(self.forwarder().serve(self.frontend_rx.take()?))

Complete Routing Flow#

frontend_rx (external connections)    ──┐
                                        ├──> serve() ──> ProcOrDial   ──┬──> service proc
backend_rx (from spawned procs)       ──┘                               ├──> local proc
                                                                        └──> DialMailboxRouter
                                                                             │
                                                                             └──> looks up proc by name
                                                                                  └──> dials backend addr

Both receivers feed into the same ProcOrDial router, creating bidirectional routing:

  • Inbound (frontend): External → ProcOrDial → service proc or local proc or spawned proc

  • Inbound (backend): Spawned procs → ProcOrDial → service proc or local proc or other spawned procs

  • Outbound (from service proc or local proc): service_proc.forwarder / local_proc.forwarder = DialMailboxRouter → spawned procs

See MailboxServer::serve() for how receivers are bridged to routers.

Channel Receivers#

The ChannelRx<M> receiver returned from channel::serve() implements the Rx<M> trait:

trait Rx<M: RemoteMessage> {
    async fn recv(&mut self) -> Result<M, ChannelError>;
    fn addr(&self) -> ChannelAddr;
}

It’s a stream of incoming messages of type M. In the host context, M = MessageEnvelope, so it receives actor messages from the network.

How the host uses receivers:

  • Frontend: Serves the user-provided addr → receives messages from external connections via frontend_rx

  • Backend: Serves a wildcard backend address → receives messages from spawned procs via backend_rx

Both are consumed by calling .serve() on the ProcOrDial forwarder, which bridges the channel receivers to the mailbox routing system.

Next Steps#

  • See ProcOrDial Router for the routing implementation

  • See Routers for DialMailboxRouter details

  • See Proc for how procs integrate with routers, including the local proc bypass optimization