62 lines
1.7 KiB
Elixir
62 lines
1.7 KiB
Elixir
defmodule Nulla.Snowflake do
|
|
use GenServer
|
|
import Bitwise
|
|
|
|
@epoch :calendar.datetime_to_gregorian_seconds({{2020, 1, 1}, {0, 0, 0}}) * 1000
|
|
@max_sequence 4095
|
|
@time_shift 22
|
|
@worker_shift 12
|
|
|
|
def start_link(worker_id) when worker_id in 0..1023 do
|
|
GenServer.start_link(__MODULE__, worker_id, name: __MODULE__)
|
|
end
|
|
|
|
def next_id do
|
|
GenServer.call(__MODULE__, :next_id)
|
|
end
|
|
|
|
@impl true
|
|
def init(worker_id) do
|
|
{:ok, %{last_timestamp: -1, sequence: 0, worker_id: worker_id}}
|
|
end
|
|
|
|
@impl true
|
|
def handle_call(:next_id, _from, %{worker_id: worker_id} = state) do
|
|
timestamp = current_time()
|
|
|
|
{timestamp, sequence, state} =
|
|
cond do
|
|
timestamp < state.last_timestamp ->
|
|
raise "Clock moved backwards"
|
|
|
|
timestamp == state.last_timestamp and state.sequence < @max_sequence ->
|
|
{timestamp, state.sequence + 1, %{state | sequence: state.sequence + 1}}
|
|
|
|
timestamp == state.last_timestamp ->
|
|
wait_for_next_millisecond(timestamp)
|
|
new_timestamp = current_time()
|
|
{new_timestamp, 0, %{state | last_timestamp: new_timestamp, sequence: 0}}
|
|
|
|
true ->
|
|
{timestamp, 0, %{state | last_timestamp: timestamp, sequence: 0}}
|
|
end
|
|
|
|
raw_id =
|
|
((timestamp - @epoch) <<< @time_shift)
|
|
|> bor(worker_id <<< @worker_shift)
|
|
|> bor(sequence)
|
|
|
|
id = Bitwise.band(raw_id, 0x7FFFFFFFFFFFFFFF)
|
|
|
|
{:reply, id, %{state | last_timestamp: timestamp, sequence: sequence}}
|
|
end
|
|
|
|
defp current_time do
|
|
System.system_time(:millisecond)
|
|
end
|
|
|
|
defp wait_for_next_millisecond(last_ts) do
|
|
:timer.sleep(1)
|
|
if current_time() <= last_ts, do: wait_for_next_millisecond(last_ts), else: :ok
|
|
end
|
|
end
|