Current section

Files

Jump to
snowflakeid_ex lib snowflake_id helper.ex
Raw

lib/snowflake_id/helper.ex

defmodule SnowflakeID.Helper do
@moduledoc """
Utility functions intended for SnowflakeID application.
epoch() and machine_id() are useful for inspecting in production.
"""
@id_bit_size 64
@sequence_bits 12
@min_timestamp_bits 41
@max_timestamp_bits @id_bit_size - @sequence_bits - 1
@default_config [
nodes: [],
epoch: 0,
timestamp_bits: 42
]
@doc """
Grabs epoch from config value
"""
@spec epoch() :: integer
def epoch() do
Application.get_env(:snowflakeid_ex, :epoch) || @default_config[:epoch]
end
@doc """
Grabs timestamp bit size with a minimum of #{@min_timestamp_bits} bits
"""
@spec timestamp_bits() :: pos_integer
def timestamp_bits() do
Application.get_env(:snowflakeid_ex, :timestamp_bits, @default_config[:timestamp_bits])
|> clamp_timestamp_bits()
end
@doc """
Returns how many machine id bits remain after reserving timestamp and sequence bits.
"""
@spec machine_id_bits(pos_integer) :: pos_integer
def machine_id_bits(timestamp_bits) do
normalized_bits = clamp_timestamp_bits(timestamp_bits)
remaining = @id_bit_size - @sequence_bits - normalized_bits
max(remaining, 1)
end
@doc """
Computes the maximum machine id value based on the configured timestamp bits.
"""
@spec machine_id_max(pos_integer) :: non_neg_integer
def machine_id_max(timestamp_bits) do
bits = machine_id_bits(timestamp_bits)
Integer.pow(2, bits) - 1
end
@doc false
def clamp_timestamp_bits(bits) when is_integer(bits) do
cond do
bits < @min_timestamp_bits -> @min_timestamp_bits
bits > @max_timestamp_bits -> @max_timestamp_bits
true -> bits
end
end
def clamp_timestamp_bits(_bits), do: clamp_timestamp_bits(@default_config[:timestamp_bits])
@doc """
Returns the machine id of the current node, constrained to the bits implied by
the configured timestamp size. When no explicit machine id is configured, the
helper inspects the hostname, fqdn, Node name, and IPs to find a match in the
configured nodes list.
"""
@spec machine_id(pos_integer | nil) :: integer
def machine_id(timestamp_bits \\ timestamp_bits()) do
normalized_bits = clamp_timestamp_bits(timestamp_bits)
max_machine_id = machine_id_max(normalized_bits)
configured_id = Application.get_env(:snowflakeid_ex, :machine_id)
machine_id(configured_id, normalized_bits, max_machine_id)
end
defp machine_id(nil, _timestamp_bits, max_machine_id) do
nodes = Application.get_env(:snowflakeid_ex, :nodes, @default_config[:nodes])
host_addrs = [hostname(), fqdn(), Node.self()] ++ ip_addrs()
with [matching_node] <-
MapSet.intersection(MapSet.new(host_addrs), MapSet.new(nodes)) |> Enum.take(1),
idx when is_integer(idx) <- Enum.find_index(nodes, fn node -> node == matching_node end),
true <- idx <= max_machine_id do
idx
else
_ -> max_machine_id
end
end
defp machine_id(id, _timestamp_bits, max_machine_id)
when is_integer(id) and id >= 0 and id <= max_machine_id,
do: id
defp machine_id(id, _timestamp_bits, max_machine_id) when is_integer(id), do: max_machine_id
defp machine_id(_id, timestamp_bits, max_machine_id),
do: machine_id(nil, timestamp_bits, max_machine_id)
defp ip_addrs() do
case :inet.getifaddrs() do
{:ok, ifaddrs} ->
ifaddrs
|> Enum.flat_map(fn {_, kwlist} ->
kwlist |> Enum.filter(fn {type, _} -> type == :addr end)
end)
|> Enum.filter(&(tuple_size(elem(&1, 1)) in [4, 6]))
|> Enum.map(fn {_, addr} ->
case addr do
# ipv4
{a, b, c, d} -> [a, b, c, d] |> Enum.join(".")
# ipv6
{a, b, c, d, e, f} -> [a, b, c, d, e, f] |> Enum.join(":")
end
end)
_ ->
[]
end
end
defp hostname() do
{:ok, name} = :inet.gethostname()
to_string(name)
end
defp fqdn() do
case :inet.get_rc()[:domain] do
nil -> nil
domain -> hostname() <> "." <> to_string(domain)
end
end
end