PeerNet.Liveness (PeerNet v0.1.0)

Copy Markdown View Source

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:

  1. Generates a fresh 16-byte nonce.
  2. Calls :emit with the nonce — the caller is expected to send a {:ping, nonce} envelope to the peer.
  3. Schedules a :check after :timeout_ms.

When :check fires:

  • If handle_pong/2 was called with the matching nonce → the peer is alive, schedule the next ping (interval_ms after 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

opts()

@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

child_spec(init_arg)

Returns a specification to start this module under a supervisor.

See Supervisor.

handle_pong(server, nonce)

@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.

start_link(opts)

@spec start_link(opts()) :: GenServer.on_start()