Packages

A high-performance Elixir library for managing counters with pluggable backends. Supports ETS, Erlang :atomics, and :counters modules with namespace organization.

Current section

Files

Jump to
counter_ex lib counter_ex.ex
Raw

lib/counter_ex.ex

defmodule CounterEx do
@moduledoc """
High-performance counter library with pluggable backends and namespace support.
CounterEx provides atomic counter operations with three backend options:
- **ETS** (default): Dynamic counters, unlimited capacity
- **Atomics**: Fixed capacity, ultra-fast operations
- **Counters**: Fixed capacity, write-optimized
## Quick Start
# Start the counter server (default ETS backend)
{:ok, _pid} = CounterEx.start_link()
# Increment counters
{:ok, 1} = CounterEx.inc(:my_counter)
{:ok, 2} = CounterEx.inc(:my_counter)
# Get counter value
{:ok, 2} = CounterEx.get(:my_counter)
# Use namespaces to organize counters
{:ok, 1} = CounterEx.inc(:metrics, :http_requests)
{:ok, 1} = CounterEx.inc(:metrics, :db_queries)
## Backend Selection
# Use Atomics backend for maximum performance
{:ok, _pid} = CounterEx.start_link(
backend: CounterEx.Backend.Atomics,
backend_opts: [capacity: 10_000]
)
# Use Counters backend for write-heavy workloads
{:ok, _pid} = CounterEx.start_link(
backend: CounterEx.Backend.Counters,
backend_opts: [capacity: 5_000]
)
## Namespaces
Counters can be organized into namespaces for logical grouping:
{:ok, _} = CounterEx.inc(:http, :requests)
{:ok, _} = CounterEx.inc(:http, :errors)
{:ok, _} = CounterEx.inc(:db, :queries)
# Get all counters in a namespace
{:ok, counters} = CounterEx.all(:http)
# => %{requests: 1, errors: 1}
"""
alias CounterEx.Keeper
alias CounterEx.Backend
@default_namespace :default
## Supervisor Integration
@doc """
Returns a child spec for use in supervision trees.
## Examples
children = [
{CounterEx, backend: CounterEx.Backend.ETS},
# ... other children
]
Supervisor.start_link(children, strategy: :one_for_one)
"""
def child_spec(opts) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [opts]}
}
end
@doc """
Start the CounterEx server.
## Options
- `:backend` - Backend module (default: `CounterEx.Backend.ETS`)
- `:backend_opts` - Options for backend initialization
- `:interval` - Sweep interval in milliseconds (clears all counters periodically)
- `:name` - GenServer name (default: `CounterEx.Keeper`)
## Examples
# Start with default ETS backend
{:ok, pid} = CounterEx.start_link()
# Start with Atomics backend
{:ok, pid} = CounterEx.start_link(
backend: CounterEx.Backend.Atomics,
backend_opts: [capacity: 10_000]
)
"""
@spec start_link(keyword()) :: GenServer.on_start()
def start_link(opts \\ []) do
Keeper.start_link(opts)
end
@doc """
Returns supervisor child spec for backward compatibility.
## Examples
children = [
CounterEx.start_keeper(),
# ... other children
]
"""
@spec start_keeper :: {module(), keyword()}
def start_keeper, do: {Keeper, []}
@doc """
Returns supervisor child spec with sweep interval for backward compatibility.
## Examples
children = [
CounterEx.start_keeper_with_sweep(60_000),
# ... other children
]
"""
@spec start_keeper_with_sweep(integer()) ::
{module(), keyword()} | {:error, :interval_is_not_integer}
def start_keeper_with_sweep(interval) when is_integer(interval) do
{Keeper, [interval: interval]}
end
def start_keeper_with_sweep(_), do: {:error, :interval_is_not_integer}
## Counter Operations
@doc """
Increment a counter by the given step (default: 1).
If the counter doesn't exist, it's initialized with `default` (default: 0) before incrementing.
## Examples
{:ok, 1} = CounterEx.inc(:my_counter)
{:ok, 2} = CounterEx.inc(:my_counter)
{:ok, 12} = CounterEx.inc(:my_counter, 10)
# With namespace
{:ok, 1} = CounterEx.inc(:metrics, :requests)
# With default value
{:ok, 105} = CounterEx.inc(:counter, 5, 100)
"""
@spec inc(Backend.key(), integer(), integer()) :: {:ok, integer()} | {:error, term()}
@spec inc(Backend.namespace(), Backend.key(), integer(), integer()) ::
{:ok, integer()} | {:error, term()}
def inc(key_or_namespace, step_or_key \\ 1, default_or_step \\ 0, default \\ 0)
def inc(key, step, default, _default)
when (is_atom(key) or is_binary(key)) and is_integer(step) and is_integer(default) do
Keeper.increment(@default_namespace, key, step, default)
end
def inc(namespace, key, step, default)
when (is_atom(namespace) or is_binary(namespace)) and
(is_atom(key) or is_binary(key)) and
is_integer(step) and is_integer(default) do
Keeper.increment(namespace, key, step, default)
end
@doc """
Get the current value of a counter.
Returns `{:ok, value}` if the counter exists, or `{:ok, nil}` if it doesn't.
## Examples
{:ok, nil} = CounterEx.get(:my_counter)
{:ok, _} = CounterEx.inc(:my_counter)
{:ok, 1} = CounterEx.get(:my_counter)
# With namespace
{:ok, value} = CounterEx.get(:metrics, :requests)
"""
@spec get(Backend.key()) :: {:ok, integer() | nil} | {:error, term()}
@spec get(Backend.namespace(), Backend.key()) :: {:ok, integer() | nil} | {:error, term()}
def get(key_or_namespace, key \\ nil)
def get(key, nil) when is_atom(key) or is_binary(key) do
Keeper.get_value(@default_namespace, key)
end
def get(namespace, key)
when (is_atom(namespace) or is_binary(namespace)) and
(is_atom(key) or is_binary(key)) do
Keeper.get_value(namespace, key)
end
@doc """
Set a counter to a specific value.
## Examples
{:ok, 42} = CounterEx.set(:my_counter, 42)
{:ok, 42} = CounterEx.get(:my_counter)
# With namespace
{:ok, 100} = CounterEx.set(:metrics, :requests, 100)
"""
@spec set(Backend.key(), integer()) :: {:ok, integer()} | {:error, term()}
@spec set(Backend.namespace(), Backend.key(), integer()) :: {:ok, integer()} | {:error, term()}
def set(key_or_namespace, value_or_key, value \\ nil)
def set(key, value, nil) when (is_atom(key) or is_binary(key)) and is_integer(value) do
Keeper.set(@default_namespace, key, value)
end
def set(namespace, key, value)
when (is_atom(namespace) or is_binary(namespace)) and
(is_atom(key) or is_binary(key)) and is_integer(value) do
Keeper.set(namespace, key, value)
end
@doc """
Reset a counter to the initial value (default: 0).
## Examples
{:ok, _} = CounterEx.inc(:my_counter)
{:ok, 0} = CounterEx.reset(:my_counter)
# Reset to custom value
{:ok, 10} = CounterEx.reset(:my_counter, 10)
# With namespace
{:ok, 0} = CounterEx.reset(:metrics, :requests)
"""
@spec reset(Backend.key(), integer()) :: {:ok, integer()} | {:error, term()}
@spec reset(Backend.namespace(), Backend.key(), integer()) ::
{:ok, integer()} | {:error, term()}
def reset(key_or_namespace, value_or_key \\ 0, value \\ 0)
def reset(key, value, _value) when (is_atom(key) or is_binary(key)) and is_integer(value) do
Keeper.reset(Keeper, @default_namespace, key, value)
end
def reset(namespace, key, value)
when (is_atom(namespace) or is_binary(namespace)) and
(is_atom(key) or is_binary(key)) and is_integer(value) do
Keeper.reset(Keeper, namespace, key, value)
end
@doc """
Delete a counter.
## Examples
{:ok, _} = CounterEx.inc(:my_counter)
:ok = CounterEx.delete(:my_counter)
{:ok, nil} = CounterEx.get(:my_counter)
# With namespace
:ok = CounterEx.delete(:metrics, :requests)
"""
@spec delete(Backend.key()) :: :ok | {:error, term()}
@spec delete(Backend.namespace(), Backend.key()) :: :ok | {:error, term()}
def delete(key_or_namespace, key \\ nil)
def delete(key, nil) when is_atom(key) or is_binary(key) do
Keeper.delete(@default_namespace, key)
end
def delete(namespace, key)
when (is_atom(namespace) or is_binary(namespace)) and
(is_atom(key) or is_binary(key)) do
Keeper.delete(namespace, key)
end
@doc """
Get all counters in a namespace.
Returns a map of counter keys to their values.
## Examples
{:ok, _} = CounterEx.inc(:counter1)
{:ok, _} = CounterEx.inc(:counter2)
{:ok, counters} = CounterEx.all()
# => %{counter1: 1, counter2: 1}
# With specific namespace
{:ok, counters} = CounterEx.all(:metrics)
"""
@spec all() :: {:ok, %{Backend.key() => integer()}} | {:error, term()}
@spec all(Backend.namespace()) :: {:ok, %{Backend.key() => integer()}} | {:error, term()}
def all(namespace \\ @default_namespace) do
Keeper.get_all_values(namespace)
end
@doc """
Delete all counters in a namespace.
## Examples
:ok = CounterEx.delete_namespace(:metrics)
"""
@spec delete_namespace(Backend.namespace()) :: :ok | {:error, term()}
def delete_namespace(namespace) do
Keeper.delete_namespace(namespace)
end
@doc """
Compare-and-swap operation.
Atomically updates the counter to `new_value` only if its current value equals `expected`.
Returns `{:ok, new_value}` on success, or `{:error, :mismatch, current_value}` if the
current value doesn't match the expected value.
## Examples
{:ok, 10} = CounterEx.set(:my_counter, 10)
# CAS succeeds when value matches
{:ok, 20} = CounterEx.compare_and_swap(:my_counter, 10, 20)
# CAS fails when value doesn't match
{:error, :mismatch, 20} = CounterEx.compare_and_swap(:my_counter, 10, 30)
# With namespace
{:ok, 5} = CounterEx.compare_and_swap(:metrics, :requests, 0, 5)
"""
@spec compare_and_swap(Backend.key(), integer(), integer()) ::
{:ok, integer()} | {:error, :mismatch, integer()} | {:error, term()}
@spec compare_and_swap(Backend.namespace(), Backend.key(), integer(), integer()) ::
{:ok, integer()} | {:error, :mismatch, integer()} | {:error, term()}
def compare_and_swap(key_or_namespace, expected_or_key, new_value_or_expected, new_value \\ nil)
def compare_and_swap(key, expected, new_value, nil)
when (is_atom(key) or is_binary(key)) and is_integer(expected) and is_integer(new_value) do
Keeper.compare_and_swap(@default_namespace, key, expected, new_value)
end
def compare_and_swap(namespace, key, expected, new_value)
when (is_atom(namespace) or is_binary(namespace)) and
(is_atom(key) or is_binary(key)) and
is_integer(expected) and is_integer(new_value) do
Keeper.compare_and_swap(namespace, key, expected, new_value)
end
@doc """
Get information about the backend and counters.
Returns a map with metadata including:
- Backend type (`:ets`, `:atomics`, `:counters`)
- Number of counters
- Active namespaces
- Backend-specific information
## Examples
{:ok, info} = CounterEx.info()
info.type
# => :ets
info.counters_count
# => 42
"""
@spec info() :: {:ok, map()} | {:error, term()}
def info do
Keeper.info()
end
## Benchmarking
@doc """
Run benchmarks comparing backend performance.
## Examples
CounterEx.benchmark()
CounterEx.benchmark(parallel: 4)
"""
def benchmark(opts \\ []) do
unless Code.ensure_loaded?(Benchee) do
raise """
Benchee is not available. Add {:benchee, "~> 1.3"} to your mix.exs deps to run benchmarks.
"""
end
parallel = Keyword.get(opts, :parallel, 2)
backends = [
{:ets, CounterEx.Backend.ETS, []},
{:atomics, CounterEx.Backend.Atomics, [capacity: 1000]},
{:counters, CounterEx.Backend.Counters, [capacity: 1000]}
]
Enum.each(backends, fn {name, backend, backend_opts} ->
IO.puts("\n=== Benchmarking #{name} backend ===\n")
{:ok, _pid} = CounterEx.start_link(backend: backend, backend_opts: backend_opts)
Benchee.run(
%{
"#{name}: inc" => fn -> CounterEx.inc(:test_key) end,
"#{name}: get" => fn -> CounterEx.get(:test_key) end,
"#{name}: set" => fn -> CounterEx.set(:test_key, 42) end
},
parallel: parallel,
time: 2,
warmup: 1
)
Keeper.stop()
Process.sleep(100)
end)
end
end