183 lines
		
	
	
	
		
			4.8 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
			
		
		
	
	
			183 lines
		
	
	
	
		
			4.8 KiB
		
	
	
	
		
			Elixir
		
	
	
	
	
	
| # Pleroma: A lightweight social networking server
 | |
| # Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
 | |
| # SPDX-License-Identifier: AGPL-3.0-only
 | |
| 
 | |
| defmodule Pleroma.FlakeId do
 | |
|   @moduledoc """
 | |
|   Flake is a decentralized, k-ordered id generation service.
 | |
| 
 | |
|   Adapted from:
 | |
| 
 | |
|   * [flaky](https://github.com/nirvana/flaky), released under the terms of the Truly Free License,
 | |
|   * [Flake](https://github.com/boundary/flake), Copyright 2012, Boundary, Apache License, Version 2.0
 | |
|   """
 | |
| 
 | |
|   @type t :: binary
 | |
| 
 | |
|   @behaviour Ecto.Type
 | |
|   use GenServer
 | |
|   require Logger
 | |
|   alias __MODULE__
 | |
|   import Kernel, except: [to_string: 1]
 | |
| 
 | |
|   defstruct node: nil, time: 0, sq: 0
 | |
| 
 | |
|   @doc "Converts a binary Flake to a String"
 | |
|   def to_string(<<0::integer-size(64), id::integer-size(64)>>) do
 | |
|     Kernel.to_string(id)
 | |
|   end
 | |
| 
 | |
|   def to_string(flake = <<_::integer-size(64), _::integer-size(48), _::integer-size(16)>>) do
 | |
|     encode_base62(flake)
 | |
|   end
 | |
| 
 | |
|   def to_string(s), do: s
 | |
| 
 | |
|   for i <- [-1, 0] do
 | |
|     def from_string(unquote(i)), do: <<0::integer-size(128)>>
 | |
|     def from_string(unquote(Kernel.to_string(i))), do: <<0::integer-size(128)>>
 | |
|   end
 | |
| 
 | |
|   def from_string(flake = <<_::integer-size(128)>>), do: flake
 | |
| 
 | |
|   def from_string(string) when is_binary(string) and byte_size(string) < 18 do
 | |
|     case Integer.parse(string) do
 | |
|       {id, _} -> <<0::integer-size(64), id::integer-size(64)>>
 | |
|       _ -> nil
 | |
|     end
 | |
|   end
 | |
| 
 | |
|   def from_string(string) do
 | |
|     string |> decode_base62 |> from_integer
 | |
|   end
 | |
| 
 | |
|   def to_integer(<<integer::integer-size(128)>>), do: integer
 | |
| 
 | |
|   def from_integer(integer) do
 | |
|     <<_time::integer-size(64), _node::integer-size(48), _seq::integer-size(16)>> =
 | |
|       <<integer::integer-size(128)>>
 | |
|   end
 | |
| 
 | |
|   @doc "Generates a Flake"
 | |
|   @spec get :: binary
 | |
|   def get, do: to_string(:gen_server.call(:flake, :get))
 | |
| 
 | |
|   # -- Ecto.Type API
 | |
|   @impl Ecto.Type
 | |
|   def type, do: :uuid
 | |
| 
 | |
|   @impl Ecto.Type
 | |
|   def cast(value) do
 | |
|     {:ok, FlakeId.to_string(value)}
 | |
|   end
 | |
| 
 | |
|   @impl Ecto.Type
 | |
|   def load(value) do
 | |
|     {:ok, FlakeId.to_string(value)}
 | |
|   end
 | |
| 
 | |
|   @impl Ecto.Type
 | |
|   def dump(value) do
 | |
|     {:ok, FlakeId.from_string(value)}
 | |
|   end
 | |
| 
 | |
|   def autogenerate(), do: get()
 | |
| 
 | |
|   # -- GenServer API
 | |
|   def start_link do
 | |
|     :gen_server.start_link({:local, :flake}, __MODULE__, [], [])
 | |
|   end
 | |
| 
 | |
|   @impl GenServer
 | |
|   def init([]) do
 | |
|     {:ok, %FlakeId{node: mac(), time: time()}}
 | |
|   end
 | |
| 
 | |
|   @impl GenServer
 | |
