@@ -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