From 44b484de216343beb373bef68fa52102537ccf12 Mon Sep 17 00:00:00 2001 From: miraikumiko Date: Fri, 13 Jun 2025 15:10:17 +0200 Subject: [PATCH] Add snowflake IDs --- config/config.exs | 4 ++ lib/nulla/application.ex | 5 +- lib/nulla/models/activity.ex | 1 + lib/nulla/models/bookmarks.ex | 1 + lib/nulla/models/follow.ex | 1 + lib/nulla/models/hashtag.ex | 1 + lib/nulla/models/media_attachment.ex | 1 + lib/nulla/models/moderation_log.ex | 1 + lib/nulla/models/note.ex | 1 + lib/nulla/models/notification.ex | 1 + lib/nulla/models/session.ex | 1 + lib/nulla/models/user.ex | 11 ++++ lib/nulla/snowflake.ex | 62 +++++++++++++++++++ ...0250527054942_create_instance_settings.exs | 9 ++- .../20250530110822_create_users.exs | 3 +- .../20250604083506_create_notes.exs | 3 +- .../20250606100445_create_bookmarks.exs | 3 +- .../20250606103230_create_notifications.exs | 3 +- ...20250606103527_create_moderations_logs.exs | 3 +- .../20250606103649_create_hashtags.exs | 3 +- .../20250606103707_create_follows.exs | 3 +- .../20250606131715_create_sessions.exs | 3 +- ...0250606132108_create_media_attachments.exs | 3 +- .../20250607124601_create_activities.exs | 3 +- 24 files changed, 116 insertions(+), 14 deletions(-) create mode 100644 lib/nulla/snowflake.ex diff --git a/config/config.exs b/config/config.exs index 2c52b8a..fc9357b 100644 --- a/config/config.exs +++ b/config/config.exs @@ -22,6 +22,10 @@ config :nulla, NullaWeb.Endpoint, pubsub_server: Nulla.PubSub, live_view: [signing_salt: "jcAt5/U+"] +# Snowflake configuration +config :nulla, :snowflake, + worker_id: 1 + # Configures the mailer # # By default it uses the "Local" adapter which stores the emails diff --git a/lib/nulla/application.ex b/lib/nulla/application.ex index 7e86858..6832652 100644 --- a/lib/nulla/application.ex +++ b/lib/nulla/application.ex @@ -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 diff --git a/lib/nulla/models/activity.ex b/lib/nulla/models/activity.ex index bf0101e..e41b879 100644 --- a/lib/nulla/models/activity.ex +++ b/lib/nulla/models/activity.ex @@ -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 diff --git a/lib/nulla/models/bookmarks.ex b/lib/nulla/models/bookmarks.ex index b4e7a5b..1688167 100644 --- a/lib/nulla/models/bookmarks.ex +++ b/lib/nulla/models/bookmarks.ex @@ -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 diff --git a/lib/nulla/models/follow.ex b/lib/nulla/models/follow.ex index 0469dbd..3646881 100644 --- a/lib/nulla/models/follow.ex +++ b/lib/nulla/models/follow.ex @@ -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 diff --git a/lib/nulla/models/hashtag.ex b/lib/nulla/models/hashtag.ex index 0be647d..5e6d71e 100644 --- a/lib/nulla/models/hashtag.ex +++ b/lib/nulla/models/hashtag.ex @@ -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 diff --git a/lib/nulla/models/media_attachment.ex b/lib/nulla/models/media_attachment.ex index 9eca245..bafed6f 100644 --- a/lib/nulla/models/media_attachment.ex +++ b/lib/nulla/models/media_attachment.ex @@ -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 diff --git a/lib/nulla/models/moderation_log.ex b/lib/nulla/models/moderation_log.ex index f6d3469..7e30af1 100644 --- a/lib/nulla/models/moderation_log.ex +++ b/lib/nulla/models/moderation_log.ex @@ -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 diff --git a/lib/nulla/models/note.ex b/lib/nulla/models/note.ex index 57a07e6..efaf328 100644 --- a/lib/nulla/models/note.ex +++ b/lib/nulla/models/note.ex @@ -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 diff --git a/lib/nulla/models/notification.ex b/lib/nulla/models/notification.ex index c0d595c..946f58b 100644 --- a/lib/nulla/models/notification.ex +++ b/lib/nulla/models/notification.ex @@ -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 diff --git a/lib/nulla/models/session.ex b/lib/nulla/models/session.ex index c505ad6..0eb45b7 100644 --- a/lib/nulla/models/session.ex +++ b/lib/nulla/models/session.ex @@ -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 diff --git a/lib/nulla/models/user.ex b/lib/nulla/models/user.ex index caf90c5..b3ef7ce 100644 --- a/lib/nulla/models/user.ex +++ b/lib/nulla/models/user.ex @@ -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) diff --git a/lib/nulla/snowflake.ex b/lib/nulla/snowflake.ex new file mode 100644 index 0000000..5b7c88e --- /dev/null +++ b/lib/nulla/snowflake.ex @@ -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 diff --git a/priv/repo/migrations/20250527054942_create_instance_settings.exs b/priv/repo/migrations/20250527054942_create_instance_settings.exs index 3f8be91..64c45f0 100644 --- a/priv/repo/migrations/20250527054942_create_instance_settings.exs +++ b/priv/repo/migrations/20250527054942_create_instance_settings.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do use Ecto.Migration 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 :description, :text, default: "Freedom Social Network", null: false add :domain, :string, default: "localhost", null: false @@ -16,6 +17,8 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do timestamps() end + execute "ALTER TABLE instance_settings ADD CONSTRAINT single_row CHECK (id = 1);" + flush() execute(fn -> @@ -31,11 +34,11 @@ defmodule Nulla.Repo.Migrations.CreateInstanceSettings do sql = """ INSERT INTO instance_settings ( - name, description, domain, registration, + id, name, description, domain, registration, max_characters, max_upload_size, api_offset, public_key, private_key, inserted_at, updated_at ) VALUES ( - 'Nulla', 'Freedom Social Network', '#{domain}', false, + 1, 'Nulla', 'Freedom Social Network', '#{domain}', false, 5000, 50, 100, #{esc.(public_key)}, #{esc.(private_key)}, '#{now}', '#{now}' diff --git a/priv/repo/migrations/20250530110822_create_users.exs b/priv/repo/migrations/20250530110822_create_users.exs index cc7ab33..b597c2f 100644 --- a/priv/repo/migrations/20250530110822_create_users.exs +++ b/priv/repo/migrations/20250530110822_create_users.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateUsers do use Ecto.Migration 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 :email, :string add :password, :string diff --git a/priv/repo/migrations/20250604083506_create_notes.exs b/priv/repo/migrations/20250604083506_create_notes.exs index e14f519..975fd6f 100644 --- a/priv/repo/migrations/20250604083506_create_notes.exs +++ b/priv/repo/migrations/20250604083506_create_notes.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateNotes do use Ecto.Migration def change do - create table(:notes) do + create table(:notes, primary_key: false) do + add :id, :bigint, primary_key: true add :content, :text add :visibility, :string, default: "public" add :sensitive, :boolean, default: false diff --git a/priv/repo/migrations/20250606100445_create_bookmarks.exs b/priv/repo/migrations/20250606100445_create_bookmarks.exs index 810721e..5ddb447 100644 --- a/priv/repo/migrations/20250606100445_create_bookmarks.exs +++ b/priv/repo/migrations/20250606100445_create_bookmarks.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateBookmarks do use Ecto.Migration def change do - create table(:bookmarks) do + create table(:bookmarks, primary_key: false) do + add :id, :bigint, primary_key: true add :url, :string add :user_id, references(:users, on_delete: :delete_all) diff --git a/priv/repo/migrations/20250606103230_create_notifications.exs b/priv/repo/migrations/20250606103230_create_notifications.exs index 85ec848..ec8cbc0 100644 --- a/priv/repo/migrations/20250606103230_create_notifications.exs +++ b/priv/repo/migrations/20250606103230_create_notifications.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateNotifications do use Ecto.Migration 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 :actor_id, references(:users, on_delete: :nilify_all) add :type, :string, null: false diff --git a/priv/repo/migrations/20250606103527_create_moderations_logs.exs b/priv/repo/migrations/20250606103527_create_moderations_logs.exs index 252231f..546f88e 100644 --- a/priv/repo/migrations/20250606103527_create_moderations_logs.exs +++ b/priv/repo/migrations/20250606103527_create_moderations_logs.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateModerationLogs do use Ecto.Migration 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 :target_type, :string, null: false add :target_id, :string, null: false diff --git a/priv/repo/migrations/20250606103649_create_hashtags.exs b/priv/repo/migrations/20250606103649_create_hashtags.exs index 6a155cc..ac94cb2 100644 --- a/priv/repo/migrations/20250606103649_create_hashtags.exs +++ b/priv/repo/migrations/20250606103649_create_hashtags.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateHashtags do use Ecto.Migration 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 :usage_count, :integer, default: 0, null: false diff --git a/priv/repo/migrations/20250606103707_create_follows.exs b/priv/repo/migrations/20250606103707_create_follows.exs index 27f161f..b39244e 100644 --- a/priv/repo/migrations/20250606103707_create_follows.exs +++ b/priv/repo/migrations/20250606103707_create_follows.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateFollows do use Ecto.Migration 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 :target_id, references(:users, on_delete: :delete_all), null: false diff --git a/priv/repo/migrations/20250606131715_create_sessions.exs b/priv/repo/migrations/20250606131715_create_sessions.exs index b11c9fc..3e0e9e2 100644 --- a/priv/repo/migrations/20250606131715_create_sessions.exs +++ b/priv/repo/migrations/20250606131715_create_sessions.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateSessions do use Ecto.Migration 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 :token, :string, null: false add :user_agent, :string diff --git a/priv/repo/migrations/20250606132108_create_media_attachments.exs b/priv/repo/migrations/20250606132108_create_media_attachments.exs index 0aef7bd..1d9fe64 100644 --- a/priv/repo/migrations/20250606132108_create_media_attachments.exs +++ b/priv/repo/migrations/20250606132108_create_media_attachments.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateMediaAttachments do use Ecto.Migration 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 :file, :string, null: false add :mime_type, :string diff --git a/priv/repo/migrations/20250607124601_create_activities.exs b/priv/repo/migrations/20250607124601_create_activities.exs index 0ed3cf2..25addfd 100644 --- a/priv/repo/migrations/20250607124601_create_activities.exs +++ b/priv/repo/migrations/20250607124601_create_activities.exs @@ -2,7 +2,8 @@ defmodule Nulla.Repo.Migrations.CreateActivities do use Ecto.Migration 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 :actor, :string, null: false add :object, :map, null: false