Skip to content

Commit

Permalink
Check machine ID (#2)
Browse files Browse the repository at this point in the history
* Helpers to validate function reference

* Helper to validate machine ID using check_machine_id callback option

* Bump to version 0.3.0

* Add new option check_machine_id to docs

* Remove pending item from README
  • Loading branch information
elciok authored Apr 5, 2024
1 parent 2d2c3c0 commit fdb1506
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 13 deletions.
6 changes: 1 addition & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Add `sonyflakex` to your list of dependencies in `mix.exs`:
```elixir
def deps do
[
{:sonyflakex, "~> 0.2.0"}
{:sonyflakex, "~> 0.3.0"}
]
end
```
Expand Down Expand Up @@ -70,10 +70,6 @@ Like the reference implementation in Go, the default `Sonyflakex` GenServer will

If you need to generate a higher volume of IDs in short periods of time, then you might need to run a pool of multiple `Sonyflakex` GenServers (each with a unique machine ID).

## Pending

- [ ] Callback to check machine ID is unique.

## License

The MIT License (MIT)
Expand Down
12 changes: 11 additions & 1 deletion lib/sonyflakex.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,13 @@ defmodule Sonyflakex do
Options:
- start_time: UNIX timestamp used as starting point
for other timestamps used to compose IDs
- machine_id: integer that identifies the machine
- machine_id: Integer that identifies the machine
generating IDs. It is also part of the ID
so it should fit in 16 bits.
- check_machine_id: Callback function to validate
the uniqueness of the machine ID. If check_machine_id
returns false, Sonyflakex process is not started. If
check_machine_id is nil, no validation is done.
Returns:
- `{:ok, pid}`: In case process is started successfully, it returns a tuple containing an ID.
Expand All @@ -38,7 +42,13 @@ defmodule Sonyflakex do
end
end
```
In case of machine ID customization you can use a `check_machine_id` function
reference that receives machine IDs and returns true if the machine ID is unique.
Since machine ID is part of the ID generated, it should be unique for each set
of generators, otherwise collisions (repeated IDs) might be generated.
You should only customize machine_id if the default value used (lower 16 bits
of the first private IP v4 address) won't work for your case.
"""
@spec start_link(keyword()) :: :ignore | {:error, any()} | {:ok, pid()}
def start_link(args \\ []) do
Expand Down
47 changes: 47 additions & 0 deletions lib/sonyflakex/config.ex
Original file line number Diff line number Diff line change
Expand Up @@ -82,4 +82,51 @@ defmodule Sonyflakex.Config do
<<max::integer-size(length_bits + 1)>> = <<1::size(1), 0::size(length_bits)>>
value < max
end

@doc """
Validates option value is a function reference with input arity.
This is checked only if value is set.
"""
@spec validate_is_function(keyword(), atom(), non_neg_integer()) ::
{:error, {:non_function, atom(), any()}}
| {:error, {:wrong_function_arity, atom(), any()}}
| {:ok, keyword()}
def validate_is_function(opts, option_name, arity) do
case Keyword.fetch(opts, option_name) do
{:ok, value} when is_function(value, arity) ->
{:ok, opts}

{:ok, value} when is_function(value) ->
{:error, {:wrong_function_arity, option_name, value}}

{:ok, value} ->
{:error, {:non_function, option_name, value}}

:error ->
{:ok, opts}
end
end

@doc """
Validates machine ID value using check_machine_id function if option is set.
Returns validation error if check_machine_id returns false.
"""
@spec validate_machine_id(keyword(), atom(), atom()) ::
{:error, {:machine_id_not_unique, integer()}}
| {:ok, keyword()}
def validate_machine_id(opts, check_machine_id_option, machine_id_option) do
case Keyword.fetch(opts, check_machine_id_option) do
{:ok, check_machine_id} ->
machine_id = Keyword.fetch!(opts, machine_id_option)

if check_machine_id.(machine_id) do
{:ok, opts}
else
{:error, {:machine_id_not_unique, machine_id}}
end

:error ->
{:ok, opts}
end
end
end
29 changes: 23 additions & 6 deletions lib/sonyflakex/state.ex
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ defmodule Sonyflakex.State do

@option_start_time :start_time
@option_machine_id :machine_id
@option_check_machine_id :check_machine_id

@typedoc """
Represents internal ID generator state used to compute the next ID.
Expand All @@ -40,14 +41,27 @@ defmodule Sonyflakex.State do
Initializes ID generator state.
This method should be used by clients to create the initial state for
the ID generator with the following default configuration:
- *Start time*: '2014-09-01T00:00:00Z' is used when calculating
elapsed time for timestamps in state.
- *Machine ID*: Lower 16 bits of one of the machine's private
the ID generator with the following configuration.
Options (all optional):
- start_time: UNIX timestamp used as starting point
for other timestamps used to compose IDs
- machine_id: Integer that identifies the machine
generating IDs. It is also part of the ID
so it should fit in 16 bits.
- check_machine_id: Callback function to validate
the uniqueness of the machine ID. If check_machine_id
returns false, Sonyflakex process is not started. If
check_machine_id is nil, no validation is done.
When not set, these will be the ddefault values for options
used by the Sonyflake generator:
- start_time: '2014-09-01T00:00:00Z'.
- machine_id: Lower 16 bits of one of the machine's private
IP addresses. If you run multiple generators in the same
machine this field will be set to the same value and duplicated
IDs might be generated.
- check_machine_id: `nil`
"""
@spec new(keyword()) :: {:ok, t()} | {:error, any()}
def new(opts) do
Expand All @@ -72,7 +86,10 @@ defmodule Sonyflakex.State do
end),
{:ok, opts} <- Config.validate_is_integer(opts, @option_machine_id),
{:ok, opts} <-
Config.validate_bit_option_length(opts, @option_machine_id, @bits_machine_id) do
Config.validate_bit_option_length(opts, @option_machine_id, @bits_machine_id),
{:ok, opts} <- Config.validate_is_function(opts, @option_check_machine_id, 1),
{:ok, opts} <-
Config.validate_machine_id(opts, @option_check_machine_id, @option_machine_id) do
{:ok, opts}
end
end
Expand Down
2 changes: 1 addition & 1 deletion mix.exs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ defmodule Sonyflakex.MixProject do
use Mix.Project

@source_url "https://github.com/elciok/sonyflakex"
@version "0.2.0"
@version "0.3.0"

def project do
[
Expand Down
65 changes: 65 additions & 0 deletions test/sonyflakex/config_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,69 @@ defmodule Sonyflakex.ConfigTest do
assert which == {:value_too_big, :age, 42}
end
end

describe "validate_is_function/3" do
test "returns ok if no option set" do
{:ok, _opts} = Config.validate_is_function([age: 42], :check_function, 2)
end

test "returns ok if option is set with function reference with correct arity" do
check_function = fn _, _ -> true end

{:ok, opts} =
Config.validate_is_function([check_function: check_function], :check_function, 2)

assert {:ok, ^check_function} = Keyword.fetch(opts, :check_function)
end

test "returns error if option is set with function reference with wrong arity" do
check_function = fn _, _ -> true end

{:error, which} =
Config.validate_is_function([check_function: check_function], :check_function, 100)

assert which == {:wrong_function_arity, :check_function, check_function}
end

test "returns error if option set is not a function reference" do
{:error, which} =
Config.validate_is_function([check_function: 1234], :check_function, 1)

assert which == {:non_function, :check_function, 1234}
end
end

describe "validate_machine_id/3" do
test "returns ok if check_machine_id option is not set" do
{:ok, _opts} = Config.validate_machine_id([age: 42], :check_machine_id, :machine_id)
end

test "returns ok if check_machine_id(machine_id) returns true" do
check_function = fn machine_id ->
if machine_id == 1, do: true, else: false
end

{:ok, _opts} =
Config.validate_machine_id(
[check_machine_id: check_function, machine_id: 1],
:check_machine_id,
:machine_id
)
end

test "returns error if check_machine_id(machine_id) returns true" do
check_function = fn machine_id ->
if machine_id == 1, do: true, else: false
end

{:error, which} =
Config.validate_machine_id(
[check_machine_id: check_function, machine_id: 2],
:check_machine_id,
:machine_id
)

assert which == {:machine_id_not_unique, 2}
end
end
end

0 comments on commit fdb1506

Please sign in to comment.