AuroraMeter. Counter
(Aurora Meter v0.3.1)
View Source
The hot path: atomic ETS counter operations.
Increments use a single :ets.update_counter/3 call (lock-free,
concurrency-safe). A cold key is seeded once from the last flushed database
value so the in-ETS value is always absolute; after that first touch the path
is pure ETS. Every mutation marks the key dirty (for the flusher) and touched
(for the broadcaster).
Each row is {key, value, pending_flush, pending_gossip}:
valueis this node's view of the cluster-wide totalpending_flushis what this node has added since its last database flushpending_gossipis what this node has added since its last PubSub tick
The flusher writes pending_flush as a delta (value = value + Δ), so
nodes add up instead of overwriting one another, then re-bases value on
the total the database returns. The broadcaster ships pending_gossip to
the other nodes so their views converge within one tick. See
AuroraMeter.Cluster and the clustering guide.
Two key shapes share the table:
{tenant_key, feature, period_start}— the billing-period counter that entitlements and reporting read{tenant_key, feature, {:day, date}}— a UTC day bucket, maintained alongside the period counter when:historyis enabled, feedingAuroraMeter.history/3
Summary
Types
A history counter key: {tenant_key, feature, {:day, date}}.
Any counter key.
Which pending column to take: the database flush or the PubSub gossip.
A period counter key: {tenant_key, feature, period_start}.
Functions
Returns a map of feature => value for a tenant's warm period counters in a period.
Applies a delta received from another node to this node's view. Only the value moves: the delta is not ours to flush or re-gossip. The key is marked touched so local LiveViews see the change. Cold keys are skipped (they seed from the database on first read, which already contains every flushed delta).
This node's base for a key: what it believes the database holds (value - pending_flush).
Removes a single key from the dirty set.
Removes a single key from the touched set.
Returns the current value of a UTC day bucket (rehydrating from the database if cold).
Snapshots the current set of dirty keys.
Whether a key is a history (day bucket) key.
Increments a period counter by qty and returns the new value.
Marks a counter key dirty (pending flush) and touched (pending broadcast).
Re-bases this node's view on an authoritative database total: value
becomes total + pending_flush. Applied as a delta against a snapshot, so a
bump that lands mid-rebase is kept exactly. Cold keys are skipped.
Releases a previously reserved qty (rollback on a raised function).
Atomically reserves qty against an optional hard limit.
Puts a taken flush delta back (the database write failed) and re-marks the key dirty so the next flush retries it.
Atomically takes and zeroes a pending column, returning the delta accumulated
since the last take. Concurrent bumps between the read and the zeroing are
preserved (the column is decremented by the amount read, not set to zero).
Cold keys yield 0.
Snapshots the current set of touched keys.
Returns the current value for a period counter (rehydrating from the database if cold).
Returns date => value for a feature's warm day buckets (no database access).
Types
A history counter key: {tenant_key, feature, {:day, date}}.
@type key() :: period_key() | day_key()
Any counter key.
@type pending() :: :flush | :gossip
Which pending column to take: the database flush or the PubSub gossip.
@type period_key() :: {String.t(), atom(), DateTime.t()}
A period counter key: {tenant_key, feature, period_start}.
Functions
@spec all_for(String.t(), DateTime.t()) :: %{required(atom()) => integer()}
Returns a map of feature => value for a tenant's warm period counters in a period.
Applies a delta received from another node to this node's view. Only the value moves: the delta is not ours to flush or re-gossip. The key is marked touched so local LiveViews see the change. Cold keys are skipped (they seed from the database on first read, which already contains every flushed delta).
This node's base for a key: what it believes the database holds (value - pending_flush).
@spec clear_dirty(key()) :: :ok
Removes a single key from the dirty set.
@spec clear_touched(key()) :: :ok
Removes a single key from the touched set.
Returns the current value of a UTC day bucket (rehydrating from the database if cold).
@spec dirty_keys() :: [key()]
Snapshots the current set of dirty keys.
Whether a key is a history (day bucket) key.
@spec incr(String.t(), atom(), integer(), DateTime.t()) :: integer()
Increments a period counter by qty and returns the new value.
@spec mark_dirty(key()) :: :ok
Marks a counter key dirty (pending flush) and touched (pending broadcast).
Re-bases this node's view on an authoritative database total: value
becomes total + pending_flush. Applied as a delta against a snapshot, so a
bump that lands mid-rebase is kept exactly. Cold keys are skipped.
@spec release(String.t(), atom(), integer(), DateTime.t()) :: :ok
Releases a previously reserved qty (rollback on a raised function).
@spec reserve(String.t(), atom(), integer(), DateTime.t(), non_neg_integer() | nil) :: :ok | {:error, :limit_exceeded}
Atomically reserves qty against an optional hard limit.
Increments first; if the new value exceeds limit it rolls the increment back
and returns {:error, :limit_exceeded}. A nil limit always succeeds.
Puts a taken flush delta back (the database write failed) and re-marks the key dirty so the next flush retries it.
Atomically takes and zeroes a pending column, returning the delta accumulated
since the last take. Concurrent bumps between the read and the zeroing are
preserved (the column is decremented by the amount read, not set to zero).
Cold keys yield 0.
@spec touched_keys() :: [key()]
Snapshots the current set of touched keys.
@spec value(String.t(), atom(), DateTime.t()) :: integer()
Returns the current value for a period counter (rehydrating from the database if cold).
Returns date => value for a feature's warm day buckets (no database access).