Skip to content

Commit 3d14798

Browse files
fzhinkinSpace Team
authored andcommitted
KT-81092 Add a function to generate V7 Uuids for a given timestamp
^KT-81092 Fixed Merge-request: KT-MR-23496 Merged-by: Filipp Zhinkin <[email protected]>
1 parent 4fc42d7 commit 3d14798

File tree

5 files changed

+125
-0
lines changed

5 files changed

+125
-0
lines changed

libraries/stdlib/samples/test/samples/uuid/uuid.kt

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ package samples.uuid
77

88
import samples.*
99
import kotlin.test.*
10+
import kotlin.time.Instant
1011
import kotlin.uuid.*
1112

1213
@OptIn(ExperimentalUuidApi::class)
@@ -256,6 +257,36 @@ class Uuids {
256257
assertPrints(uuid2 < uuid3, "true")
257258
}
258259

260+
@Sample
261+
fun v7NonMonotonicAt() {
262+
val timestamp = Instant.fromEpochMilliseconds(1757440583000L)
263+
264+
val uuid1 = Uuid.generateV7NonMonotonicAt(timestamp)
265+
val uuid2 = Uuid.generateV7NonMonotonicAt(timestamp)
266+
267+
// Uuids have the same timestamp prefix (01992f9f-2558-, which corresponds to 1757440583000 in hex),
268+
// but other bytes are different.
269+
assertTrue(uuid1.toHexDashString().startsWith("01992f9f-2558-"))
270+
println(uuid1.toHexDashString())
271+
272+
assertTrue(uuid2.toHexDashString().startsWith("01992f9f-2558-"))
273+
println(uuid2.toHexDashString())
274+
275+
assertNotEquals(uuid1, uuid2)
276+
}
277+
278+
@Sample
279+
fun v7ForTimestampSorted() {
280+
val timestamp = Instant.fromEpochMilliseconds(1757440583000L)
281+
282+
// Generate 5 uuids for the same timestamp, and them explicitly sort them to ensure monotonicity
283+
val uuids = sequence<Uuid> { Uuid.generateV7NonMonotonicAt(timestamp) }.take(5).sorted().toList()
284+
285+
for (idx in 1..<uuids.size) {
286+
assertTrue(uuids[idx - 1] < uuids[idx])
287+
}
288+
}
289+
259290
@Sample
260291
fun compareTo() {
261292
val uuid1 = Uuid.parse("49d6d991-c780-4eb5-8585-5169c25af912")

libraries/stdlib/src/kotlin/uuid/Uuid.kt

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import kotlin.internal.ReadObjectParameterType
1919
import kotlin.internal.throwReadObjectNotSupported
2020
import kotlin.time.Clock
2121
import kotlin.time.ExperimentalTime
22+
import kotlin.time.Instant
2223

2324
/**
2425
* Represents a Universally Unique Identifier (UUID), also known as a Globally Unique Identifier (GUID).
@@ -669,6 +670,77 @@ public class Uuid private constructor(
669670
@OptIn(ExperimentalTime::class)
670671
public fun generateV7(): Uuid = generateV7(Clock.System)
671672

673+
/**
674+
* Generates a new random [Uuid] Version 7 instance for a specified [moment in time][timestamp].
675+
*
676+
* The returned uuid is a time-based sortable UUID that conforms to the
677+
* [IETF variant (variant 2)](https://www.rfc-editor.org/rfc/rfc9562.html#section-4.1)
678+
* and [version 7](https://www.rfc-editor.org/rfc/rfc9562.html#section-4.2),
679+
* uses UNIX timestamp in milliseconds extracted from [timestamp] as a prefix and a randomly generated suffix.
680+
*
681+
* Unlike [generateV7], this function does not provide any monotonicity guarantees, meaning that there will be no guaranteed order
682+
* for uuids created by calling this function two or more times with exactly the same [timestamp].
683+
* If multiple uuids corresponding to the same timestamp are needed, consider generating them using this function,
684+
* then sorting the result before using it to achieve the monotonicity.
685+
*
686+
* This function is aimed for generating v7 uuids corresponding to the past (or the future).
687+
* Always consider using [generateV7] if an uuid corresponding to a current moment in time in needed.
688+
*
689+
* This function does not affect the state and monotonicity guarantees of [generateV7] in any way,
690+
* so even if it is invoked with a timestamp from a distant future,
691+
* [generateV7] will continue returning uuids with a timestamp corresponding to a current moment in time.
692+
*
693+
* The random part of the uuid is produced using a cryptographically secure pseudorandom number generator (CSPRNG)
694+
* available on the platform.
695+
* If the underlying system has not collected enough entropy, this function
696+
* may block until sufficient entropy is collected, and the CSPRNG is fully initialized.
697+
* It is worth mentioning
698+
* that the PRNG used in the Kotlin/WasmWasi target is not guaranteed to be cryptographically secure.
699+
* See the list below for details about the API used for producing the random uuid in each supported target.
700+
*
701+
* Note that the returned uuid is not recommended for use for cryptographic purposes.
702+
* Because version 7 uuid has a partially predictable bit pattern, and utilizes at most
703+
* 74 bits of entropy, regardless of platform.
704+
*
705+
* The following APIs are used for producing the random uuid in each of the supported targets:
706+
* - Kotlin/JVM - [java.security.SecureRandom](https://docs.oracle.com/javase/8/docs/api/java/security/SecureRandom.html)
707+
* - Kotlin/JS - [Crypto.getRandomValues()](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues)
708+
* - Kotlin/WasmJs - [Crypto.getRandomValues()](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues)
709+
* - Kotlin/WasmWasi - [random_get](https://github.com/WebAssembly/WASI/blob/main/legacy/preview1/docs.md#random_get)
710+
* - Kotlin/Native:
711+
* - Linux targets - [getrandom](https://www.man7.org/linux/man-pages/man2/getrandom.2.html)
712+
* - Apple and Android Native targets - [arc4random_buf](https://man7.org/linux/man-pages/man3/arc4random_buf.3.html)
713+
* - Windows targets - [BCryptGenRandom](https://learn.microsoft.com/en-us/windows/win32/api/bcrypt/nf-bcrypt-bcryptgenrandom)
714+
*
715+
* Note that the underlying API used to produce random uuids may change in the future.
716+
*
717+
* @return A randomly generated uuid.
718+
* @throws RuntimeException if the underlying API fails. Refer to the corresponding underlying API
719+
* documentation for possible reasons for failure and guidance on how to handle them.
720+
*
721+
* @sample samples.uuid.Uuids.v7NonMonotonicAt
722+
* @sample samples.uuid.Uuids.v7ForTimestampSorted
723+
*/
724+
@SinceKotlin("2.3")
725+
@ExperimentalTime
726+
public fun generateV7NonMonotonicAt(timestamp: Instant): Uuid {
727+
val randomBytes = ByteArray(10).also {
728+
secureRandomBytes(it)
729+
}
730+
731+
val verAndRandA = randomBytes[8].toInt().and(0x0F).or(0x70).shl(8)
732+
.or(randomBytes[9].toInt().and(0xFF))
733+
734+
val tsVerAndRandA = timestamp.toEpochMilliseconds().shl(16).or(verAndRandA.toLong())
735+
736+
randomBytes[0] = randomBytes[0]
737+
.and(0x3F) // clear two MSBs
738+
.or(0x80.toByte()) // set then to 0b10
739+
val varAndRandB = randomBytes.getLongAt(0)
740+
741+
return fromLongs(tsVerAndRandA, varAndRandB)
742+
}
743+
672744
@OptIn(ExperimentalTime::class)
673745
internal fun generateV7(clock: Clock): Uuid = UuidV7Generator.generate(clock)
674746

libraries/stdlib/test/uuid/UuidTest.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -461,6 +461,26 @@ class UuidTest {
461461
}
462462
}
463463

