PhiAccrual.Core (phi_accrual v1.0.0)

View Source

Pure EWMA φ-accrual math. No processes, no side effects.

State evolves via observe/2 (fed the arrival timestamp of a new heartbeat) and is queried via phi/2 (computes φ for the elapsed time since the last arrival).

The estimator follows Hayashibara 2004, with West 1979 incremental variance and separate α for mean and variance (dual-α EWMA):

delta      = sample - mean
mean'      = mean + α_mean * delta
variance'  = (1 - α_var) * (variance + α_var * delta²)

Variance typically needs more smoothing than mean — otherwise a single anomalous sample doubles variance and craters φ. Defaults keep both α equal; tune alpha_var smaller than alpha_mean under bursty input.

Summary

Functions

Build fresh estimator state. Accepts any struct field as a keyword override, plus :initial_interval_ms / :initial_std_dev_ms which seed mean and variance before the first sample lands.

Record a heartbeat arrival at local monotonic-ms timestamp ts.

Compute φ given state and current monotonic-ms timestamp.

Types

phi_result()

@type phi_result() ::
  {:ok, float(), :steady}
  | {:ok, float(), :recovering}
  | {:insufficient_data, pos_integer()}
  | {:stale, non_neg_integer()}

t()

@type t() :: %PhiAccrual.Core{
  alpha_mean: float(),
  alpha_var: float(),
  last_arrival_ts: integer() | nil,
  last_interval_ms: float() | nil,
  mean: float(),
  min_samples: pos_integer(),
  min_std_dev_ms: float(),
  recovering_grace_samples: non_neg_integer(),
  recovering_remaining: non_neg_integer(),
  recovering_threshold_ms: pos_integer(),
  samples_seen: non_neg_integer(),
  stale_after_ms: pos_integer(),
  variance: float()
}

Functions

new(opts \\ [])

@spec new(keyword()) :: t()

Build fresh estimator state. Accepts any struct field as a keyword override, plus :initial_interval_ms / :initial_std_dev_ms which seed mean and variance before the first sample lands.

observe(state, ts)

@spec observe(t(), integer()) :: t()

Record a heartbeat arrival at local monotonic-ms timestamp ts.

First call seeds last_arrival_ts without updating mean/variance — an EWMA update needs two timestamps to derive an interval.

phi(state, now)

@spec phi(t(), integer()) :: phi_result()

Compute φ given state and current monotonic-ms timestamp.

Returns one of four states per the v1 contract:

  • {:ok, phi, :steady} — warm estimator, normal operation
  • {:ok, phi, :recovering} — warm estimator, still absorbing a recent gap
  • {:insufficient_data, n} — bootstrap phase, n samples remaining
  • {:stale, elapsed_ms} — no arrival for longer than stale_after_ms