Current section
Files
Jump to
Current section
Files
lib/aphora.ex
# 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 """
[](https://travis-ci.org/schicksalapp/aphora)
[](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