Packages

A distributed cache library for Elixir with pluggable backends, topologies, and near-cache support.

Current section

Files

Jump to
vela_cache lib vela stats collector.ex
Raw

lib/vela/stats/collector.ex

defmodule Vela.Stats.Collector do
@moduledoc """
Tracks cache statistics using lock-free atomic counters.
The hot path (every get/put) increments counters directly using
the :counters module — no GenServer message needed. This GenServer
only wakes periodically to emit a telemetry summary.
"""
use GenServer
# Counter indices
@hits 1
@misses 2
@writes 3
@deletes 4
@evictions 5
@errors 6
@num_counters 6
@flush_interval 60_000
# ---- Public API ----
def start_link(opts) do
cache_name = Keyword.fetch!(opts, :name)
GenServer.start_link(__MODULE__, opts, name: collector_name(cache_name))
end
def collector_name(cache_name), do: :"vela_stats_#{cache_name}"
@doc "Increment the hits counter. Called from the hot path — no GenServer involved."
def incr_hits(cache_name), do: incr(cache_name, @hits)
def incr_misses(cache_name), do: incr(cache_name, @misses)
def incr_writes(cache_name), do: incr(cache_name, @writes)
def incr_deletes(cache_name), do: incr(cache_name, @deletes)
def incr_evictions(cache_name), do: incr(cache_name, @evictions)
def incr_errors(cache_name), do: incr(cache_name, @errors)
@doc "Return current stats as a map."
def stats(cache_name) do
ref = get_ref(cache_name)
%{
hits: :counters.get(ref, @hits),
misses: :counters.get(ref, @misses),
writes: :counters.get(ref, @writes),
deletes: :counters.get(ref, @deletes),
evictions: :counters.get(ref, @evictions),
errors: :counters.get(ref, @errors)
}
end
# ---- GenServer Callbacks ----
@impl true
def init(opts) do
config = Keyword.fetch!(opts, :config)
ref = :counters.new(@num_counters, [:atomics])
# Store the ref in persistent_term so callers can access it
# without going through this GenServer
:persistent_term.put({:vela_stats_ref, config.name}, ref)
Process.send_after(self(), :flush, @flush_interval)
{:ok, %{config: config, ref: ref, window_start: System.monotonic_time()}}
end
@impl true
def handle_info(:flush, state) do
s = stats(state.config.name)
duration = System.monotonic_time() - state.window_start
# Emit a telemetry snapshot — dashboards/exporters can compute rates
# from successive snapshots. Counters are cumulative and never reset,
# so stats/1 always returns lifetime totals.
:telemetry.execute(
state.config.telemetry_prefix ++ [:stats, :flush],
Map.put(s, :window_duration, duration),
%{cache: state.config.name}
)
Process.send_after(self(), :flush, @flush_interval)
{:noreply, %{state | window_start: System.monotonic_time()}}
end
# ---- Private ----
defp incr(cache_name, index) do
ref = get_ref(cache_name)
:counters.add(ref, index, 1)
end
defp get_ref(cache_name) do
:persistent_term.get({:vela_stats_ref, cache_name})
end
end