Packages

A distributed unique ID generator inspired by Twitter's Snowflake, rewritten in elixir

Current section

Files

Jump to
sonyflake lib sonyflake.ex
Raw

lib/sonyflake.ex

defmodule Sonyflake do
@moduledoc """
Documentation for `Sonyflake`.
"""
import Bitwise
@enforce_keys [:machine_id, :start_time, :sequence]
defstruct [:machine_id, :start_time, elapsed_time: 0, msb: 0, sequence: 0]
@spec new :: %Sonyflake{
elapsed_time: 0,
machine_id: non_neg_integer,
msb: 0,
sequence: 255,
start_time: non_neg_integer
}
@doc """
Create an instance of `Sonyflake` unique ID generator.
"""
def new() do
setting = Sonyflake.Setting.new()
%Sonyflake{
sequence: (1 <<< Sonyflake.Constants.bit_len_sequence()) - 1,
machine_id: setting.machine_id,
start_time: setting.start_time
}
end
@spec new(%Sonyflake.Setting{
start_time: non_neg_integer,
machine_id: non_neg_integer
}) ::
nil
| %Sonyflake{
elapsed_time: 0,
machine_id: non_neg_integer,
msb: 0,
sequence: 255,
start_time: non_neg_integer
}
@doc """
Create an instance of `Sonyflake` unique ID generator, using a predetermined
setting.
"""
def new(setting) do
if is_nil(setting),
do: nil,
else: %Sonyflake{
sequence: (1 <<< Sonyflake.Constants.bit_len_sequence()) - 1,
machine_id: setting.machine_id,
start_time: setting.start_time
}
end
@spec current_elapsed_time(%Sonyflake{start_time: non_neg_integer}) :: non_neg_integer
@doc """
Get time elapsed since the SonyFlake ID generator was initialised.
"""
def current_elapsed_time(%Sonyflake{start_time: start_time}) do
(Sonyflake.Utils.to_sonyflake_time(DateTime.utc_now()) - start_time) |> trunc()
end
@spec next_id(%Sonyflake{
elapsed_time: non_neg_integer,
machine_id: non_neg_integer,
msb: non_neg_integer,
sequence: non_neg_integer,
start_time: non_neg_integer
}) ::
{:ok,
%Sonyflake{
elapsed_time: non_neg_integer,
machine_id: non_neg_integer,
msb: non_neg_integer,
sequence: non_neg_integer,
start_time: non_neg_integer
}, non_neg_integer}
@doc """
Generates and returns the next unique ID.
Raises a `TimeoutError` after the `SonyFlake` time overflows.
"""
def next_id(sonyflake) do
mask_sequence = (1 <<< Sonyflake.Constants.bit_len_sequence()) - 1
current_time = current_elapsed_time(sonyflake)
sonyflake =
if sonyflake.elapsed_time < current_time do
%Sonyflake{sonyflake | elapsed_time: current_time, sequence: 0}
else
sequence = sonyflake.sequence + 1 &&& mask_sequence
temp =
if sequence == 0,
do:
(
new_elapsed_time = sonyflake.elapsed_time + 1
overtime = new_elapsed_time - current_time
Process.sleep(overtime)
%Sonyflake{sonyflake | sequence: sequence, elapsed_time: new_elapsed_time}
)
temp
end
{:ok, sonyflake, to_id(sonyflake)}
end
defp to_id(%Sonyflake{elapsed_time: elapsed_time, sequence: sequence, machine_id: machine_id}) do
time =
elapsed_time <<<
(Sonyflake.Constants.bit_len_sequence() + Sonyflake.Constants.bit_len_machine_id())
sequence = sequence <<< Sonyflake.Constants.bit_len_machine_id()
time ||| sequence ||| machine_id
end
@spec decompose(non_neg_integer) :: [
{:id, non_neg_integer}
| {:machine_id, non_neg_integer}
| {:msb, non_neg_integer}
| {:sequence, non_neg_integer}
| {:time, non_neg_integer},
...
]
@doc """
Decompose a generated sonyflake id back to its components.
"""
def decompose(id) when is_number(id) do
mask_sequence =
((1 <<< Sonyflake.Constants.bit_len_sequence()) - 1) <<<
Sonyflake.Constants.bit_len_machine_id()
mask_machine_id = (1 <<< Sonyflake.Constants.bit_len_machine_id()) - 1
msb = id >>> 63
time =
id >>> (Sonyflake.Constants.bit_len_sequence() + Sonyflake.Constants.bit_len_machine_id())
sequence = (id &&& mask_sequence) >>> Sonyflake.Constants.bit_len_machine_id()
machine_id = id &&& mask_machine_id
[id: id, msb: msb, time: time, sequence: sequence, machine_id: machine_id]
end
end