XGW.Peer.SendHandle (xgw v0.1.0)

Copy Markdown View Source

Hot-path outbound RTP without GenServer hops on Peer or Listener.

Why a send handle exists

After DTLS, every media packet must be SRTP-encrypted and written to UDP (or TURN-wrapped). Calling into the Peer GenServer for each packet adds latency and mailbox pressure. Instead, Peer publishes encrypt state into an ETS table; forwarders call send_rtp/3 with a handle from XGW.Peer.outbound_handle/1.

ICE-TCP has no hot handle ({:error, :use_genserver}); fall back to XGW.Peer.send_rtp_async/3.

Benches/tests may call publish_passthrough/2 (RTP) or publish_dc_passthrough/2 (datachannel) to register a synchronous MFA sink so Forwarder takes the SendHandle success path without a Peer GenServer hop.

Datachannel production egress uses the per-Peer XGW.Peer.Datachannel process (Peer.send_datachannel_async/4 casts there when published). A full SCTP-in-ETS hot path is not shipped (ssl_sock ownership).

ponytail: v1 assumes one packet writer per Peer outbound (forwarder). ROC write-back uses generation-matched select_replace so Peer republish cannot be clobbered. Multi-writer CAS across forwarders is the upgrade path.

RFCs

  • RFC 3711 -- SRTP encrypt on the outbound hot path
  • RFC 8656 -- TURN ChannelData / Send Indication when the path is relay

Summary

Types

t()

Opaque handle used by send_rtp/3.

Functions

Hot-path send handle.

Creates the ETS table (idempotent). Called from XGW.Application.

Looks up a published handle for peer.

Removes any ETS row for peer.

Publishes a plaintext datachannel passthrough sink for benches/tests.

Publishes a plaintext passthrough sink for benches/tests (no SRTP/UDP).

Publishes or refreshes the hot-path row from Peer GenServer state.

Publishes RTP and datachannel passthrough MFAs in one ETS row so Forwarder can use both hot paths for the same peer pid (trunk pipe).

Hot-path datachannel send when a :dc_passthrough row is published.

Encrypts and sends one RTP packet on the hot path.

Types

t()

@type t() :: %XGW.Peer.SendHandle{generation: pos_integer(), peer: pid()}

Opaque handle used by send_rtp/3.

Fields

  • :peer - Peer GenServer pid whose ETS row backs this handle
  • :generation - publish generation; stale handles fail closed after republish

Functions

%XGW.Peer.SendHandle{}

(struct)

Hot-path send handle.

Fields

  • :peer - Peer GenServer pid whose ETS row backs this handle
  • :generation - publish generation; stale handles fail closed after republish

ensure_table!()

@spec ensure_table!() :: :ok

Creates the ETS table (idempotent). Called from XGW.Application.

fetch(peer)

@spec fetch(pid()) :: {:ok, t()} | {:error, :not_ready | :use_genserver}

Looks up a published handle for peer.

Returns {:error, :not_ready} when missing, or {:error, :use_genserver} for ICE-TCP / GenServer-only rows.

invalidate(peer)

@spec invalidate(pid()) :: :ok

Removes any ETS row for peer.

publish_dc_passthrough(peer, mfa)

@spec publish_dc_passthrough(pid(), {module(), atom(), term()}) :: :ok

Publishes a plaintext datachannel passthrough sink for benches/tests.

Distinct from RTP publish_passthrough/2 so MFA arity does not clash (apply(mod, fun, [sink, channel_ref, data, type]) where type is :string | :binary).

publish_passthrough(peer, mfa)

@spec publish_passthrough(pid(), {module(), atom(), pid()}) :: :ok

Publishes a plaintext passthrough sink for benches/tests (no SRTP/UDP).

Forwarder prefers send_rtp/3 when a handle exists. The MFA is invoked synchronously as apply(mod, fun, [sink, ssrc, rtp]) so latency probes can record without a Peer GenServer hop. Used by latency benches and FakePeer tests.

publish_peer_state(peer, state)

@spec publish_peer_state(pid(), map()) :: :ok

Publishes or refreshes the hot-path row from Peer GenServer state.

Preserves existing ETS encrypt contexts across path republish. TCP selected transport stores a GenServer-only marker (no UDP hot path).

publish_trunk_passthrough(peer, rtp_mfa, dc_mfa)

@spec publish_trunk_passthrough(
  pid(),
  {module(), atom(), pid()},
  {module(), atom(), pid()}
) :: :ok

Publishes RTP and datachannel passthrough MFAs in one ETS row so Forwarder can use both hot paths for the same peer pid (trunk pipe).

send_datachannel(handle_or_peer, channel_ref, data, type \\ :string)

@spec send_datachannel(t() | pid(), reference(), binary(), :string | :binary) ::
  :ok | {:error, term()}

Hot-path datachannel send when a :dc_passthrough row is published.

type is :string (PPI 51) or :binary (PPI 53); passed to the MFA as the fourth argument so benches can observe PPI. Production Peer fallback uses the same type via XGW.Peer.send_datachannel_async/4.

Returns {:error, :not_ready} when missing, {:error, :stale} on generation mismatch, or {:error, :use_genserver} for GenServer-only / RTP rows (caller should fall back to XGW.Peer.send_datachannel_async/4).

send_rtp(peer, ssrc, packet)

@spec send_rtp(t() | pid(), non_neg_integer(), binary() | XMediaLib.Rtp.t()) ::
  :ok | {:error, term()}

Encrypts and sends one RTP packet on the hot path.

On success: no Peer or Listener GenServer involvement.