Add snowflake IDs

This commit is contained in:
Mirai Kumiko 2025-06-13 15:10:17 +02:00
parent 4a890a39f4
commit 44b484de21
Signed by: miraikumiko
GPG key ID: 3F178B1B5E0CB278
24 changed files with 116 additions and 14 deletions

View file

@ -7,6 +7,8 @@ defmodule Nulla.Application do
@impl true
def start(_type, _args) do
worker_id = Application.fetch_env!(:nulla, :snowflake)[:worker_id]
children = [
NullaWeb.Telemetry,
Nulla.Repo,
@ -17,7 +19,8 @@ defmodule Nulla.Application do
# Start a worker by calling: Nulla.Worker.start_link(arg)
# {Nulla.Worker, arg},
# Start to serve requests, typically the last entry
NullaWeb.Endpoint
NullaWeb.Endpoint,
{Nulla.Snowflake, worker_id}
]
# See https://hexdocs.pm/elixir/Supervisor.html

View file

@ -2,6 +2,7 @@ defmodule Nulla.Models.Activity do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :integer, autogenerate: false}
schema "activities" do
field :type, :string
field :actor, :string

View file

@ -5,6 +5,7 @@ defmodule Nulla.Models.Bookmark do
alias Nulla.Repo
alias Nulla.Models.Bookmark
@primary_key {:id, :integer, autogenerate: false}
schema "bookmarks" do
field :url, :string
field :user_id, :id

View file

@ -2,6 +2,7 @@ defmodule Nulla.Models.Follow do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :integer, autogenerate: false}
schema "follows" do
belongs_to :user, Nulla.Models.User
belongs_to :target, Nulla.Models.User

View file

@ -2,6 +2,7 @@ defmodule Nulla.Models.Hashtag do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :integer, autogenerate: false}
schema "hashtags" do
field :tag, :string
field :usage_count, :integer, default: 0

View file

@ -2,6 +2,7 @@ defmodule Nulla.Models.MediaAttachment do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :integer, autogenerate: false}
schema "media_attachments" do
field :file, :string
field :mime_type, :string

View file

@ -2,6 +2,7 @@ defmodule Nulla.Models.ModerationLog do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :integer, autogenerate: false}
schema "moderation_logs" do
field :target_type, :string
field :target_id, :string

View file

@ -5,6 +5,7 @@ defmodule Nulla.Models.Note do
alias Nulla.Repo
alias Nulla.Models.Note
@primary_key {:id, :integer, autogenerate: false}
schema "notes" do
field :content, :string

View file

@ -2,6 +2,7 @@ defmodule Nulla.Models.Notification do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :integer, autogenerate: false}
schema "notifications" do
field :type, :string
field :data, :map

View file

@ -2,6 +2,7 @@ defmodule Nulla.Models.Session do
use Ecto.Schema
import Ecto.Changeset
@primary_key {:id, :integer, autogenerate: false}
schema "sessions" do
field :token, :string
field :user_agent, :string

View file

@ -2,8 +2,10 @@ defmodule Nulla.Models.User do
use Ecto.Schema
import Ecto.Changeset
alias Nulla.Repo
alias Nulla.Snowflake
alias Nulla.Models.User
@primary_key {:id, :integer, autogenerate: false}
schema "users" do
field :username, :string
field :email, :string
@ -77,6 +79,15 @@ defmodule Nulla.Models.User do
])
end
def create_user(attrs) do
id = Snowflake.next_id()
%User{}
|> User.changeset(attrs)
|> Ecto.Changeset.put_change(:id, id)
|> Repo.insert()
end
def get_user_by_username(username), do: Repo.get_by(User, username: username)
def get_user_by_username!(username), do: Repo.get_by!(User, username: username)

62
lib/nulla/snowflake.ex Normal file
View file

@ -0,0 +1,62 @@
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