-
Notifications
You must be signed in to change notification settings - Fork 10
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add UniqueIds for improved scalable UUID generation (#1789)
Add UniqueIds for improved scalable UUID generation
- Loading branch information
Showing
7 changed files
with
371 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
type: improvement | ||
improvement: | ||
description: Add UniqueIds for improved scalable UUID generation | ||
links: | ||
- https://github.com/palantir/tritium/pull/1789 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
apply plugin: 'com.palantir.external-publish-jar' | ||
apply plugin: 'me.champeau.jmh' | ||
|
||
jmh { | ||
jvmArgsAppend = ['-Xms2g', '-Xmx2g', '-XX:+UseG1GC'] | ||
} | ||
|
||
dependencies { | ||
implementation 'com.google.guava:guava' | ||
implementation 'com.palantir.safe-logging:logger' | ||
implementation 'com.palantir.safe-logging:preconditions' | ||
|
||
jmhImplementation 'com.fasterxml.uuid:java-uuid-generator' | ||
|
||
testImplementation 'org.assertj:assertj-core' | ||
testImplementation 'org.junit.jupiter:junit-jupiter' | ||
testImplementation 'org.junit.jupiter:junit-jupiter-api' | ||
testImplementation 'org.junit.jupiter:junit-jupiter-params' | ||
} |
132 changes: 132 additions & 0 deletions
132
tritium-ids/src/jmh/java/com/palantir/tritium/ids/UuidBenchmark.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,132 @@ | ||
/* | ||
* (c) Copyright 2022 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.palantir.tritium.ids; | ||
|
||
import com.fasterxml.uuid.Generators; | ||
import java.security.SecureRandom; | ||
import java.util.Random; | ||
import java.util.UUID; | ||
import java.util.concurrent.TimeUnit; | ||
import org.openjdk.jmh.annotations.Benchmark; | ||
import org.openjdk.jmh.annotations.BenchmarkMode; | ||
import org.openjdk.jmh.annotations.Fork; | ||
import org.openjdk.jmh.annotations.Measurement; | ||
import org.openjdk.jmh.annotations.Mode; | ||
import org.openjdk.jmh.annotations.OutputTimeUnit; | ||
import org.openjdk.jmh.annotations.Scope; | ||
import org.openjdk.jmh.annotations.State; | ||
import org.openjdk.jmh.annotations.Threads; | ||
import org.openjdk.jmh.annotations.Warmup; | ||
import org.openjdk.jmh.runner.Runner; | ||
import org.openjdk.jmh.runner.options.OptionsBuilder; | ||
import org.openjdk.jmh.runner.options.TimeValue; | ||
|
||
@State(Scope.Benchmark) | ||
@BenchmarkMode(Mode.Throughput) | ||
@OutputTimeUnit(TimeUnit.MICROSECONDS) | ||
@Warmup(iterations = 3, time = 2, timeUnit = TimeUnit.SECONDS) | ||
@Measurement(iterations = 3, time = 3, timeUnit = TimeUnit.SECONDS) | ||
@Threads(Threads.MAX) | ||
@Fork(1) | ||
@SuppressWarnings({"checkstyle:hideutilityclassconstructor", "VisibilityModifier", "DesignForExtension"}) | ||
public class UuidBenchmark { | ||
|
||
private final SecureRandom secureRandom = new SecureRandom(); | ||
private final SecureRandom sha1secureRandom = UniqueIds.createSecureRandom(); | ||
private final ThreadLocal<Random> threadLocalSecureRandom = ThreadLocal.withInitial(SecureRandom::new); | ||
private final ThreadLocal<Random> threadLocalSha1SecureRandom = | ||
ThreadLocal.withInitial(UniqueIds::createSecureRandom); | ||
|
||
@Benchmark | ||
@Threads(1) | ||
public UUID jdkRandomUuidOne() { | ||
return UUID.randomUUID(); | ||
} | ||
|
||
@Benchmark | ||
@Threads(Threads.MAX) | ||
public UUID jdkRandomUuidMax() { | ||
return UUID.randomUUID(); | ||
} | ||
|
||
@Benchmark | ||
@Threads(1) | ||
public UUID randomUuidV4One() { | ||
return UniqueIds.randomUuidV4(); | ||
} | ||
|
||
@Benchmark | ||
@Threads(Threads.MAX) | ||
public UUID randomUuidV4Max() { | ||
return UniqueIds.randomUuidV4(); | ||
} | ||
|
||
@Benchmark | ||
@Threads(1) | ||
public UUID pseudoRandomUuidV4One() { | ||
return UniqueIds.pseudoRandomUuidV4(); | ||
} | ||
|
||
@Benchmark | ||
@Threads(Threads.MAX) | ||
public UUID pseudoRandomUuidV4Max() { | ||
return UniqueIds.pseudoRandomUuidV4(); | ||
} | ||
|
||
@Benchmark | ||
@Threads(Threads.MAX) | ||
public UUID secureRandomMax() { | ||
return UniqueIds.randomUuidV4(sha1secureRandom); | ||
} | ||
|
||
@Benchmark | ||
@Threads(Threads.MAX) | ||
public UUID threadLocalMax() { | ||
return UniqueIds.randomUuidV4(threadLocalSecureRandom.get()); | ||
} | ||
|
||
@Benchmark | ||
@Threads(Threads.MAX) | ||
public UUID threadLocalSha1Max() { | ||
return UniqueIds.randomUuidV4(threadLocalSha1SecureRandom.get()); | ||
} | ||
|
||
@Benchmark | ||
@Threads(Threads.MAX) | ||
public UUID twoLongsMax() { | ||
return new UUID(secureRandom.nextLong(), secureRandom.nextLong()); | ||
} | ||
|
||
@Benchmark | ||
@Threads(Threads.MAX) | ||
public UUID jugSecureRandomMax() { | ||
return Generators.randomBasedGenerator(sha1secureRandom).generate(); | ||
} | ||
|
||
public static void main(String[] _args) throws Exception { | ||
new Runner(new OptionsBuilder() | ||
.include(UuidBenchmark.class.getSimpleName()) | ||
.forks(1) | ||
.threads(4) | ||
.warmupIterations(3) | ||
.warmupTime(TimeValue.seconds(3)) | ||
.measurementIterations(3) | ||
.measurementTime(TimeValue.seconds(1)) | ||
.build()) | ||
.run(); | ||
} | ||
} |
87 changes: 87 additions & 0 deletions
87
tritium-ids/src/main/java/com/palantir/tritium/ids/UniqueIds.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,87 @@ | ||
/* | ||
* (c) Copyright 2022 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.palantir.tritium.ids; | ||
|
||
import com.google.common.annotations.VisibleForTesting; | ||
import com.palantir.logsafe.Preconditions; | ||
import com.palantir.logsafe.logger.SafeLogger; | ||
import com.palantir.logsafe.logger.SafeLoggerFactory; | ||
import java.security.NoSuchAlgorithmException; | ||
import java.security.SecureRandom; | ||
import java.util.Random; | ||
import java.util.UUID; | ||
import java.util.concurrent.ThreadLocalRandom; | ||
|
||
public final class UniqueIds { | ||
private static final SafeLogger log = SafeLoggerFactory.get(UniqueIds.class); | ||
private static final int CHUNK_SIZE = 16; | ||
|
||
private UniqueIds() {} | ||
|
||
private static final ThreadLocal<Random> SECURE_RANDOM = ThreadLocal.withInitial(UniqueIds::createSecureRandom); | ||
|
||
static SecureRandom createSecureRandom() { | ||
try { | ||
return SecureRandom.getInstance("SHA1PRNG"); | ||
} catch (NoSuchAlgorithmException e) { | ||
log.warn("Falling back to default SecureRandom", e); | ||
return new SecureRandom(); | ||
} | ||
} | ||
|
||
/** | ||
* Returns a pseudo-randomly generated {@link UUID} in IETF RFC 4122 v4 format. | ||
* Note that this should not be used in cases where one must have strong guarantees of cryptographically secure | ||
* randomness and uniqueness. | ||
*/ | ||
public static UUID pseudoRandomUuidV4() { | ||
return randomUuidV4(ThreadLocalRandom.current()); | ||
} | ||
|
||
/** | ||
* Returns a unique, cryptographically secure randomly generated {@link UUID} in IETF RFC 4122 v4 format. | ||
*/ | ||
public static UUID randomUuidV4() { | ||
return randomUuidV4(SECURE_RANDOM.get()); | ||
} | ||
|
||
@VisibleForTesting | ||
static UUID randomUuidV4(Random rand) { | ||
byte[] data = bytes(rand); | ||
Preconditions.checkArgument(data.length == CHUNK_SIZE, "Invalid data length, expected 16 bytes"); | ||
data[6] = (byte) ((data[6] & 0x0f) | 0x40); // version 4 | ||
data[8] = (byte) ((data[8] & 0x3f) | 0x80); // IETF variant | ||
|
||
long mostSigBits = 0; | ||
for (int i = 0; i < 8; i++) { | ||
mostSigBits = (mostSigBits << 8) | (data[i] & 0xff); | ||
} | ||
|
||
long leastSigBits = 0; | ||
for (int i = 8; i < 16; i++) { | ||
leastSigBits = (leastSigBits << 8) | (data[i] & 0xff); | ||
} | ||
|
||
return new UUID(mostSigBits, leastSigBits); | ||
} | ||
|
||
private static byte[] bytes(Random rand) { | ||
byte[] data = new byte[16]; | ||
rand.nextBytes(data); | ||
return data; | ||
} | ||
} |
126 changes: 126 additions & 0 deletions
126
tritium-ids/src/test/java/com/palantir/tritium/ids/UniqueIdsTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
/* | ||
* (c) Copyright 2023 Palantir Technologies Inc. All rights reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package com.palantir.tritium.ids; | ||
|
||
import static org.assertj.core.api.Assertions.assertThat; | ||
import static org.assertj.core.api.Assertions.assertThatThrownBy; | ||
|
||
import com.google.common.collect.Sets; | ||
import com.google.common.util.concurrent.Futures; | ||
import java.util.ArrayList; | ||
import java.util.Iterator; | ||
import java.util.List; | ||
import java.util.Random; | ||
import java.util.Set; | ||
import java.util.UUID; | ||
import java.util.concurrent.Callable; | ||
import java.util.concurrent.ExecutorService; | ||
import java.util.concurrent.ForkJoinPool; | ||
import java.util.concurrent.Future; | ||
import java.util.stream.IntStream; | ||
import java.util.stream.Stream; | ||
import org.junit.jupiter.api.Test; | ||
import org.junit.jupiter.params.ParameterizedTest; | ||
import org.junit.jupiter.params.provider.Arguments; | ||
import org.junit.jupiter.params.provider.MethodSource; | ||
|
||
public final class UniqueIdsTest { | ||
|
||
private final Random random = new Random(1234567890L); | ||
|
||
@Test | ||
public void deterministicRandomSource() { | ||
UUID uuid = UniqueIds.randomUuidV4(random); | ||
assertThat(uuid).isEqualTo(UUID.fromString("ea6addb7-2596-428e-8a77-acf9a43797b3")); | ||
assertThat(uuid.variant()).isEqualTo(2); // IETF RFC 4122 | ||
assertThat(uuid.version()).isEqualTo(4); // v4 Randomly generated UUID | ||
assertThatThrownBy(uuid::clockSequence) | ||
.isInstanceOf(UnsupportedOperationException.class) | ||
.hasMessageContaining("Not a time-based UUID"); | ||
assertThatThrownBy(uuid::timestamp) | ||
.isInstanceOf(UnsupportedOperationException.class) | ||
.hasMessageContaining("Not a time-based UUID"); | ||
assertThat(uuid.getMostSignificantBits()).isEqualTo(0xea6addb72596428eL); | ||
assertThat(uuid.getLeastSignificantBits()).isEqualTo(0x8a77acf9a43797b3L); | ||
assertThat(uuid).isEqualTo(UUID.fromString(uuid.toString())); | ||
|
||
assertThat(UniqueIds.randomUuidV4(random)) | ||
.isEqualTo(UUID.fromString("4b58fcf1-a036-4cf3-9ec0-4f2620d015d0")) | ||
.extracting(UUID::version) | ||
.isEqualTo(4); | ||
assertThat(UniqueIds.randomUuidV4(random)) | ||
.isEqualTo(UUID.fromString("174cafe6-1614-44fb-b3af-b63e0b344571")) | ||
.extracting(UUID::version) | ||
.isEqualTo(4); | ||
} | ||
|
||
static Stream<Arguments> inputs() { | ||
return Stream.<Callable<UUID>>of(UniqueIds::pseudoRandomUuidV4, UniqueIds::randomUuidV4) | ||
.flatMap(uuidGen -> | ||
IntStream.of(1, 1_000, 100_000, 256 * 1024).mapToObj(size -> Arguments.of(uuidGen, size))); | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("inputs") | ||
void concurrentRequests(Callable<UUID> callable, int size) throws Exception { | ||
ExecutorService executor = ForkJoinPool.commonPool(); | ||
try { | ||
List<Future<UUID>> futures = new ArrayList<>(size); | ||
for (int i = 0; i < size; i++) { | ||
futures.add(executor.submit(callable)); | ||
} | ||
executor.shutdown(); | ||
Iterator<Future<UUID>> iterator = futures.iterator(); | ||
assertRfc4122UuidV4(() -> Futures.getUnchecked(iterator.next()), size); | ||
} finally { | ||
executor.shutdownNow(); | ||
} | ||
} | ||
|
||
@ParameterizedTest | ||
@MethodSource("inputs") | ||
void generate(Callable<UUID> callable, int size) throws Exception { | ||
assertRfc4122UuidV4(callable, size); | ||
} | ||
|
||
private static void assertRfc4122UuidV4(Callable<UUID> supplier, int size) throws Exception { | ||
Set<UUID> uuids = Sets.newHashSetWithExpectedSize(size); | ||
for (int i = 0; i < size; i++) { | ||
UUID uuid = supplier.call(); | ||
assertRfc4122UuidV4(uuid); | ||
assertThat(uuids.add(uuid)).as("should be unique: %s", uuid).isTrue(); | ||
} | ||
assertThat(uuids).hasSize(size); | ||
} | ||
|
||
private static void assertRfc4122UuidV4(UUID uuid) { | ||
assertThat(uuid) | ||
.extracting(UUID::variant) | ||
.as("'%s' should be IETF RFC 4122", uuid) | ||
.isEqualTo(2); | ||
assertThat(uuid) | ||
.extracting(UUID::version) | ||
.as("'%s' should be v4 random version", uuid) | ||
.isEqualTo(4); | ||
assertThatThrownBy(uuid::clockSequence) | ||
.isInstanceOf(UnsupportedOperationException.class) | ||
.hasMessageContaining("Not a time-based UUID"); | ||
assertThatThrownBy(uuid::timestamp) | ||
.isInstanceOf(UnsupportedOperationException.class) | ||
.hasMessageContaining("Not a time-based UUID"); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters