Skip to content

Commit dccb7f8

Browse files
uuidv7 monotonicity
1 parent e6da70d commit dccb7f8

File tree

1 file changed

+101
-7
lines changed

1 file changed

+101
-7
lines changed

lib/ecto/uuid.ex

Lines changed: 101 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,91 @@ 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+
#
253+
# Take care to ensure UUIDs generated in batches are also monotonic. That
254+
# is, if one thousand UUIDs are generated for the same timestamp, there
255+
# should be sufficient logic for organizing the creation order of those
256+
# one thousand UUIDs. Batch UUID creation implementations MAY utilize a
257+
# monotonic counter that increments for each UUID created during a given
258+
# timestamp.
259+
case Keyword.get(opts, :monotonic_method) do
260+
# Millisecond granularity only. No monotonic guarantee.
261+
nil ->
262+
milliseconds = System.system_time(:millisecond)
263+
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
264+
<<milliseconds::48, 7::4, rand_a::12, 2::2, rand_b::62>>
265+
266+
# For single-node UUID implementations that do not need to create
267+
# batches of UUIDs, the embedded timestamp within UUIDv7 can provide
268+
# sufficient monotonicity guarantees by simply ensuring that timestamp
269+
# increments before creating a new UUID. (RFC9562§6.2)
270+
:millisecond ->
271+
milliseconds = next_ascending(:millisecond)
272+
<<rand_a::12, _::6, rand_b::62>> = :crypto.strong_rand_bytes(10)
273+
<<milliseconds::48, 7::4, rand_a::12, 2::2, rand_b::62>>
274+
275+
# Replace Leftmost Random Bits with Increased Clock Precision (RFC9562§6.2, Method 3):
276+
:increased_clock_precision ->
277+
nanoseconds = next_ascending(:nanosecond)
278+
milliseconds = div(nanoseconds, @ns_per_ms)
279+
clock_precision = (rem(nanoseconds, @ns_per_ms) * @possible_values) |> div(@ns_per_ms)
280+
<<_rand_a::2, rand_b::62>> = :crypto.strong_rand_bytes(8)
281+
<<milliseconds::48, 7::4, clock_precision::12, 2::2, rand_b::62>>
282+
283+
method ->
284+
raise ArgumentError, "invalid monotonic method: #{inspect(method)}"
285+
end
286+
end
287+
288+
defp next_ascending(time_unit) when time_unit in [:millisecond, :nanosecond] do
289+
timestamp_ref =
290+
with nil <- :persistent_term.get({__MODULE__, time_unit}, nil) do
291+
timestamp_ref = :atomics.new(1, signed: false)
292+
:ok = :persistent_term.put({__MODULE__, time_unit}, timestamp_ref)
293+
timestamp_ref
294+
end
295+
296+
step =
297+
case time_unit do
298+
:millisecond -> 1
299+
:nanosecond -> @minimal_step_ns
300+
end
301+
302+
previous_ts = :atomics.get(timestamp_ref, 1)
303+
current_ts = System.system_time(time_unit)
304+
305+
min_step_ts =
306+
case time_unit do
307+
:millisecond -> current_ts + step
308+
:nanosecond -> previous_ts + step
309+
end
310+
311+
# If the current timestamp is not at least the minimal step greater than the
312+
# previous step, then we make it so.
313+
new_ts =
314+
if current_ts > min_step_ts do
315+
current_ts
316+
else
317+
min_step_ts
318+
end
319+
320+
compare_exchange(timestamp_ref, previous_ts, new_ts, step)
321+
end
235322

236-
<<milliseconds::48, 7::4, u0::12, 2::2, u1::62>>
323+
defp compare_exchange(timestamp_ref, previous_ts, new_ts, step) do
324+
case :atomics.compare_exchange(timestamp_ref, 1, previous_ts, new_ts) do
325+
# If the new value was written, then we return it.
326+
:ok -> new_ts
327+
# If the atomic value has changed in the meantime, we add the minimal step
328+
# nanoseconds value to that and try again.
329+
updated_ts -> compare_exchange(timestamp_ref, updated_ts, updated_ts + step, step)
330+
end
237331
end
238332

239333
# Callback invoked by autogenerate fields.

0 commit comments

Comments
 (0)