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

@ -22,6 +22,10 @@ config :nulla, NullaWeb.Endpoint,
pubsub_server: Nulla.PubSub, pubsub_server: Nulla.PubSub,
live_view: [signing_salt: "jcAt5/U+"] live_view: [signing_salt: "jcAt5/U+"]
# Snowflake configuration
config :nulla, :snowflake,
worker_id: 1
# Configures the mailer # Configures the mailer
# #
# By default it uses the "Local" adapter which stores the emails # By default it uses the "Local" adapter which stores the emails

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -2,8 +2,10 @@ defmodule Nulla.Models.User do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
alias Nulla.Repo alias Nulla.Repo
alias Nulla.Snowflake
alias Nulla.Models.User alias Nulla.Models.User
@primary_key {:id, :integer, autogenerate: false}
schema "users" do schema "users" do
field :username, :string field :username, :string
field :email, :string field :email, :string
@ -77,6 +79,15 @@ defmodule Nulla.Models.User do
]) ])
end 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)
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

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:instance_settings) do create table(:instance_settings, primary_key: false) do
add :id, :integer, primary_key: true
add :name, :string, default: "Nulla", null: false add :name, :string, default: "Nulla", null: false
add :description, :text, default: "Freedom Social Network", null: false add :description, :text, default: "Freedom Social Network", null: false
add :domain, :string, default: "localhost", null: false add :domain, :string, default: "localhost", null: false
@ -16,6 +17,8 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do
timestamps() timestamps()
end end
execute "ALTER TABLE instance_settings ADD CONSTRAINT single_row CHECK (id = 1);"
flush() flush()
execute(fn -> execute(fn ->
@ -31,11 +34,11 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do
sql = """ sql = """
INSERT INTO instance_settings ( INSERT INTO instance_settings (
name, description, domain, registration, id, name, description, domain, registration,
max_characters, max_upload_size, api_offset, max_characters, max_upload_size, api_offset,
public_key, private_key, inserted_at, updated_at public_key, private_key, inserted_at, updated_at
) VALUES ( ) VALUES (
'Nulla', 'Freedom Social Network', '#{domain}', false, 1, 'Nulla', 'Freedom Social Network', '#{domain}', false,
5000, 50, 100, 5000, 50, 100,
#{esc.(public_key)}, #{esc.(private_key)}, #{esc.(public_key)}, #{esc.(private_key)},
'#{now}', '#{now}' '#{now}', '#{now}'

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateUsers do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:users) do create table(:users, primary_key: false) do
add :id, :bigint, primary_key: true
add :username, :string, null: false, unique: true add :username, :string, null: false, unique: true
add :email, :string add :email, :string
add :password, :string add :password, :string

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateNotes do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:notes) do create table(:notes, primary_key: false) do
add :id, :bigint, primary_key: true
add :content, :text add :content, :text
add :visibility, :string, default: "public" add :visibility, :string, default: "public"
add :sensitive, :boolean, default: false add :sensitive, :boolean, default: false

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateBookmarks do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:bookmarks) do create table(:bookmarks, primary_key: false) do
add :id, :bigint, primary_key: true
add :url, :string add :url, :string
add :user_id, references(:users, on_delete: :delete_all) add :user_id, references(:users, on_delete: :delete_all)

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateNotifications do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:notifications) do create table(:notifications, primary_key: false) do
add :id, :bigint, primary_key: true
add :user_id, references(:users, on_delete: :delete_all), null: false add :user_id, references(:users, on_delete: :delete_all), null: false
add :actor_id, references(:users, on_delete: :nilify_all) add :actor_id, references(:users, on_delete: :nilify_all)
add :type, :string, null: false add :type, :string, null: false

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateModerationLogs do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:moderation_logs) do create table(:moderation_logs, primary_key: false) do
add :id, :bigint, primary_key: true
add :moderator_id, references(:users, on_delete: :nilify_all), null: false add :moderator_id, references(:users, on_delete: :nilify_all), null: false
add :target_type, :string, null: false add :target_type, :string, null: false
add :target_id, :string, null: false add :target_id, :string, null: false

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateHashtags do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:hashtags) do create table(:hashtags, primary_key: false) do
add :id, :bigint, primary_key: true
add :tag, :string, null: false add :tag, :string, null: false
add :usage_count, :integer, default: 0, null: false add :usage_count, :integer, default: 0, null: false

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateFollows do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:follows) do create table(:follows, primary_key: false) do
add :id, :bigint, primary_key: true
add :user_id, references(:users, on_delete: :delete_all), null: false add :user_id, references(:users, on_delete: :delete_all), null: false
add :target_id, references(:users, on_delete: :delete_all), null: false add :target_id, references(:users, on_delete: :delete_all), null: false

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateSessions do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:sessions) do create table(:sessions, primary_key: false) do
add :id, :bigint, primary_key: true
add :user_id, references(:users, on_delete: :delete_all), null: false add :user_id, references(:users, on_delete: :delete_all), null: false
add :token, :string, null: false add :token, :string, null: false
add :user_agent, :string add :user_agent, :string

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateMediaAttachments do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:media_attachments) do create table(:media_attachments, primary_key: false) do
add :id, :bigint, primary_key: true
add :note_id, references(:notes, on_delete: :delete_all), null: false add :note_id, references(:notes, on_delete: :delete_all), null: false
add :file, :string, null: false add :file, :string, null: false
add :mime_type, :string add :mime_type, :string

View file

@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateActivities do
use Ecto.Migration use Ecto.Migration
def change do def change do
create table(:activities) do create table(:activities, primary_key: false) do
add :id, :bigint, primary_key: true
add :type, :string, null: false add :type, :string, null: false
add :actor, :string, null: false add :actor, :string, null: false
add :object, :map, null: false add :object, :map, null: false