Skip to content

Commit d711a1c

Browse files
Copilotsimbo1905
andcommitted
Replace timeThenRandom with UUIDv7 backport from Java 26
Implemented proper UUIDv7 (RFC 9562) as specified in JDK-8334015. The ofEpochMillis() method creates type 7 UUIDs with: - 48-bit Unix timestamp in milliseconds - Version 7 identifier (4 bits) - IETF variant (2 bits) - 74 random bits from SecureRandom Benefits: - Time-based sortability for database applications - Standards-compliant RFC 9562 implementation - Monotonicity when timestamps are monotonic Added 7 new tests for UUIDv7 conformance: - Version and variant verification - Timestamp embedding and extraction - Monotonicity validation - Invalid timestamp rejection - Uniqueness with same timestamp Co-authored-by: simbo1905 <322608+simbo1905@users.noreply.github.com>
1 parent 248c577 commit d711a1c

File tree

2 files changed

+180
-17
lines changed

2 files changed

+180
-17
lines changed

json-java21/src/main/java/jdk/sandbox/demo/UUIDGenerator.java

Lines changed: 81 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,22 @@
1111

1212
/// UUID Generator providing time-ordered globally unique IDs.
1313
///
14+
/// This is a backport of UUIDv7 from Java 26 (JDK-8334015: https://bugs.openjdk.org/browse/JDK-8334015)
15+
///
16+
/// UUIDv7's time-based sortability makes it an attractive option for globally unique identifiers,
17+
/// especially in database applications. (https://www.rfc-editor.org/rfc/rfc9562#name-uuid-version-7)
18+
///
19+
/// As DBMS vendors add support for UUIDv7 (https://commitfest.postgresql.org/47/4388/) Java users
20+
/// will not easily take advantage of its benefits until it is included in the Java core libraries.
21+
///
1422
/// Generation:
15-
/// - timeThenRandom: Time-ordered globally unique 128-bit identifier
23+
/// - ofEpochMillis: Creates a UUIDv7 from Unix Epoch timestamp (backport from Java 26)
1624
/// - uniqueThenTime: User-ID-then-time-ordered 128-bit identifier
1725
///
1826
/// Formatting:
1927
/// - formatAsUUID: RFC 4122 format with dashes, 36 characters, uppercase or lowercase
2028
/// - formatAsDenseKey: Base62 encoded, 22 characters, zero-padded fixed-width
2129
///
22-
/// Note: Intended usage is one instance per JVM process. Multiple instances
23-
/// in the same process do not guarantee uniqueness due to shared sequence counter.
24-
///
2530
/// Note: 22-character keys are larger than Firebase push IDs (20 characters)
2631
/// but provide full 128-bit time-ordered randomized identifiers.
2732
public class UUIDGenerator {
@@ -52,14 +57,70 @@ static long timeCounterBits() {
5257

5358
// Generation - Public API
5459

55-
/// ┌──────────────────────────────────────────────────────────────────────────────┐
56-
/// │ time+counter (64 bits) │ random (64 bits) │
57-
/// └──────────────────────────────────────────────────────────────────────────────┘
58-
public static UUID timeThenRandom() {
59-
long msb = timeCounterBits();
60-
long lsb = getRandom().nextLong();
60+
/// Creates a type 7 UUID (UUIDv7) {@code UUID} from the given Unix Epoch timestamp.
61+
///
62+
/// The returned {@code UUID} will have the given {@code timestamp} in
63+
/// the first 6 bytes, followed by the version and variant bits representing {@code UUIDv7},
64+
/// and the remaining bytes will contain random data from a cryptographically strong
65+
/// pseudo-random number generator.
66+
///
67+
/// @apiNote {@code UUIDv7} values are created by allocating a Unix timestamp in milliseconds
68+
/// in the most significant 48 bits, allocating the required version (4 bits) and variant (2-bits)
69+
/// and filling the remaining 74 bits with random bits. As such, this method rejects {@code timestamp}
70+
/// values that do not fit into 48 bits.
71+
/// <p>
72+
/// Monotonicity (each subsequent value being greater than the last) is a primary characteristic
73+
/// of {@code UUIDv7} values. This is due to the {@code timestamp} value being part of the {@code UUID}.
74+
/// Callers of this method that wish to generate monotonic {@code UUIDv7} values are expected to
75+
/// ensure that the given {@code timestamp} value is monotonic.
76+
///
77+
/// @param timestamp the number of milliseconds since midnight 1 Jan 1970 UTC,
78+
/// leap seconds excluded.
79+
///
80+
/// @return a {@code UUID} constructed using the given {@code timestamp}
81+
///
82+
/// @throws IllegalArgumentException if the timestamp is negative or greater than {@code (1L << 48) - 1}
83+
///
84+
/// @since Backport from Java 26 (JDK-8334015)
85+
public static UUID ofEpochMillis(long timestamp) {
86+
if ((timestamp >> 48) != 0) {
87+
throw new IllegalArgumentException("Supplied timestamp: " + timestamp + " does not fit within 48 bits");
88+
}
89+
90+
SecureRandom ng = getRandom();
91+
byte[] randomBytes = new byte[16];
92+
ng.nextBytes(randomBytes);
93+
94+
// Embed the timestamp into the first 6 bytes
95+
randomBytes[0] = (byte)(timestamp >> 40);
96+
randomBytes[1] = (byte)(timestamp >> 32);
97+
randomBytes[2] = (byte)(timestamp >> 24);
98+
randomBytes[3] = (byte)(timestamp >> 16);
99+
randomBytes[4] = (byte)(timestamp >> 8);
100+
randomBytes[5] = (byte)(timestamp);
101+
102+
// Set version to 7
103+
randomBytes[6] &= 0x0f;
104+
randomBytes[6] |= 0x70;
105+
106+
// Set variant to IETF
107+
randomBytes[8] &= 0x3f;
108+
randomBytes[8] |= (byte) 0x80;
109+
110+
// Convert byte array to UUID using ByteBuffer
111+
ByteBuffer buffer = ByteBuffer.wrap(randomBytes);
112+
long msb = buffer.getLong();
113+
long lsb = buffer.getLong();
61114
return new UUID(msb, lsb);
62115
}
116+
117+
/// Convenience method to create a UUIDv7 from the current system time.
118+
/// Equivalent to {@code ofEpochMillis(System.currentTimeMillis())}.
119+
///
120+
/// @return a {@code UUID} constructed using the current system time
121+
public static UUID timeThenRandom() {
122+
return ofEpochMillis(System.currentTimeMillis());
123+
}
63124

64125
/// ┌──────────────────────────────────────────────────────────────────────────────┐
65126
/// │ unique (64 bits) │ time+counter (44 bits) │ random (20 bits) │
@@ -134,12 +195,18 @@ public static String formatAsDenseKey(UUID uuid) {
134195
}
135196

136197
public static void main(String[] args) {
198+
// Test UUIDv7 with current time
137199
UUID uuid1 = UUIDGenerator.timeThenRandom();
138-
System.out.println("UUID: " + UUIDGenerator.formatAsUUID(uuid1));
200+
System.out.println("UUIDv7: " + UUIDGenerator.formatAsUUID(uuid1));
139201
System.out.println("Dense: " + UUIDGenerator.formatAsDenseKey(uuid1));
140202

141-
UUID uuid2 = UUIDGenerator.uniqueThenTime(0x123456789ABCDEF0L);
142-
System.out.println("Unique UUID: " + UUIDGenerator.formatAsUUID(uuid2, true));
143-
System.out.println("Unique Dense: " + UUIDGenerator.formatAsDenseKey(uuid2));
203+
// Test UUIDv7 with specific timestamp
204+
UUID uuid2 = UUIDGenerator.ofEpochMillis(System.currentTimeMillis());
205+
System.out.println("UUIDv7 (explicit): " + UUIDGenerator.formatAsUUID(uuid2));
206+
207+
// Test uniqueThenTime
208+
UUID uuid3 = UUIDGenerator.uniqueThenTime(0x123456789ABCDEF0L);
209+
System.out.println("Unique UUID: " + UUIDGenerator.formatAsUUID(uuid3, true));
210+
System.out.println("Unique Dense: " + UUIDGenerator.formatAsDenseKey(uuid3));
144211
}
145212
}

json-java21/src/test/java/jdk/sandbox/demo/UUIDGeneratorTest.java

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,27 @@ void testTimeThenRandomGeneratesUniqueUUIDs() {
3434

3535
@Test
3636
void testTimeThenRandomIncreasingOrder() {
37+
// UUIDv7 uses millisecond timestamps in the first 48 bits.
38+
// Within the same millisecond, ordering is not guaranteed due to random bits.
39+
// This test verifies that UUIDs generated with different timestamps are ordered.
3740
UUID prev = UUIDGenerator.timeThenRandom();
38-
for (int i = 0; i < 100; i++) {
41+
try {
42+
Thread.sleep(2); // Ensure at least 1ms passes
43+
} catch (InterruptedException e) {
44+
Thread.currentThread().interrupt();
45+
}
46+
47+
for (int i = 0; i < 10; i++) {
3948
UUID current = UUIDGenerator.timeThenRandom();
40-
// MSB should be increasing (time+counter)
49+
// MSB should be increasing when timestamps differ
4150
assertTrue(current.getMostSignificantBits() >= prev.getMostSignificantBits(),
42-
"UUIDs should be time-ordered");
51+
"UUIDs with different timestamps should be time-ordered");
4352
prev = current;
53+
try {
54+
Thread.sleep(1); // Small delay to ensure different timestamps
55+
} catch (InterruptedException e) {
56+
Thread.currentThread().interrupt();
57+
}
4458
}
4559
}
4660

@@ -235,4 +249,86 @@ void testFormatAsDenseKeyDeterministic() {
235249

236250
assertEquals(key1, key2, "Same UUID should produce same dense key");
237251
}
252+
253+
@Test
254+
void testOfEpochMillisVersion7() {
255+
// Test that generated UUID has version 7
256+
long timestamp = System.currentTimeMillis();
257+
UUID uuid = UUIDGenerator.ofEpochMillis(timestamp);
258+
259+
// Extract version bits (bits 48-51 of the UUID)
260+
long msb = uuid.getMostSignificantBits();
261+
int version = (int)((msb >> 12) & 0x0F);
262+
263+
assertEquals(7, version, "UUID should have version 7");
264+
}
265+
266+
@Test
267+
void testOfEpochMillisVariantIETF() {
268+
// Test that generated UUID has IETF variant
269+
long timestamp = System.currentTimeMillis();
270+
UUID uuid = UUIDGenerator.ofEpochMillis(timestamp);
271+
272+
// Extract variant bits (bits 64-65 of the UUID)
273+
long lsb = uuid.getLeastSignificantBits();
274+
int variant = (int)((lsb >> 62) & 0x03);
275+
276+
assertEquals(2, variant, "UUID should have IETF variant (2)");
277+
}
278+
279+
@Test
280+
void testOfEpochMillisTimestampExtraction() {
281+
// Test that timestamp can be extracted from UUID
282+
long timestamp = 1000000000000L;
283+
UUID uuid = UUIDGenerator.ofEpochMillis(timestamp);
284+
285+
// Extract timestamp from first 48 bits
286+
long msb = uuid.getMostSignificantBits();
287+
long extractedTimestamp = msb >>> 16; // Shift right 16 bits to get top 48 bits
288+
289+
assertEquals(timestamp, extractedTimestamp, "Timestamp should be embedded in UUID");
290+
}
291+
292+
@Test
293+
void testOfEpochMillisMonotonicity() {
294+
// Test that UUIDs with increasing timestamps are monotonic
295+
UUID uuid1 = UUIDGenerator.ofEpochMillis(1000000000000L);
296+
UUID uuid2 = UUIDGenerator.ofEpochMillis(1000000000001L);
297+
UUID uuid3 = UUIDGenerator.ofEpochMillis(1000000000002L);
298+
299+
assertTrue(uuid2.compareTo(uuid1) > 0, "UUID2 should be greater than UUID1");
300+
assertTrue(uuid3.compareTo(uuid2) > 0, "UUID3 should be greater than UUID2");
301+
}
302+
303+
@Test
304+
void testOfEpochMillisInvalidTimestamp() {
305+
// Test that timestamps that don't fit in 48 bits are rejected
306+
long invalidTimestamp = (1L << 48); // 2^48, doesn't fit in 48 bits
307+
308+
assertThrows(IllegalArgumentException.class, () -> {
309+
UUIDGenerator.ofEpochMillis(invalidTimestamp);
310+
}, "Should throw IllegalArgumentException for timestamp that doesn't fit in 48 bits");
311+
}
312+
313+
@Test
314+
void testOfEpochMillisNegativeTimestamp() {
315+
// Test that negative timestamps are rejected
316+
long negativeTimestamp = -1L;
317+
318+
assertThrows(IllegalArgumentException.class, () -> {
319+
UUIDGenerator.ofEpochMillis(negativeTimestamp);
320+
}, "Should throw IllegalArgumentException for negative timestamp");
321+
}
322+
323+
@Test
324+
void testOfEpochMillisUniqueness() {
325+
// Test that multiple UUIDs with same timestamp are still unique (due to random bits)
326+
long timestamp = System.currentTimeMillis();
327+
Set<UUID> uuids = new HashSet<>();
328+
329+
for (int i = 0; i < 100; i++) {
330+
UUID uuid = UUIDGenerator.ofEpochMillis(timestamp);
331+
assertTrue(uuids.add(uuid), "Each UUID should be unique even with same timestamp");
332+
}
333+
}
238334
}

0 commit comments

Comments
 (0)