macula_dist_relay_client (macula v11.1.0)

View Source

Client for the Macula dist relay (macula-io/macula-dist-relay).

Maintains a persistent QUIC connection to a dedicated dist relay and exposes a simple API for establishing point-to-point tunnels between Erlang nodes. Each tunnel is backed by a raw QUIC stream — no framing, no pub/sub, no application-level encryption.

This replaces the pub/sub bridge approach in macula_dist_pool (the SDK's previous dist-over-mesh implementation) which forced dist bytes through the station's MessagePack/handler pipeline.

Protocol

Control frames on stream 0 (length-prefixed MessagePack):

     node  relay    identify{node_name}
     relay  node    identified{status}
     node  relay    tunnel_request{target}
     relay  node    tunnel_ok{tunnel_id} | tunnel_error{reason}
     relay  node    tunnel_notify{tunnel_id, source}
     node  relay    tunnel_close{tunnel_id}

Tunnel data streams (stream 1+) carry the raw dist wire bytes after a 32-byte tunnel_id prefix (written by the relay on stream open) that lets the client match new_stream events to pending tunnels.

Lifecycle

1. start_link(RelayUrl, NodeName) connects + sends identify 2. Await identified reply 3. request_tunnel(TargetNode) blocks until tunnel_ok + tunnel stream arrive; returns {ok, ConnRef, StreamRef, Received} for use as the dist Socket in macula_dist, where Received holds the tunnel's bytes the client read before handing the stream over 4. Incoming tunnels: tunnel_notify arrives on control, then a new_stream event; client reads 32-byte prefix, matches to the notified tunnel, hands the stream to net_kernel via the standard {accept, _, Socket, Family, Driver} protocol, and the tunnel's bytes read before the handoff reach the dist controller in its controller-ok message.

Tunnel bytes at a handoff

A tunnel stream changes owner once or twice: to the caller of request_tunnel/2, or to a setup process and then the dist controller. The bytes an owner read before the handoff, including those that arrive while a tunnel waits for tunnel_ok or tunnel_notify, travel inside the handoff message, so the new owner has them before anything the stream delivers to it directly.

Tunnel end

The client monitors the process that holds each tunnel stream after the handoff: the caller of request_tunnel/2 for an outbound tunnel, and for an inbound tunnel the setup process until it names the dist controller, then the controller. When that process ends, however it ends, the client drops the tunnel and sends tunnel_close for it once.

Control stream

Control frames reach the relay in the order the client sends them. While the relay takes no data on the control stream, the client holds them and keeps serving tunnel requests, inbound tunnels and status/1, which reports how many it holds as held_control_frames.

Not yet implemented (Phase 2 MVP)

- net_kernel handoff for incoming tunnels (needs macula_dist integration on the accept side) - automatic reconnect on relay disconnect - multiple relay failover

Summary

Functions

Child spec for running the client under macula_root, as macula:join_dist_relay/1 does. Temporary: the client ends when the relay closes the connection and has no reconnect, so a restart would only repeat against a relay that is gone.

Close a tunnel the client knows, active or still being set up: the relay gets tunnel_close for it once. An unknown tunnel id is ignored.

Request a tunnel to TargetNode. Blocks until the tunnel stream is ready or the request fails/times out. Returns the connection and the stream, suitable for use as the dist Socket in macula_dist, and the tunnel's bytes this client read before handing the stream over: they come before anything the stream delivers to the caller.

Tell the client which process to deliver {accept, ...} messages to when inbound tunnels arrive. Called by macula_dist:accept/1 with the net_kernel pid (self() at that call site).

Start the client with a locally-registered name so macula_dist can find it without being passed the pid. Only one dist_relay_client per node makes sense — a node connects to exactly one dist relay for its cluster traffic.

Locate the registered client, if any.

Functions

child_spec(RelayUrl, NodeName)

-spec child_spec(binary() | string(), binary()) -> supervisor:child_spec().

Child spec for running the client under macula_root, as macula:join_dist_relay/1 does. Temporary: the client ends when the relay closes the connection and has no reconnect, so a restart would only repeat against a relay that is gone.

close_tunnel(Client, TunnelId)

-spec close_tunnel(pid(), binary()) -> ok.

Close a tunnel the client knows, active or still being set up: the relay gets tunnel_close for it once. An unknown tunnel id is ignored.

handle_call(Msg, From, State)

handle_cast(Msg, State)

handle_info(Info, State)

init(_)

request_tunnel(Client, TargetNode)

-spec request_tunnel(pid(), binary()) -> {ok, reference(), reference(), binary()} | {error, term()}.

Request a tunnel to TargetNode. Blocks until the tunnel stream is ready or the request fails/times out. Returns the connection and the stream, suitable for use as the dist Socket in macula_dist, and the tunnel's bytes this client read before handing the stream over: they come before anything the stream delivers to the caller.

set_kernel(Client, Kernel)

-spec set_kernel(pid(), pid()) -> ok.

Tell the client which process to deliver {accept, ...} messages to when inbound tunnels arrive. Called by macula_dist:accept/1 with the net_kernel pid (self() at that call site).

start_link(RelayUrl, NodeName)

-spec start_link(binary() | string(), binary()) -> {ok, pid()} | {error, term()}.

start_link(RelayUrl, NodeName, Opts)

-spec start_link(binary() | string(), binary(), map()) -> {ok, pid()} | {error, term()}.

Start the client with a locally-registered name so macula_dist can find it without being passed the pid. Only one dist_relay_client per node makes sense — a node connects to exactly one dist relay for its cluster traffic.

status(Client)

-spec status(pid()) -> map().

terminate(Reason, State)

whereis_client()

-spec whereis_client() -> pid() | undefined.

Locate the registered client, if any.