Current section

Files

Jump to
uuid_v7 lib clock.ex
Raw

lib/clock.ex

defmodule UUIDv7.Clock do
@moduledoc false
@ns_per_ms 1_000_000
# For macOS (Darwin) or Windows we would normally use 10 bits instead of 12.
# However, it would be an extra complexity and tradeoff of checking OS at
# runtime with some extra calcs, just for 2 bits of extra randomness for
# people running their applications on Windows or macOS.
@sub_ms_bits 12
# The count of possible values that fit in those bits (4096 or 2^12).
@possible_values Bitwise.bsl(1, @sub_ms_bits)
@minimal_step_ns div(@ns_per_ms, @possible_values) + 1
@doc """
Get an always-ascending unix nanosecond timestamp.
This is a re-implementation of the Postgres version here (in `C`):
https://github.com/postgres/postgres/blob/0e42d31b0b2273c376ce9de946b59d155fac589c/src/backend/utils/adt/uuid.c#L480
We use `:atomics` to ensure this works with concurrent executions without race
conditions.
"""
def next_ascending do
# Get the atomic ref for the timestamp and initialize it if it doesn't exist yet.
timestamp_ref =
with nil <- :persistent_term.get(__MODULE__, nil) do
timestamp_ref = :atomics.new(1, signed: false)
:ok = :persistent_term.put(__MODULE__, timestamp_ref)
timestamp_ref
end
previous_ts = :atomics.get(timestamp_ref, 1)
min_step_ts = previous_ts + @minimal_step_ns
current_ts = System.system_time(:nanosecond)
# If the current timestamp is not at least the minimal step nanoseconds
# greater than the previous step, then we make it so.
new_ts =
if current_ts > min_step_ts do
current_ts
else
min_step_ts
end
compare_exchange(timestamp_ref, previous_ts, new_ts)
end
defp compare_exchange(timestamp_ref, previous_ts, new_ts) do
case :atomics.compare_exchange(timestamp_ref, 1, previous_ts, new_ts) do
# If the new value was written, then we return it.
:ok ->
new_ts
# If the atomic value has changed in the meantime, we add the minimal step
# nanoseconds value to that and try again.
updated_ts ->
compare_exchange(timestamp_ref, updated_ts, updated_ts + @minimal_step_ns)
end
end
end