Page MenuHomePhorge

No OneTemporary

Size
15 KB
Referenced Files
None
Subscribers
None
diff --git a/lib/flake_id.ex b/lib/flake_id.ex
index 1107a0e..34fbc45 100644
--- a/lib/flake_id.ex
+++ b/lib/flake_id.ex
@@ -1,154 +1,154 @@
# FlakeId: Decentralized, k-ordered ID generation service
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: LGPL-3.0-only
defmodule FlakeId do
@moduledoc """
Decentralized, k-ordered ID generation service.
"""
import Kernel, except: [to_string: 1]
alias FlakeId.Builder
alias FlakeId.Worker
alias FlakeId.Local
@type error ::
{:error, :clock_running_backwards} | {:error, :clock_stuck} | {:error, :invalid_state}
@doc """
Converts a binary Flake to a String
## Examples
iex> FlakeId.to_string(<<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>>)
"9n2ciuz1wdesFnrGJU"
"""
def to_string(<<_::integer-size(64), _::integer-size(48), _::integer-size(16)>> = binary_flake) do
<<integer::integer-size(128)>> = binary_flake
Base62.encode(integer)
end
def to_string(string), do: string
@doc """
Converts a String to a binary Flake
## Examples
iex> FlakeId.from_string("9n2ciuz1wdesFnrGJU")
<<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>>
"""
@spec from_string(String.t()) :: nil | <<_::128>>
def from_string(string)
def from_string(<<_::integer-size(128)>> = flake), do: flake
def from_string(string) when is_binary(string) and byte_size(string) < 18, do: nil
def from_string(string), do: string |> Base62.decode!() |> from_integer
@doc """
Converts a binary Flake to an integer
## Examples
iex> FlakeId.to_integer(<<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>>)
28939165907792829150732718047232
"""
@spec to_integer(<<_::128>>) :: non_neg_integer
def to_integer(binary_flake)
def to_integer(<<integer::integer-size(128)>>), do: integer
@doc """
Converts an integer to a binary Flake
## Examples
iex> FlakeId.from_integer(28939165907792829150732718047232)
<<0, 0, 1, 109, 67, 124, 251, 125, 95, 28, 30, 59, 36, 42, 0, 0>>
"""
@spec from_integer(integer) :: <<_::128>>
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 string with Flake
"""
- @spec get :: {:ok, String.t()} | error()
- def get do
- case Worker.get() do
+ @spec get(atom()) :: {:ok, String.t()} | error()
+ def get(name \\ Worker) do
+ case Worker.get(name) do
{:ok, flake} -> {:ok, to_string(flake)}
error -> error
end
end
@doc """
Generates a local Flake (in-process state). Worker ID will be derived from global worker-id and the process pid.
First call will be slower but subsequent calls will be faster than worker based `get/0` and is quite useful for long running processes.
"""
@spec get_local :: {:ok, String.t()} | error()
def get_local do
case Local.get() do
{:ok, flake} -> {:ok, to_string(flake)}
error -> error
end
end
@doc """
Generates a predictable and back-dated FlakeId.
This can be useful when you want to insert historical data without messing with sorting: be **very careful** at choosing a `unique` value that is _really unique_ for the given `datetime`, otherwise, collisons will happen.
"""
@spec backdate(any(), DateTime.t()) :: {:ok, String.t()} | error()
def backdate(datetime, unique) do
hash = :erlang.phash2(unique)
time = DateTime.to_unix(datetime, :millisecond)
case Builder.get(time, %Builder{node: hash}) do
{:ok, flake, _} -> {:ok, to_string(flake)}
error -> error
end
end
@doc """
Generates a notional FlakeId (with an empty worker id) given a timestamp.
This is useful for generating a lexical range of values that could have been generated in a span of time.
"""
def notional(datetime) do
time = DateTime.to_unix(datetime, :millisecond)
case Builder.get(time, %Builder{node: 0}) do
{:ok, flake, _} -> {:ok, to_string(flake)}
error -> error
end
end
@doc """
Checks that ID is a valid FlakeId
## Examples
iex> FlakeId.flake_id?("9n2ciuz1wdesFnrGJU")
true
iex> FlakeId.flake_id?("#cofe")
false
iex> FlakeId.flake_id?("pleroma.social")
false
"""
@spec flake_id?(String.t()) :: boolean
def flake_id?(id), do: flake_id?(String.to_charlist(id), true)
defp flake_id?([c | cs], true) when c >= ?0 and c <= ?9, do: flake_id?(cs, true)
defp flake_id?([c | cs], true) when c >= ?A and c <= ?Z, do: flake_id?(cs, true)
defp flake_id?([c | cs], true) when c >= ?a and c <= ?z, do: flake_id?(cs, true)
defp flake_id?([], true), do: true
defp flake_id?(_, _), do: false
end
diff --git a/lib/flake_id/application.ex b/lib/flake_id/application.ex
index 61fbc50..72e42b4 100644
--- a/lib/flake_id/application.ex
+++ b/lib/flake_id/application.ex
@@ -1,27 +1,36 @@
# FlakeId: Decentralized, k-ordered ID generation service
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: LGPL-3.0-only
defmodule FlakeId.Application do
@moduledoc false
use Application
require Logger
def start(_type, _args) do
- unless :erlang.system_info(:time_warp_mode) == :multi_time_warp do
+ if elem(default_clock(), 1) == :os do
Logger.error("""
FlakeId: Time consistency is not optimal, using system time.
More information at: https://hexdocs.pm/flake_id/readme.html#time-warp
""")
end
children = [
FlakeId.TimeHint,
FlakeId.Worker
]
opts = [strategy: :one_for_one, name: FlakeId.Supervisor]
Supervisor.start_link(children, opts)
end
+
+ def default_clock() do
+ if :erlang.system_info(:time_warp_mode) == :multi_time_warp do
+ {:erlang, :system_time}
+ else
+ {:os, :system_time}
+ end
+ end
+
end
diff --git a/lib/flake_id/builder.ex b/lib/flake_id/builder.ex
index e363044..9164dcc 100644
--- a/lib/flake_id/builder.ex
+++ b/lib/flake_id/builder.ex
@@ -1,130 +1,122 @@
# FlakeId: Decentralized, k-ordered ID generation service
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: LGPL-3.0-only
defmodule FlakeId.Builder do
@moduledoc false
defstruct node: nil, time: 0, sq: 0, clock: nil
@type t :: %__MODULE__{
node: non_neg_integer,
time: non_neg_integer,
sq: non_neg_integer
}
- @type options :: [worker_id_option() | time_hint_option() | clock_option()]
- @type worker_id_option :: {:worker_id, any()}
- @type time_hint_option :: {:time_hint, false | true | atom()}
- @type clock_option :: {module(), atom()}
-
- @spec new(options()) :: t()
+ @spec new(options) :: t() when options: [worker_id | time_hint | clock],
+ worker_id: {:worker_id, :random | :node | {:iface, String.t()} | Integer.t | String.t()},
+ time_hint: {:time_hint, false | true | atom()},
+ clock: {module(), atom()}
def new(options \\ []) do
worker_id = worker_id(Keyword.get(options, :worker_id))
time =
case Keyword.get(options, :time_hint) do
false -> 0
name when is_atom(name) -> FlakeId.TimeHint.get(name)
_ -> FlakeId.TimeHint.get()
end
- clock = Keyword.get(options, :clock, default_clock())
+ clock = Keyword.get(options, :clock, FlakeId.Application.default_clock())
%__MODULE__{node: worker_id, time: time, clock: clock}
end
+ @spec backwards?(t()) :: boolean()
def backwards?(%__MODULE__{time: time, clock: clock}) do
time > time(clock)
end
def time() do
- time(default_clock())
+ time(FlakeId.Application.default_clock())
end
+ @spec time(t() | {module(), atom()}) :: non_neg_integer()
def time(%__MODULE__{clock: clock}) do
time(clock)
end
def time({mod, fun}) do
apply(mod, fun, [:millisecond])
end
@spec get(t) :: {:ok, <<_::128>>, t} | FlakeId.error()
def get(state = %__MODULE__{clock: clock}) do
get(time(clock), state)
end
- @spec get(non_neg_integer, t) :: {:ok, <<_::128>>, t} | FlakeId.error()
+ @spec get(non_neg_integer(), t()) :: {:ok, <<_::128>>, t()} | FlakeId.error()
# Error when time didn't change and sequence is too big for 16bit int.
def get(time, %__MODULE__{time: time, sq: sq}) when sq >= 65_535 do
{:error, :clock_stuck}
end
# Matches when the calling time is the same as the state time. Incr. sq
def get(time, %__MODULE__{node: node, time: time, sq: seq} = state)
when is_integer(time) and is_integer(node) and is_integer(seq) do
new_state = %__MODULE__{state | sq: seq + 1}
{:ok, gen_flake(new_state), new_state}
end
# Error when clock is running backwards
def get(newtime, %__MODULE__{time: time}) when newtime < time do
{:error, :clock_running_backwards}
end
# Matches when the times are different, reset sq
def get(newtime, %__MODULE__{node: node, time: time, sq: seq} = state)
when is_integer(time) and is_integer(node) and is_integer(seq) and newtime > time do
new_state = %__MODULE__{state | time: newtime, sq: 0}
{:ok, gen_flake(new_state), new_state}
end
def get(_, _) do
{:error, :invalid_state}
end
@spec gen_flake(t) :: <<_::128>>
defp gen_flake(%__MODULE__{time: time, node: node, sq: seq}) do
<<time::integer-size(64), node::integer-size(48), seq::integer-size(16)>>
end
- defp default_clock() do
- if :erlang.system_info(:time_warp_mode) == :multi_time_warp do
- {:erlang, :system_time}
- else
- {:os, :system_time}
- end
- end
-
def worker_id(setting \\ nil)
def worker_id(nil) do
worker_id(Application.get_env(:flake_id, :worker_id, :random))
end
def worker_id(:node) do
:erlang.phash2(node())
end
def worker_id({:mac, iface}) do
{:ok, addresses} = :inet.getifaddrs()
proplist = :proplists.get_value(iface, addresses)
hwaddr = Enum.take(:proplists.get_value(:hwaddr, proplist), 6)
<<worker::integer-size(48)>> = :binary.list_to_bin(hwaddr)
worker
end
def worker_id(:random) do
<<worker::integer-size(48)>> = :crypto.strong_rand_bytes(6)
worker
end
def worker_id(integer) when is_integer(integer) do
integer
end
def worker_id(binary) when is_binary(binary) do
:erlang.phash2(binary)
end
end
diff --git a/lib/flake_id/time_hint.ex b/lib/flake_id/time_hint.ex
index 08846f2..633e7e3 100644
--- a/lib/flake_id/time_hint.ex
+++ b/lib/flake_id/time_hint.ex
@@ -1,121 +1,125 @@
# FlakeId: Decentralized, k-ordered ID generation service
# Copyright © 2017-2019 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: LGPL-3.0-only
defmodule FlakeId.TimeHint do
@moduledoc false
use GenServer
require Logger
@tick :timer.minutes(1)
@default_env false
@default_file "flake_id_time"
- @clock {FlakeId.Builder, :time}
@spec get() :: 0 | non_neg_integer
@doc "Returns the latest time hint, or zero if disabled"
def get(name \\ nil) do
if name || Application.get_env(:flake_id, :time_hint, @default_env) do
GenServer.call(name || __MODULE__, :get)
else
0
end
end
+ @spec start_link([] | options) :: GenServer.on_start when options: [name | file | tick | clock],
+ name: {:name, atom()},
+ file: {:file, Path.t()},
+ tick: {:tick, non_neg_integer()},
+ clock: {module(), atom()}
def start_link(options)
def start_link([]) do
if Application.get_env(:flake_id, :time_hint, false) do
file =
Application.get_env(
:flake_id,
:time_hint_file,
Application.app_dir(:flake_id, ["priv", @default_file])
)
tick = Application.get_env(:flake_id, :time_hint_tick, @tick)
- clock = Application.get_env(:flake_id, :time_hint_clock, @clock)
+ clock = Application.get_env(:flake_id, :time_hint_clock, FlakeId.Application.default_clock())
GenServer.start_link(__MODULE__, [file, tick, clock], name: __MODULE__)
else
:ignore
end
end
def start_link(options) do
name = Keyword.fetch!(options, :name)
file = Keyword.fetch!(options, :file)
tick = Keyword.get(options, :tick, @tick)
- clock = Keyword.get(options, :clock, @clock)
+ clock = Keyword.get(options, :clock, FlakeId.Application.default_clock())
GenServer.start_link(__MODULE__, [file, tick, clock], name: name)
end
@impl true
def init([file, tick, clock]) do
Process.send_after(self(), :tick, tick)
{:ok, {file, tick, clock, read_and_update(file, clock)}}
end
@impl true
def handle_call(:get, _, state = {_, _, _, time}) do
{:reply, time, state}
end
@impl true
def handle_info(:tick, {file, tick, clock, prev_time}) do
Process.send_after(self(), :tick, tick)
{:noreply, {file, tick, clock, update(file, clock, prev_time)}}
end
if Mix.env() == :test do
@impl true
def handle_info(:stop, state) do
{:stop, :normal, state}
end
end
@impl true
def terminate(_, {file, _, clock, prev_time}) do
update(file, clock, prev_time)
end
defp read_and_update(file, clock) do
update(file, clock, read(file))
end
- defp update(file, {mod, fun}, prev_time) do
- time = :erlang.apply(mod, fun, [])
+ defp update(file, {mod,fun}, prev_time) do
+ time = apply(mod, fun, [:millisecond])
if time > prev_time do
:ok = write(file, time)
time
else
prev_time
end
end
defp read(file) do
case File.read(file) do
{:ok, data} ->
{__MODULE__, time} = :erlang.binary_to_term(data)
time
{:error, :enoent} ->
0
error ->
Logger.error("#{__MODULE__}: could not read file #{file}: #{inspect(error)}")
throw(error)
end
end
defp write(file, time) do
case File.write(file, :erlang.term_to_binary({__MODULE__, time})) do
:ok ->
:ok
error ->
Logger.error("#{__MODULE__}: could not write file #{file}: #{inspect(error)}")
error
end
end
end
diff --git a/lib/flake_id/worker.ex b/lib/flake_id/worker.ex
index e253594..7dc7be6 100644
--- a/lib/flake_id/worker.ex
+++ b/lib/flake_id/worker.ex
@@ -1,45 +1,43 @@
# FlakeId: Decentralized, k-ordered ID generation service
# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
# SPDX-License-Identifier: LGPL-3.0-only
defmodule FlakeId.Worker do
@moduledoc false
use GenServer
require Logger
alias FlakeId.Builder
- @type options :: [{:name, atom()} | Builder.options()]
-
def start_link(options \\ []) do
name = Keyword.get(options, :name, __MODULE__)
GenServer.start_link(__MODULE__, [options], name: name)
end
- @spec get :: {:ok, String.t()} | FlakeId.error()
- def get(name \\ __MODULE__), do: GenServer.call(name, :get)
+ @spec get(atom()) :: {:ok, String.t()} | FlakeId.error()
+ def get(name), do: GenServer.call(name, :get)
@impl true
@spec init([]) :: {:ok, Builder.t()} | {:stop, :clock_is_backwards}
def init([options]) do
state = Builder.new(options)
if Builder.backwards?(state) do
Logger.error("#{__MODULE__} Current time is backwards, not starting")
{:stop, :clock_is_backwards}
else
{:ok, Builder.new()}
end
end
@impl true
def handle_call(:get, _from, state) do
case Builder.get(state) do
{:ok, flake, new_state} ->
{:reply, {:ok, flake}, new_state}
error ->
{:reply, error, state}
end
end
end
diff --git a/test/support/fake_clock.ex b/test/support/fake_clock.ex
index 3c84038..3a85455 100644
--- a/test/support/fake_clock.ex
+++ b/test/support/fake_clock.ex
@@ -1,4 +1,4 @@
defmodule FakeClock do
def set(time), do: :persistent_term.put(__MODULE__, time)
- def time(), do: :persistent_term.get(__MODULE__)
+ def time(_), do: :persistent_term.get(__MODULE__)
end

File Metadata

Mime Type
text/x-diff
Expires
Mon, Nov 25, 3:51 AM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
39569
Default Alt Text
(15 KB)

Event Timeline