Skip to main content

Module metrics

Module metrics 

Source
Expand description

Lock-free metrics for BEAM actor send/fanout observability.

§Why this exists

Before this module, the Router (and other actors) used let _ = addr.send(msg) everywhere to dispatch messages. When an actor’s mailbox was full or closed, Addr::send returned Err(()) and the codebase silently dropped it.

Silent drops are dangerous because:

  1. Invisible failures: a “successful” Node::put might never reach the storage adapter if its mailbox is full. The caller has no signal.
  2. No debugging trail: when data doesn’t replicate, there’s no counter showing “1000 Puts were silently dropped” — the operator sees a working system that silently lost data.
  3. No backpressure visibility: a slow consumer looks identical to a fast one until messages start disappearing into the void.

This module fixes the observability gap without changing behavior. Metrics is a tiny, lock-free counter struct that any actor can hold (via ActorContext or directly) to record events of interest.

§Hot-path instrumentation

In addition to the original drop/ack counters, this module now tracks the relay hot path — the sequence every message traverses from WebSocket receive to WebSocket send. These counters let us identify the load-bearing component when throughput is bottlenecked:

  1. messages_parsed — JSON parse entry (Message::try_from)
  2. messages_dropped_dup — dedup gate hit (Dup::check returned true)
  3. messages_relayed — successful relay fan-out (handle_put_relay)
  4. subscriber_fanout_total — total subscriber deliveries across all relays
  5. serialization_calls — wire-format serialization (Message::to_string)
  6. ws_messages_received — inbound WebSocket frames
  7. ws_messages_sent — outbound WebSocket frames

§Design principles

  • Lock-free: all counters are AtomicU64 with Relaxed ordering. They are advisory observation, not synchronization primitives. Snapshot reads may be slightly stale but are guaranteed monotonic.
  • Cumulative: counters only increase for the lifetime of the Metrics instance. There is no “reset” — if you need per-window counters, create a new Metrics.
  • Composition-Root IoC: Metrics is passed into actors that need it, not accessed from a global registry. This makes the dependency graph explicit and testable.
  • No behavior change: recording a metric is a side effect that does not affect control flow. Existing fire-and-forget semantics are preserved.
  • Negligible cost: Relaxed atomic increments are a single LOCK instruction on x86 (~1-2 ns). At 100k TPS the total overhead is ~0.2 ms/s — well within the noise floor of any benchmark.

§Usage

use beam::metrics::Metrics;
use std::sync::Arc;

let metrics = Arc::new(Metrics::new());

// Record a drop
metrics.record_dropped_send();

// Snapshot for telemetry
let snap = metrics.snapshot();
println!("dropped_sends = {}", snap.dropped_sends);
println!("messages_parsed = {}", snap.messages_parsed);

Structs§

Metrics
Lock-free counters for BEAM actor sends, drops, and hot-path throughput.
MetricsSnapshot
Plain-old-data snapshot of Metrics for safe export across threads.