464+
@Test
465+
fun testV7GenerateUnordered() {
466+
val uuid = Uuid.generateV7NonMonotonicAt(Instant.fromEpochMilliseconds(0L))
467+
468+
assertEquals(7, uuid.version, "Generated Uuid has a wrong version: ${uuid.toHexDashString()}")
469+
assertTrue(uuid.isIetfVariant, "Generated Uuid has a wrong variant: ${uuid.toHexDashString()}")
470+
uuid.toLongs { msb, _ ->
471+
assertEquals(msb.shr(16), 0L, "Timestamp field has a wrong value")
472+
}
473+
474+
val anotherUuid = Uuid.generateV7NonMonotonicAt(Instant.fromEpochMilliseconds(0L))
475+
assertNotEquals(uuid, anotherUuid, "Two uuids generated for the same timestamp should not be equal")
476+
477+
val tsValue = 1757440583123L
478+
val nonZeroTs = Uuid.generateV7NonMonotonicAt(Instant.fromEpochMilliseconds(tsValue))
479+
nonZeroTs.toLongs { msb, _ ->
480+
assertEquals(msb.shr(16), tsValue, "Timestamp field has a wrong value")
481+
}
482+
}
483+
464484
private class NonMonotonicClock : Clock {
465485
private var currentTime = Clock.System.now()
466486

libraries/tools/binary-compatibility-validator/klib-public-api/kotlin-stdlib.api

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2112,6 +2112,7 @@ final class kotlin.uuid/Uuid : kotlin.io/Serializable, kotlin/Comparable<kotlin.
21122112
final fun fromULongs(kotlin/ULong, kotlin/ULong): kotlin.uuid/Uuid // kotlin.uuid/Uuid.Companion.fromULongs|fromULongs(kotlin.ULong;kotlin.ULong){}[0]
21132113
final fun generateV4(): kotlin.uuid/Uuid // kotlin.uuid/Uuid.Companion.generateV4|generateV4(){}[0]
21142114
final fun generateV7(): kotlin.uuid/Uuid // kotlin.uuid/Uuid.Companion.generateV7|generateV7(){}[0]
2115+
final fun generateV7NonMonotonicAt(kotlin.time/Instant): kotlin.uuid/Uuid // kotlin.uuid/Uuid.Companion.generateV7NonMonotonicAt|generateV7NonMonotonicAt(kotlin.time.Instant){}[0]
21152116
final fun parse(kotlin/String): kotlin.uuid/Uuid // kotlin.uuid/Uuid.Companion.parse|parse(kotlin.String){}[0]
21162117
final fun parseHex(kotlin/String): kotlin.uuid/Uuid // kotlin.uuid/Uuid.Companion.parseHex|parseHex(kotlin.String){}[0]
21172118
final fun parseHexDash(kotlin/String): kotlin.uuid/Uuid // kotlin.uuid/Uuid.Companion.parseHexDash|parseHexDash(kotlin.String){}[0]

libraries/tools/binary-compatibility-validator/reference-public-api/kotlin-stdlib-runtime-merged.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6496,6 +6496,7 @@ public final class kotlin/uuid/Uuid$Companion {
64966496
public final fun fromULongs-eb3DHEI (JJ)Lkotlin/uuid/Uuid;
64976497
public final fun generateV4 ()Lkotlin/uuid/Uuid;
64986498
public final fun generateV7 ()Lkotlin/uuid/Uuid;
6499+
public final fun generateV7NonMonotonicAt (Lkotlin/time/Instant;)Lkotlin/uuid/Uuid;
64996500
public final fun getLEXICAL_ORDER ()Ljava/util/Comparator;
65006501
public final fun getNIL ()Lkotlin/uuid/Uuid;
65016502
public final fun parse (Ljava/lang/String;)Lkotlin/uuid/Uuid;

0 commit comments

Comments
 (0)