PhiAccrualUdp.Packet (phi_accrual_udp v0.1.0)

View Source

Wire format codec for phi_accrual_udp heartbeats.

Format (v1, 12 bytes fixed)

<<magic::16, version::8, flags::8, timestamp::64-unsigned>>
  • magic0xCEA6. Two-byte magic identifying a phi_accrual UDP heartbeat. Anything else is rejected at decode time before reaching the estimator.
  • version0x01 for this format. Receivers MUST reject unknown versions at decode time. Future versions will be parsed by additional decode/1 clauses.
  • flags — reserved, MUST be 0 in v1. Non-zero flags cause a decode error in v1; future versions may relax this.
  • timestamp — 64-bit unsigned milliseconds, sender's choice of clock. The receiver does NOT use this for the EWMA — it uses local monotonic receipt time. The packet timestamp is diagnostic only (e.g., one-way delay when NTP-synced).

Why magic + version + flags

Three bytes of overhead earn:

  • Early reject of garbage. A misconfigured sender or stray UDP packet on the listener port doesn't corrupt estimator state — it gets dropped with a [:phi_accrual_udp, :decode, :error] telemetry event.
  • Format evolution. Adding fields (sequence number, sender id, priority) means bumping version and adding a new decode/1 clause. Old senders keep working until v1 is retired.
  • Operator visibility. tcpdump -X shows cea6 01... for valid traffic — distinguishable from random UDP noise at a glance.

Clock discipline (read this)

The packet timestamp is provenance, not signal. The receiver MUST call :erlang.monotonic_time(:millisecond) at receipt time and pass that to PhiAccrual.observe/2. Using the packet timestamp for the EWMA breaks clock discipline (sender and receiver clocks are uncorrelated in general) and corrupts the detector.

Summary

Functions

Decode a heartbeat packet. Returns {:ok, %Packet{}} on success, or {:error, reason} for any malformed input.

Encode a heartbeat packet.

Wire size of a v1 packet, in bytes.

Types

t()

@type t() :: %PhiAccrualUdp.Packet{
  flags: non_neg_integer(),
  timestamp_ms: non_neg_integer(),
  version: non_neg_integer()
}

Functions

decode(bin)

@spec decode(binary()) :: {:ok, t()} | {:error, atom()}

Decode a heartbeat packet. Returns {:ok, %Packet{}} on success, or {:error, reason} for any malformed input.

Reasons:

  • :wrong_size — packet is not exactly 12 bytes
  • :bad_magic — first two bytes do not match the magic constant
  • :unsupported_version — version byte is not 1
  • :reserved_flags_set — flag bits other than 0 in v1

encode(timestamp_ms, opts \\ [])

@spec encode(
  non_neg_integer(),
  keyword()
) :: binary()

Encode a heartbeat packet.

timestamp_ms is whatever the sender wants to record for diagnostic purposes — typically :erlang.system_time(:millisecond) if NTP-synced operators want one-way delay, or :erlang.monotonic_time(:millisecond) if local correlation is enough. Receivers do not use this field for the EWMA.

size()

@spec size() :: pos_integer()

Wire size of a v1 packet, in bytes.