@@ -32,7 +32,25 @@ defmodule Ecto.UUID do
3232 @ typedoc """
3333 currently supported option is version, it accepts 4 or 7.
3434 """
35- @ type options :: [ version: 4 | 7 ]
35+ @ type option ::
36+ { :version , 4 | 7 }
37+ | { :monotonic_method , :millisecond | :increased_clock_precision }
38+
39+ @ type options :: [ option ]
40+
41+ # The bits available for sub-millisecond fractions when using increased clock
42+ # precision.
43+ @ sub_ms_bits 12
44+
45+ # The number of values that can be represented in the bit space (2^12).
46+ @ possible_values Bitwise . bsl ( 1 , @ sub_ms_bits )
47+
48+ # The number of nanoseconds in a millisecond.
49+ @ ns_per_ms 1_000_000
50+
51+ # The minimum step when using increased clock precision with fractional
52+ # milliseconds.
53+ @ minimal_step_ns div ( @ ns_per_ms , @ possible_values )
3654
3755 @ doc false
3856 def type , do: :uuid
@@ -217,10 +235,10 @@ defmodule Ecto.UUID do
217235 """
218236 @ spec bingenerate ( options ) :: raw
219237 def bingenerate ( opts \\ [ ] ) do
220- case Keyword . get ( opts , :version , @ default_version ) do
221- 4 -> bingenerate_v4 ( )
222- 7 -> bingenerate_v7 ( )
223- version -> raise ArgumentError , "unknown UUID version: #{ inspect ( version ) } "
238+ case Keyword . pop ( opts , :version , @ default_version ) do
239+ { 4 , _opts } -> bingenerate_v4 ( )
240+ { 7 , opts } -> bingenerate_v7 ( opts )
241+ { version , _ } -> raise ArgumentError , "unsupported UUID version: #{ inspect ( version ) } "
224242 end
225243 end
226244
@@ -229,11 +247,79 @@ defmodule Ecto.UUID do
229247 << u0 :: 48 , 4 :: 4 , u1 :: 12 , 2 :: 2 , u2 :: 62 >>
230248 end
231249
232- defp bingenerate_v7 do
233- milliseconds = System . system_time ( :millisecond )
234- << u0 :: 12 , u1 :: 62 , _ :: 6 >> = :crypto . strong_rand_bytes ( 10 )
250+ defp bingenerate_v7 ( opts ) do
251+ # From RFC 9562:
252+ # Monotonicity (each subsequent value being greater than the last) is the
253+ # backbone of time-based sortable UUIDs. Normally, time-based UUIDs will be
254+ # monotonic due to an embedded timestamp; however, implementations can
255+ # guarantee additional monotonicity via the concepts covered in section 6.2.
256+ case Keyword . get ( opts , :monotonic_method ) do
257+ # Millisecond granularity only. No monotonic guarantee.
258+ nil ->
259+ milliseconds = System . system_time ( :millisecond )
260+ << rand_a :: 12 , _ :: 6 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 10 )
261+ << milliseconds :: 48 , 7 :: 4 , rand_a :: 12 , 2 :: 2 , rand_b :: 62 >>
262+
263+ # For single-node UUID implementations that do not need to create
264+ # batches of UUIDs, the embedded timestamp within UUIDv7 can provide
265+ # sufficient monotonicity guarantees by simply ensuring that timestamp
266+ # increments before creating a new UUID. (RFC9562§6.2)
267+ :millisecond ->
268+ milliseconds = next_ascending ( :millisecond )
269+ << rand_a :: 12 , _ :: 6 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 10 )
270+ << milliseconds :: 48 , 7 :: 4 , rand_a :: 12 , 2 :: 2 , rand_b :: 62 >>
271+
272+ # Replace Leftmost Random Bits with Increased Clock Precision (RFC9562§6.2, Method 3):
273+ :increased_clock_precision ->
274+ nanoseconds = next_ascending ( :nanosecond )
275+ milliseconds = div ( nanoseconds , @ ns_per_ms )
276+ clock_precision = ( rem ( nanoseconds , @ ns_per_ms ) * @ possible_values ) |> div ( @ ns_per_ms )
277+ << _rand_a :: 2 , rand_b :: 62 >> = :crypto . strong_rand_bytes ( 8 )
278+ << milliseconds :: 48 , 7 :: 4 , clock_precision :: 12 , 2 :: 2 , rand_b :: 62 >>
279+
280+ method ->
281+ raise ArgumentError , "invalid monotonic method: #{ inspect ( method ) } "
282+ end
283+ end
235284
236- << milliseconds :: 48 , 7 :: 4 , u0 :: 12 , 2 :: 2 , u1 :: 62 >>
285+ defp next_ascending ( time_unit ) when time_unit in [ :millisecond , :nanosecond ] do
286+ timestamp_ref =
287+ with nil <- :persistent_term . get ( { __MODULE__ , time_unit } , nil ) do
288+ timestamp_ref = :atomics . new ( 1 , signed: false )
289+ :ok = :persistent_term . put ( { __MODULE__ , time_unit } , timestamp_ref )
290+ timestamp_ref
291+ end
292+
293+ step =
294+ case time_unit do
295+ :millisecond -> 1
296+ :nanosecond -> @ minimal_step_ns
297+ end
298+
299+ previous_ts = :atomics . get ( timestamp_ref , 1 )
300+ min_step_ts = previous_ts + step
301+ current_ts = System . system_time ( time_unit )
302+
303+ # If the current timestamp is not at least the minimal step greater than the
304+ # previous step, then we make it so.
305+ new_ts =
306+ if current_ts > min_step_ts do
307+ current_ts
308+ else
309+ min_step_ts
310+ end
311+
312+ compare_exchange ( timestamp_ref , previous_ts , new_ts , step )
313+ end
314+
315+ defp compare_exchange ( timestamp_ref , previous_ts , new_ts , step ) do
316+ case :atomics . compare_exchange ( timestamp_ref , 1 , previous_ts , new_ts ) do
317+ # If the new value was written, then we return it.
318+ :ok -> new_ts
319+ # If the atomic value has changed in the meantime, we add the minimal step
320+ # nanoseconds value to that and try again.
321+ updated_ts -> compare_exchange ( timestamp_ref , updated_ts , updated_ts + step , step )
322+ end
237323 end
238324
239325 # Callback invoked by autogenerate fields.
0 commit comments