# Copyright 2019 Schicksal # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. defmodule Aphora do @moduledoc """ [![Build Status](https://travis-ci.org/schicksalapp/aphora.svg?branch=master)](https://travis-ci.org/schicksalapp/aphora) [![Hex.pm Version](https://img.shields.io/hexpm/v/aphora.svg?style=flat)](https://hex.pm/packages/aphora) `Aphora` is a system do distributedly generate unique `t:id/0` without any coordination. It is similiar to [Snowflake](https://github.com/twitter-archive/snowflake/tree/snowflake-2010), but instead of a `t:non_neg_integer/0` it generates ASCII sortable a `t:String.t/0`. It uses a `72 Bit` long `binary()` which get encoded to a 12 characters long `t:id/0`. An `t:id/0` is composed of: * `45 Bit` `t:timestamp/0` with millisecond precision. It gives up to 1115 years with a custom epoch. * `5 Bit` `t:datacenter/0` allows up to `32` different datacenters. * `10 Bit` `t:worker/0` allows up to `1_024` different processes/workers/machines within a datacenter. * `12 Bit` `t:counter/0` allows for `4_096` unique `t:id/0`s to be generated each millisecond. Aphora also protects against rollovers to happen within the same millisecond. Just like [Snowflake](https://github.com/twitter-archive/snowflake/tree/snowflake-2010), Aphora protects from non-monotonic clocks, by refusing to generate `t:timestamp/0` until it gets provided with a time equals or after the one used in the last generated `t:id/0`. ## Installation The package can be installed by adding `:aphora` to your list of dependencies in `mix.exs`: ```elixir def deps do [ {:aphora, "~> 1.0.0"} ] end ``` The config file is located at `/config/config.exs`. If you run `Aphora` in more than one process, you'll need to adjust the `t:worker/0` and `t:datacenter/0`. ```elixir config :aphora, # Value between 0..31, unique between all `t:datacenter/0`. datacenter: 0, # Value between 0..1_024, unique between all `t:worker/0` within a `t:datacenter/0`. worker: 0, # Shouldn't be changed once the first `t:id/0` is generated. Default: 2019-01-01 00:00:00.000000Z in milliseconds. epoch: 1_546_300_800_000 ``` The docs can be found at [Hex](https://hexdocs.pm/aphora). ## Usage The usage of `Aphora` is quite straightforward after setting up the `config.exs` correctly, as all important methods are within the `Aphora` module. To generate a new `t:id/0`, you use `Aphora.new_id/0`. ```elixir Aphora.new_id() # {:ok, "0eHHz1--abHr"} ``` And if you want to find out when a `t:id/0` was generated, you use `Aphora.to_timestamp/2`. ```elixir Aphora.to_timestamp("0eHHz1--abHr", :relative) # 912_988_800_000 ``` ## License `Aphora` is licensed under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). """ use Application @typedoc """ A `t:counter/0` is a `12 Bits` long identifier, which by default has a value of `0`. It only increments, if two `t:id/0` are to be generated within the same `t:timestamp/0`. This allows for a total of `4_096` `t:id/0` to be generated within the same `t:timestamp/0` on the same unique combination of `t:worker/0` and `t:datacenter/0`. """ @typedoc since: "0.2.0" @type counter() :: non_neg_integer() @typedoc """ This can be any non-negative integer within the span of `0..31` and represents the `t:datacenter/0` in which the `t:worker/0` resides in. """ @typedoc since: "0.1.0" @type datacenter() :: non_neg_integer() @typedoc """ This can be any non-negative integer and represents the `t:epoch/0` with which the `t:timestamp/0` is generated. *`t:epoch/0` should **never** be changed after deploying `t:id/0` to prevent duplicates.* """ @typedoc since: "0.1.0" @type epoch() :: non_neg_integer() @typedoc """ A `t:id/0` is a `72 Bit` long unique & sortable identifier, which doesn't require any centralised coordination. It consists out of: * `45 Bit` `t:timestamp/0`. * `5 Bit` `t:datacenter/0`. * `10 Bit` `t:worker/0`. * `12 Bit` `t:counter/0`. """ @typedoc since: "0.1.0" @type id() :: String.t() @typedoc """ A `t:timestamp/0` is a positive integer, which is equals to the time in which it was generated within `Aphora.Worker.timestamp/1`. It consists of `System.os_time/1` minus the `t:epoch/0`. It has a total size of `45 Bits` within the `t:Aphora.id/0` and a `millisecond` precision. This allows for `35_184_372_088_832 millisecond` or around `1115` years of millisecond precision `t:timestamp/0`. """ @typedoc since: "0.1.0" @type timestamp() :: non_neg_integer() @typedoc """ This can be any non-negative integer within the span of `0..1_023` and represents the unique `t:worker/0` within a `t:datacenter/0`. *A `t:worker/0` should **never** be assigned twice with the same value within the same `t:datacenter/0`.* """ @typedoc since: "0.1.0" @type worker() :: non_neg_integer() def start(_type, _args) do children = [ {Aphora.Worker, %{ datacenter: Aphora.Config.get_datacenter(), worker: Aphora.Config.get_worker(), epoch: Aphora.Config.get_epoch() }} ] opts = [strategy: :one_for_one, name: __MODULE__] Supervisor.start_link(children, opts) end @doc """ Creates a new unique `t:id/0`. Please ensure, that the `config.exs` is setup correctly, to avoid a duplicate `t:id/0` to be generated on different workers/machines. `Aphora` relies on unique `t:worker/0` and `t:datacenter/0` to be set, to avoid duplicate IDs to be generated. Please also make sure to use [smeared time](https://tools.ietf.org/id/draft-ietf-ntp-bcp-06.html#rfc.section.4.6.1), to prevent reverted ticks. These won't be generated as `Aphora` has safeguards against this, but will return an `{:error, :reverted_tick}` instead. ## Examples iex> Aphora.new_id() {:ok, "-0dqbzfF----"} # If the timestamp ticked backwards. iex> Aphora.new_id() {:error, :reverted_tick} """ @doc since: "0.1.0" @spec new_id() :: {:ok, id()} | {:error, atom()} def new_id do GenServer.call(Aphora.Worker, :new_id) end @doc """ Extracts the `t:timestamp/0` out of the given `t:id/0`. By default `to_timestamp/2` selects `:absolute`, which will calculate the actual `t:timestamp/0`. If you instead want a `t:timestamp/0` relative to your `t:epoch/0`, use `:relative`. ## Examples iex> Aphora.to_timestamp("0eHHz1--abHr", :relative) 912_988_800_000 iex> Aphora.to_timestamp("-0dqbzfF----") 1_560_508_218_202 """ @doc since: "0.4.0" @spec to_timestamp(id(), :absolute | :relative) :: timestamp() def to_timestamp(id, mode \\ :absolute) do Aphora.Helper.get_timestamp(id, mode) end end