Skip to content

Commit fdca78a

Browse files
uuidv7 monotonicity
1 parent e6da70d commit fdca78a

File tree

1 file changed

+89
-7
lines changed

1 file changed

+89
-7
lines changed

lib/ecto/uuid.ex

Lines changed: 89 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,20 @@ defmodule Ecto.UUID do
3434
"""
3535
@type options :: [version: 4 | 7]
3636

37+
# The bits available for sub-millisecond fractions when using increased clock
38+
# precision.
39+
@sub_ms_bits 12
40+
41+
# The number of values that can be represented in the bit space (2^12).
42+
@possible_values Bitwise.bsl(1, @sub_ms_bits)
43+
44+
# The number of nanoseconds in a millisecond.
45+
@ns_per_ms 1_000_000
46+
47+
# The minimum step when using increased clock precision with fractional
48+
# milliseconds.
49+
@minimal_step_ns div(@ns_per_ms, @possible_values)
50+
3751
@doc false
3852
def type, do: :uuid
3953

@@ -217,9 +231,9 @@ defmodule Ecto.UUID do
217231
"""
218232
@spec bingenerate(options) :: raw
219233
def bingenerate(opts \\ []) do
220-
case Keyword.get(opts, :version, @default_version) do
221-
4 -> bingenerate_v4()
222-
7 -> bingenerate_v7()
234+
case Keyword.pop(opts, :version, @default_version) do
235+
{4, _opts} -> bingenerate_v4()
236+
{7, opts} -> bingenerate_v7(opts)
223237
version -> raise ArgumentError, "unknown UUID version: #{inspect(version)}"
224238
end
225239
end
@@ -229,11 +243,79 @@ defmodule Ecto.UUID do
229243
<<u0::48, 4::4, u1::12, 2::2, u2::62>>
230244
end
231245

232-
defp bingenerate_v7 do
233-
milliseconds = System.system_time(:millisecond)
234-
<<u0::12, u1::62, _::6>> = :crypto.strong_rand_bytes(10)
246+
defp bingenerate_v7(opts) do
247+
# From RFC 9562:
248+
# Monotonicity (each subsequent value being greater than the last) is the
249+
# backbone of time-based sortable UUIDs. Normally, time-based UUIDs will be
250+
# monotonic due to an embedded timestamp; however, implementations can
251+
# guarantee additional monotonicity via the concepts covered in section 6.2.
252+
case Keyword.get(opts, :monotonic_method) do
253+
# Millisecond granularity only. No monotonic guarantee.
254+
nil ->
255+
milliseconds = System.system_time(:millisecond)
256+
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
257+
<<milliseconds::48, 7::4, rand_a::12, 2::2, rand_b::62>>
258+
259+
# For single-node UUID implementations that do not need to create
260+
# batches of UUIDs, the embedded timestamp within UUIDv7 can provide
261+
# sufficient monotonicity guarantees by simply ensuring that timestamp
262+
# increments before creating a new UUID. (RFC9562§6.2)
263+
:millisecond ->
264+
milliseconds = next_ascending(:millisecond)
265+
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
266+
<<milliseconds::48, 7::4, rand_a::12, 2::2, rand_b::62>>
267+
268+
# Replace Leftmost Random Bits with Increased Clock Precision (RFC9562§6.2, Method 3):
269+
:increased_clock_precision ->
270+
nanoseconds = next_ascending(:nanosecond)
271+
milliseconds = div(nanoseconds, @ns_per_ms)
272+
clock_precision = (rem(nanoseconds, @ns_per_ms) * @possible_values) |> div(@ns_per_ms)
273+
<<_rand_a::2, rand_b::62>> = :crypto.strong_rand_bytes(8)
274+
<<milliseconds::48, 7::4, clock_precision::12, 2::2, rand_b::62>>
275+
276+
method ->
277+
raise ArgumentError, "invalid monotonic method: #{inspect(method)}"
278+
end
279+
end
280+
281+
defp next_ascending(time_unit) when time_unit in [:millisecond, :nanosecond] do
282+
timestamp_ref =
283+
with nil <- :persistent_term.get({__MODULE__, time_unit}, nil) do
284+
timestamp_ref = :atomics.new(1, signed: false)
285+
:ok = :persistent_term.put({__MODULE__, time_unit}, timestamp_ref)
286+
timestamp_ref
287+
end
288+
289+
step =
290+
case time_unit do
291+
:millisecond -> 1
292+
:nanosecond -> @minimal_step_ns
293+
end
294+
295+
previous_ts = :atomics.get(timestamp_ref, 1)
296+
min_step_ts = previous_ts + step
297+
current_ts = System.system_time(time_unit)
298+
299+
# If the current timestamp is not at least the minimal step greater than the
300+
# previous step, then we make it so.
301+
new_ts =
302+
if current_ts > min_step_ts do
303+
current_ts
304+
else
305+
min_step_ts
306+
end
307+
308+
compare_exchange(timestamp_ref, previous_ts, new_ts, step)
309+
end
235310

236-
<<milliseconds::48, 7::4, u0::12, 2::2, u1::62>>
311+
defp compare_exchange(timestamp_ref, previous_ts, new_ts, step) do
312+
case :atomics.compare_exchange(timestamp_ref, 1, previous_ts, new_ts) do
313+
# If the new value was written, then we return it.
314+
:ok -> new_ts
315+
# If the atomic value has changed in the meantime, we add the minimal step
316+
# nanoseconds value to that and try again.
317+
updated_ts -> compare_exchange(timestamp_ref, updated_ts, updated_ts + step, step)
318+
end
237319
end
238320

239321
# Callback invoked by autogenerate fields.

0 commit comments

Comments
 (0)