[#3] Remove overrun exception

Remove UlidCreatorException // extremely unlikely to occur overrun

Add UlidCreator.toString() // from UUID to crockford base 32

Change DefaultRandomStrategy // no need for thread local SecureRandom

Update test cases
Update README.md
Update javadoc

Test coverage: 94.2%
This commit is contained in:
Fabio Lima 2020-10-17 21:50:37 -03:00
parent 37ffb9e6ba
commit fdd33556a7
8 changed files with 66 additions and 103 deletions

View File

@ -3,22 +3,27 @@
A Java library for generating ULIDs. A Java library for generating ULIDs.
* Generated in lexicographical order;
* Can be stored as a UUID/GUID;
* Can be stored as a string of 26 chars;
* String format is encoded to [Crockford's base32](https://www.crockford.com/base32.html);
* String format is URL safe, case insensitive and accepts hyphens.
How to Use How to Use
------------------------------------------------------ ------------------------------------------------------
Create a ULID as GUID: Create a ULID:
```java ```java
UUID ulid = UlidCreator.getUlid(); UUID ulid = UlidCreator.getUlid(); // 01706d6c-6aad-c795-370c-98d0be881bba
``` ```
Create a ULID as string: Create a ULID string:
```java ```java
String ulid = UlidCreator.getUlidString(); String ulid = UlidCreator.getUlidString(); // 01E1PPRTMSQ34W7JR5YSND6B8Z
``` ```
### Maven dependency ### Maven dependency
Add these lines to your `pom.xml`. Add these lines to your `pom.xml`.
@ -28,7 +33,7 @@ Add these lines to your `pom.xml`.
<dependency> <dependency>
<groupId>com.github.f4b6a3</groupId> <groupId>com.github.f4b6a3</groupId>
<artifactId>ulid-creator</artifactId> <artifactId>ulid-creator</artifactId>
<version>2.0.2</version> <version>2.1.0</version>
</dependency> </dependency>
``` ```
See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator). See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator).
@ -36,20 +41,20 @@ See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b
Implementation Implementation
------------------------------------------------------ ------------------------------------------------------
### ULID as GUID ### ULID
The GUIDs in this library are based on the [ULID specification](https://github.com/ulid/spec). The first 48 bits represent the count of milliseconds since Unix Epoch, 1 January 1970. The remaining 60 bits are generated by a secure random number generator. The GUIDs in this library are based on the [ULID specification](https://github.com/ulid/spec). The first 48 bits represent the count of milliseconds since Unix Epoch, 1 January 1970. The remaining 60 bits are generated by a secure random number generator.
Every time the timestamp changes the random part is reset to a new random value. If the current timestamp is equal to the previous one, the random bits are incremented by 1. Every time the timestamp changes the random part is reset to a new random value. If the current timestamp is equal to the previous one, the random bits are incremented by 1.
The default random number generator is a thread safe `java.security.SecureRandom`, but it's possible to use any RNG that extends `java.util.Random`. The default random number generator is `java.security.SecureRandom`.
```java ```java
// GUID based on ULID spec // GUID based on ULID spec
UUID ulid = UlidCreator.getUlid(); UUID ulid = UlidCreator.getUlid();
``` ```
Examples of GUIDs based on ULID spec: Sequence of GUIDs based on ULID spec:
```text ```text
01706d6c-6aac-80bd-7ff5-f660c2dd58ea 01706d6c-6aac-80bd-7ff5-f660c2dd58ea
@ -74,18 +79,18 @@ Examples of GUIDs based on ULID spec:
millisecs randomness millisecs randomness
``` ```
### ULID as string ### ULID string
The ULID is a 26 char sequence. See the [ULID specification](https://github.com/ulid/spec) for more information. The ULID string is a sequence of 26 chars. See the [ULID specification](https://github.com/ulid/spec) for more information.
See the section on GUIDs to know how the 128 bits are generated in this library. See the section on GUIDs to know how the 128 bits are generated in this library.
```java ```java
// ULIDs // String based on ULID spec
String ulid = UlidCreator.getUlidString(); String ulid = UlidCreator.getUlidString();
``` ```
Examples of ULIDs: Sequence of Strings based on ULID spec:
```text ```text
01E1PPRTMSQ34W7JR5YSND6B8T 01E1PPRTMSQ34W7JR5YSND6B8T
@ -107,36 +112,35 @@ Examples of ULIDs:
^ look ^ look ^ look ^ look
|---------|--------------| |---------|--------------|
milli randomness millisecs randomness
``` ```
#### How use the `UlidSpecCreator` directly ### How use the `UlidSpecCreator` directly
These are some examples of using the `UlidSpecCreator` to create ULIDs strings: These are some examples of using the `UlidSpecCreator` to create ULID strings:
```java ```java
// with your custom timestamp strategy // with your custom timestamp strategy
TimestampStrategy customStrategy = new CustomTimestampStrategy(); TimestampStrategy customStrategy = new CustomTimestampStrategy();
String ulid = UlidCreator.getUlidSpecCreator() UlidSpecCreator creator = UlidCreator.getUlidSpecCreator()
.withTimestampStrategy(customStrategy) .withTimestampStrategy(customStrategy);
.createString(); String ulid = creator.createString();
``` ```
```java ```java
// with your custom random strategy that wraps any random generator // with your custom random strategy that wraps any random generator
RandomStrategy customStrategy = new CustomRandomStrategy(); RandomStrategy customStrategy = new CustomRandomStrategy();
String ulid = UlidCreator.getUlidSpecCreator() UlidSpecCreator creator = UlidCreator.getUlidSpecCreator()
.withRandomStrategy(customStrategy) .withRandomStrategy(customStrategy);
.createString(); String ulid = creator.createString();
``` ```
```java ```java
// with `java.util.Random` number generator // with `java.util.Random` number generator
Random random = new Random(); Random random = new Random();
String ulid = UlidCreator.getUlidSpecCreator() UlidSpecCreator creator = UlidCreator.getUlidSpecCreator()
.withRandomGenerator(random) .withRandomGenerator(random);
.createString(); String ulid = creator.createString();
``` ```
Benchmark Benchmark
------------------------------------------------------ ------------------------------------------------------
@ -150,7 +154,7 @@ Throughput.Java_RandomBased thrpt 5 2234,199 ± 2,844 ops/ms
Throughput.UlidCreator_Ulid thrpt 5 19155,742 ± 22,195 ops/ms Throughput.UlidCreator_Ulid thrpt 5 19155,742 ± 22,195 ops/ms
Throughput.UlidCreator_UlidString thrpt 5 4946,479 ± 22,800 ops/ms Throughput.UlidCreator_UlidString thrpt 5 4946,479 ± 22,800 ops/ms
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
Total time: 00:06:41 Total time: 00:04:01
--------------------------------------------------------------------------- ---------------------------------------------------------------------------
``` ```
@ -162,7 +166,7 @@ AverageTime.Java_RandomBased avgt 5 449,641 ± 0,994 ns/op
AverageTime.UlidCreator_Ulid avgt 5 52,199 ± 0,185 ns/op AverageTime.UlidCreator_Ulid avgt 5 52,199 ± 0,185 ns/op
AverageTime.UlidCreator_UlidString avgt 5 202,014 ± 2,111 ns/op AverageTime.UlidCreator_UlidString avgt 5 202,014 ± 2,111 ns/op
---------------------------------------------------------------------- ----------------------------------------------------------------------
Total time: 00:06:41 Total time: 00:04:01
---------------------------------------------------------------------- ----------------------------------------------------------------------
``` ```

View File

@ -27,6 +27,7 @@ package com.github.f4b6a3.ulid;
import java.util.UUID; import java.util.UUID;
import com.github.f4b6a3.ulid.creator.UlidSpecCreator; import com.github.f4b6a3.ulid.creator.UlidSpecCreator;
import com.github.f4b6a3.ulid.exception.InvalidUlidException;
import com.github.f4b6a3.ulid.util.UlidConverter; import com.github.f4b6a3.ulid.util.UlidConverter;
/** /**
@ -42,13 +43,31 @@ public final class UlidCreator {
/** /**
* Returns a ULID as GUID from a string. * Returns a ULID as GUID from a string.
* *
* The input string must be encoded to Crockford's base32, following the ULID
* specification.
*
* An exception is thrown if the ULID string is invalid.
*
* @param ulid a ULID string * @param ulid a ULID string
* @return a UUID * @return a UUID
* @throws InvalidUlidException if invalid
*/ */
public static UUID fromString(String ulid) { public static UUID fromString(String ulid) {
return UlidConverter.fromString(ulid); return UlidConverter.fromString(ulid);
} }
/**
* Convert a UUID to ULID string
*
* The returning string is encoded to Crockford's base32.
*
* @param uuid a UUID
* @return a ULID string
*/
public static String toString(UUID ulid) {
return UlidConverter.toString(ulid);
}
/** /**
* Returns a ULID as GUID. * Returns a ULID as GUID.
* *

View File

@ -30,7 +30,6 @@ import java.util.UUID;
import com.github.f4b6a3.ulid.strategy.RandomStrategy; import com.github.f4b6a3.ulid.strategy.RandomStrategy;
import com.github.f4b6a3.ulid.strategy.random.DefaultRandomStrategy; import com.github.f4b6a3.ulid.strategy.random.DefaultRandomStrategy;
import com.github.f4b6a3.ulid.strategy.random.OtherRandomStrategy; import com.github.f4b6a3.ulid.strategy.random.OtherRandomStrategy;
import com.github.f4b6a3.ulid.exception.UlidCreatorException;
import com.github.f4b6a3.ulid.strategy.TimestampStrategy; import com.github.f4b6a3.ulid.strategy.TimestampStrategy;
import com.github.f4b6a3.ulid.strategy.timestamp.DefaultTimestampStrategy; import com.github.f4b6a3.ulid.strategy.timestamp.DefaultTimestampStrategy;
import com.github.f4b6a3.ulid.util.UlidConverter; import com.github.f4b6a3.ulid.util.UlidConverter;
@ -120,9 +119,6 @@ public class UlidSpecCreator {
* overflow with less, the generation will fail. * overflow with less, the generation will fail.
* *
* @return {@link UUID} a GUID value * @return {@link UUID} a GUID value
*
* @throws UlidCreatorException an overrun exception if too many requests are
* made within the same millisecond.
*/ */
public synchronized UUID create() { public synchronized UUID create() {
@ -188,18 +184,12 @@ public class UlidSpecCreator {
/** /**
* Increment the random part of the GUID. * Increment the random part of the GUID.
*
* An exception is thrown when more than 2^80 increment operations are made,
* although it's extremely unlikely to occur.
*
* @throws UlidCreatorException if an overrun happens.
*/ */
protected synchronized void increment() { protected synchronized void increment() {
if (++this.random2 >= this.randomMax2) { if (++this.random2 >= this.randomMax2) {
this.random2 = this.random2 & HALF_RANDOM_COMPONENT; this.random2 = this.random2 & HALF_RANDOM_COMPONENT;
if ((++this.random1 >= this.randomMax1)) { if ((++this.random1 >= this.randomMax1)) {
this.reset(); this.reset();
throw new UlidCreatorException(OVERRUN_MESSAGE);
} }
} }
} }

View File

@ -1,34 +0,0 @@
/*
* MIT License
*
* Copyright (c) 2020 Fabio Lima
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
package com.github.f4b6a3.ulid.exception;
public final class UlidCreatorException extends RuntimeException {
private static final long serialVersionUID = 1L;
public UlidCreatorException(String message) {
super(message);
}
}

View File

@ -30,14 +30,14 @@ import java.util.Random;
import com.github.f4b6a3.ulid.strategy.RandomStrategy; import com.github.f4b6a3.ulid.strategy.RandomStrategy;
/** /**
* It uses a thread local instance of {@link java.security.SecureRandom}. * It uses an instance of {@link java.security.SecureRandom}.
*/ */
public final class DefaultRandomStrategy implements RandomStrategy { public final class DefaultRandomStrategy implements RandomStrategy {
protected static final ThreadLocal<Random> THREAD_LOCAL_RANDOM = ThreadLocal.withInitial(SecureRandom::new); private static final Random SECURE_RANDOM = new SecureRandom();
@Override @Override
public void nextBytes(byte[] bytes) { public void nextBytes(byte[] bytes) {
THREAD_LOCAL_RANDOM.get().nextBytes(bytes); SECURE_RANDOM.nextBytes(bytes);
} }
} }

View File

@ -40,13 +40,13 @@ public final class UlidConverter {
* *
* The returning string is encoded to Crockford's base32. * The returning string is encoded to Crockford's base32.
* *
* @param uuid a UUID * @param ulid a UUID
* @return a ULID * @return a ULID
*/ */
public static String toString(UUID uuid) { public static String toString(UUID ulid) {
final long msb = uuid.getMostSignificantBits(); final long msb = ulid.getMostSignificantBits();
final long lsb = uuid.getLeastSignificantBits(); final long lsb = ulid.getLeastSignificantBits();
final long time = ((msb & 0xffffffffffff0000L) >>> 16); final long time = ((msb & 0xffffffffffff0000L) >>> 16);
final long random1 = ((msb & 0x000000000000ffffL) << 24) | ((lsb & 0xffffff0000000000L) >>> 40); final long random1 = ((msb & 0x000000000000ffffL) << 24) | ((lsb & 0xffffff0000000000L) >>> 40);

View File

@ -5,7 +5,6 @@ import java.util.UUID;
import com.github.f4b6a3.ulid.UlidCreator; import com.github.f4b6a3.ulid.UlidCreator;
import com.github.f4b6a3.ulid.creator.UlidSpecCreator; import com.github.f4b6a3.ulid.creator.UlidSpecCreator;
import com.github.f4b6a3.ulid.exception.UlidCreatorException;
import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy; import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy;
/** /**
@ -93,13 +92,7 @@ public class UniquenessTest {
for (int i = 0; i < max; i++) { for (int i = 0; i < max; i++) {
// Request a UUID // Request a UUID
UUID uuid = null; UUID uuid = creator.create();
try {
uuid = creator.create();
} catch (UlidCreatorException e) {
// Ignore the overrun exception and try again
uuid = creator.create();
}
if (verbose) { if (verbose) {
// Calculate and show progress // Calculate and show progress

View File

@ -9,7 +9,6 @@ import java.util.UUID;
import org.junit.Test; import org.junit.Test;
import com.github.f4b6a3.ulid.UlidCreator; import com.github.f4b6a3.ulid.UlidCreator;
import com.github.f4b6a3.ulid.exception.UlidCreatorException;
import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy; import com.github.f4b6a3.ulid.strategy.timestamp.FixedTimestampStretegy;
import static org.junit.Assert.*; import static org.junit.Assert.*;
@ -126,7 +125,7 @@ public class UlidSpecCreatorTest {
} }
@Test @Test
public void testShouldThrowOverflowException1() { public void testIncrementRandomComponentMaximum1() {
long random1 = 0x000000ffffffffffL; long random1 = 0x000000ffffffffffL;
long random2 = 0x000000ffffffffffL; long random2 = 0x000000ffffffffffL;
@ -157,16 +156,12 @@ public class UlidSpecCreatorTest {
BigInteger bigint2 = new BigInteger(concat2, 16); BigInteger bigint2 = new BigInteger(concat2, 16);
assertEquals(bigint1.add(BigInteger.valueOf(DEFAULT_LOOP_MAX)), bigint2); assertEquals(bigint1.add(BigInteger.valueOf(DEFAULT_LOOP_MAX)), bigint2);
try { // This line resets the random component
uuid = creator.create(); uuid = creator.create();
fail("It should throw an overflow exception.");
} catch (UlidCreatorException e) {
// success
}
} }
@Test @Test
public void testShouldThrowOverflowException2() { public void testIncrementRandomComponentMaximum2() {
long random1 = (RANDOM.nextLong() & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT); long random1 = (RANDOM.nextLong() & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT);
long random2 = (RANDOM.nextLong() & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT); long random2 = (RANDOM.nextLong() & UlidSpecCreatorMock.HALF_RANDOM_COMPONENT);
@ -205,12 +200,8 @@ public class UlidSpecCreatorTest {
BigInteger bigint2 = new BigInteger(concat2, 16); BigInteger bigint2 = new BigInteger(concat2, 16);
assertEquals(bigint1.add(BigInteger.valueOf(DEFAULT_LOOP_MAX)), bigint2); assertEquals(bigint1.add(BigInteger.valueOf(DEFAULT_LOOP_MAX)), bigint2);
try { // This line resets the random component
creator.create(); creator.create();
fail("It should throw an overflow exception.");
} catch (UlidCreatorException e) {
// success
}
} }
@Test @Test