Skip to content

Commit

Permalink
Add UniqueIds for improved scalable UUID generation (#1789)
Browse files Browse the repository at this point in the history
Add UniqueIds for improved scalable UUID generation
  • Loading branch information
schlosna authored Sep 6, 2023
1 parent 7de983b commit 6e34554
Show file tree
Hide file tree
Showing 7 changed files with 371 additions and 0 deletions.
5 changes: 5 additions & 0 deletions changelog/@unreleased/pr-1789.v2.yml
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
1 change: 1 addition & 0 deletions settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ rootProject.name = 'tritium'
include 'tritium-api'
include 'tritium-caffeine'
include 'tritium-core'
include 'tritium-ids'
include 'tritium-jmh'
include 'tritium-lib'
include 'tritium-proxy'
Expand Down
19 changes: 19 additions & 0 deletions tritium-ids/build.gradle
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 tritium-ids/src/jmh/java/com/palantir/tritium/ids/UuidBenchmark.java
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 tritium-ids/src/main/java/com/palantir/tritium/ids/UniqueIds.java
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 tritium-ids/src/test/java/com/palantir/tritium/ids/UniqueIdsTest.java
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");
}
}
1 change: 1 addition & 0 deletions versions.props
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
com.fasterxml.jackson.*:* = 2.13.3
com.fasterxml.jackson.core:* = 2.15.2
com.fasterxml.uuid:* = 4.2.0
com.github.ben-manes.caffeine:* = 3.1.8
com.google.auto.service:* = 1.1.1
com.google.code.findbugs:* = 3.0.2
Expand Down

0 comments on commit 6e34554

Please sign in to comment.