Application-level heartbeat for one peer connection.
TCP keepalive is OS-level and slow (default ~2 hours). For peer-to-peer
apps that need to detect a dropped peer in seconds rather than hours, we
layer a short-period ping/pong on top. Each Liveness process owns the
cadence for one connection; the connection is responsible for actually
transmitting the ping nonce and routing the pong back.
Wiring
# Start one Liveness per Connection, supplying the emit + on_dead callbacks.
{:ok, lv} = Liveness.start_link(
interval_ms: 30_000,
timeout_ms: 60_000,
emit: fn nonce -> Connection.send_ping(self(), nonce) end,
on_dead: fn -> Connection.peer_died(self()) end
)
# When the connection receives a {:pong, nonce} envelope:
Liveness.handle_pong(lv, nonce)Cadence
Every :interval_ms the Liveness:
- Generates a fresh 16-byte nonce.
- Calls
:emitwith the nonce — the caller is expected to send a{:ping, nonce}envelope to the peer. - Schedules a
:checkafter:timeout_ms.
When :check fires:
- If
handle_pong/2was called with the matching nonce → the peer is alive, schedule the next ping (interval_msafter the previous one was emitted, not after the pong arrived — keeps cadence steady). - Otherwise → call
:on_dead. The Liveness then stays idle until told otherwise; the connection is expected to terminate.
Summary
Types
Options accepted by start_link/1.
Functions
Returns a specification to start this module under a supervisor.
Tell the liveness process that a :pong came back with nonce.
Types
@type opts() :: [ interval_ms: pos_integer(), timeout_ms: pos_integer(), emit: (binary() -> any()), on_dead: (-> any()), name: GenServer.name() ]
Options accepted by start_link/1.
Functions
Returns a specification to start this module under a supervisor.
See Supervisor.
@spec handle_pong(GenServer.server(), binary()) :: :ok
Tell the liveness process that a :pong came back with nonce.
Mismatched nonces are silently ignored — they're either late acks of earlier pings (already replaced by the current outstanding nonce) or a peer misbehaving. Either way they shouldn't reset the dead-detection window.
@spec start_link(opts()) :: GenServer.on_start()