nulla/lib/nulla/snowflake.ex
2025-06-13 15:10:17 +02:00

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