|   def handle_call(:get, _from, state) do
 | |
|     {flake, new_state} = get(time(), state)
 | |
|     {:reply, flake, new_state}
 | |
|   end
 | |
| 
 | |
|   # Matches when the calling time is the same as the state time. Incr. sq
 | |
|   defp get(time, %FlakeId{time: time, node: node, sq: seq}) do
 | |
|     new_state = %FlakeId{time: time, node: node, sq: seq + 1}
 | |
|     {gen_flake(new_state), new_state}
 | |
|   end
 | |
| 
 | |
|   # Matches when the times are different, reset sq
 | |
|   defp get(newtime, %FlakeId{time: time, node: node}) when newtime > time do
 | |
|     new_state = %FlakeId{time: newtime, node: node, sq: 0}
 | |
|     {gen_flake(new_state), new_state}
 | |
|   end
 | |
| 
 | |
|   # Error when clock is running backwards
 | |
|   defp get(newtime, %FlakeId{time: time}) when newtime < time do
 | |
|     {:error, :clock_running_backwards}
 | |
|   end
 | |
| 
 | |
|   defp gen_flake(%FlakeId{time: time, node: node, sq: seq}) do
 | |
|     <<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>>
 | |
|   end
 | |
| 
 | |
|   defp nthchar_base62(n) when n <= 9, do: ?0 + n
 | |
|   defp nthchar_base62(n) when n <= 35, do: ?A + n - 10
 | |
|   defp nthchar_base62(n), do: ?a + n - 36
 | |
| 
 | |
|   defp encode_base62(<<integer::integer-size(128)>>) do
 | |
|     integer
 | |
|     |> encode_base62([])
 | |
|     |> List.to_string()
 | |
|   end
 | |
| 
 | |
|   defp encode_base62(int, acc) when int < 0, do: encode_base62(-int, acc)
 | |
|   defp encode_base62(int, []) when int == 0, do: '0'
 | |
|   defp encode_base62(int, acc) when int == 0, do: acc
 | |
| 
 | |
|   defp encode_base62(int, acc) do
 | |
|     r = rem(int, 62)
 | |
|     id = div(int, 62)
 | |
|     acc = [nthchar_base62(r) | acc]
 | |
|     encode_base62(id, acc)
 | |
|   end
 | |
| 
 | |
|   defp decode_base62(s) do
 | |
|     decode_base62(String.to_charlist(s), 0)
 | |
|   end
 | |
| 
 | |
|   defp decode_base62([c | cs], acc) when c >= ?0 and c <= ?9,
 | |
|     do: decode_base62(cs, 62 * acc + (c - ?0))
 | |
| 
 | |
|   defp decode_base62([c | cs], acc) when c >= ?A and c <= ?Z,
 | |
|     do: decode_base62(cs, 62 * acc + (c - ?A + 10))
 | |
| 
 | |
|   defp decode_base62([c | cs], acc) when c >= ?a and c <= ?z,
 | |
|     do: decode_base62(cs, 62 * acc + (c - ?a + 36))
 | |
| 
 | |
|   defp decode_base62([], acc), do: acc
 | |
| 
 | |
|   defp time do
 | |
|     {mega_seconds, seconds, micro_seconds} = :erlang.timestamp()
 | |
|     1_000_000_000 * mega_seconds + seconds * 1000 + :erlang.trunc(micro_seconds / 1000)
 | |
|   end
 | |
| 
 | |
|   def mac do
 | |
|     {:ok, addresses} = :inet.getifaddrs()
 | |
| 
 | |
|     macids =
 | |
|       Enum.reduce(addresses, [], fn {_iface, attrs}, acc ->
 | |
|         case attrs[:hwaddr] do
 | |
|           [0, 0, 0 | _] -> acc
 | |
|           mac when is_list(mac) -> [mac_to_worker_id(mac) | acc]
 | |
|           _ -> acc
 | |
|         end
 | |
|       end)
 | |
| 
 | |
|     List.first(macids)
 | |
|   end
 | |
| 
 | |
|   def mac_to_worker_id(mac) do
 | |
|     <<worker::integer-size(48)>> = :binary.list_to_bin(mac)
 | |
|     worker
 | |
|   end
 | |
| end
 | 
