Throw exception when 2^80 requests are made
In the previous implementation an exception was thrown when the random part reached 2^80. It meant that if the first random generated within a millisecond was (2^80 - 1000), the generator could thrown the exception when only 1000 requests was made. It was very difficult, but possible. Now it only occurs in the extremely unlikely event that you manage to generate more than 2^80 ULIDs within the same millisecond.
This commit is contained in:
parent
80f5b6ef3e
commit
7ec2a7cc98
|
@ -108,10 +108,10 @@ public class UlidCreator {
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class GuidCreatorLazyHolder {
|
private static class GuidCreatorLazyHolder {
|
||||||
static final GuidCreator INSTANCE = getGuidCreator().withoutOverflowException();
|
static final GuidCreator INSTANCE = getGuidCreator().withoutOverrunException();
|
||||||
}
|
}
|
||||||
|
|
||||||
private static class FastGuidCreatorLazyHolder {
|
private static class FastGuidCreatorLazyHolder {
|
||||||
static final GuidCreator INSTANCE = getGuidCreator().withFastRandomGenerator().withoutOverflowException();
|
static final GuidCreator INSTANCE = getGuidCreator().withFastRandomGenerator().withoutOverrunException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -44,18 +44,20 @@ import com.github.f4b6a3.ulid.timestamp.DefaultTimestampStrategy;
|
||||||
*/
|
*/
|
||||||
public class GuidCreator {
|
public class GuidCreator {
|
||||||
|
|
||||||
protected static final long MAX_LOW = 0xffffffffffffffffL; // unsigned
|
|
||||||
protected static final long MAX_HIGH = 0x000000000000ffffL;
|
|
||||||
|
|
||||||
protected long previousTimestamp;
|
|
||||||
protected boolean enableOverflowException = true;
|
|
||||||
|
|
||||||
protected Random random;
|
|
||||||
|
|
||||||
protected long low;
|
protected long low;
|
||||||
protected long high;
|
protected long high;
|
||||||
|
|
||||||
protected static final String OVERFLOW_MESSAGE = "The system caused an overflow in the generator by requesting too many IDs.";
|
protected long firstLow;
|
||||||
|
protected long firstHigh;
|
||||||
|
|
||||||
|
protected long previousTimestamp;
|
||||||
|
protected boolean enableOverrunException = true;
|
||||||
|
|
||||||
|
protected Random random;
|
||||||
|
|
||||||
|
protected static final long MASK_UNSIGNED_SHORT = 0x000000000000ffffL; // 2^16
|
||||||
|
|
||||||
|
protected static final String OVERRUN_MESSAGE = "The system overran the generator by requesting too many GUIDs.";
|
||||||
|
|
||||||
protected TimestampStrategy timestampStrategy;
|
protected TimestampStrategy timestampStrategy;
|
||||||
|
|
||||||
|
@ -76,11 +78,13 @@ public class GuidCreator {
|
||||||
* 2. A part of 80 bits that has a random value generated a secure random
|
* 2. A part of 80 bits that has a random value generated a secure random
|
||||||
* generator.
|
* generator.
|
||||||
*
|
*
|
||||||
|
* The random part is reset to a new value every time the millisecond part
|
||||||
|
* changes.
|
||||||
|
*
|
||||||
* If more than one GUID is generated within the same millisecond, the
|
* If more than one GUID is generated within the same millisecond, the
|
||||||
* random part is incremented by one.
|
* random part is incremented by one.
|
||||||
*
|
*
|
||||||
* The random part is reset to a new value every time the millisecond part
|
* The maximum GUIDs that can be generated per millisecond is 2^80.
|
||||||
* changes.
|
|
||||||
*
|
*
|
||||||
* ### Specification of Universally Unique Lexicographically Sortable ID
|
* ### Specification of Universally Unique Lexicographically Sortable ID
|
||||||
*
|
*
|
||||||
|
@ -110,15 +114,14 @@ public class GuidCreator {
|
||||||
* significant bit position (with carrying).
|
* significant bit position (with carrying).
|
||||||
*
|
*
|
||||||
* If, in the extremely unlikely event that, you manage to generate more
|
* If, in the extremely unlikely event that, you manage to generate more
|
||||||
* than 280 ULIDs within the same millisecond, or cause the random component
|
* than 2^80 ULIDs within the same millisecond, or cause the random
|
||||||
* to overflow with less, the generation will fail.
|
* component to overflow with less, the generation will fail.
|
||||||
*
|
*
|
||||||
* @return {@link UUID} a UUID value
|
* @return {@link UUID} a UUID value
|
||||||
*
|
*
|
||||||
* @throws UlidCreatorException
|
* @throws UlidCreatorException
|
||||||
* an overflow exception if too many requests within the same
|
* an overrun exception if too many requests are made within the
|
||||||
* millisecond causes an overflow when incrementing the random
|
* same millisecond.
|
||||||
* bits of the GUID.
|
|
||||||
*/
|
*/
|
||||||
public synchronized UUID create() {
|
public synchronized UUID create() {
|
||||||
|
|
||||||
|
@ -129,7 +132,7 @@ public class GuidCreator {
|
||||||
|
|
||||||
return new UUID(msb, lsb);
|
return new UUID(msb, lsb);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a ULID.
|
* Return a ULID.
|
||||||
*
|
*
|
||||||
|
@ -139,7 +142,7 @@ public class GuidCreator {
|
||||||
UUID guid = create();
|
UUID guid = create();
|
||||||
return UlidUtil.fromUuidToUlid(guid);
|
return UlidUtil.fromUuidToUlid(guid);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a ULID as byte sequence.
|
* Return a ULID as byte sequence.
|
||||||
*
|
*
|
||||||
|
@ -173,27 +176,35 @@ public class GuidCreator {
|
||||||
* Reset the random part of the GUID.
|
* Reset the random part of the GUID.
|
||||||
*/
|
*/
|
||||||
protected synchronized void reset() {
|
protected synchronized void reset() {
|
||||||
|
|
||||||
|
// Get random values
|
||||||
if (random == null) {
|
if (random == null) {
|
||||||
this.low = SecureRandomLazyHolder.INSTANCE.nextLong();
|
this.low = SecureRandomLazyHolder.INSTANCE.nextLong();
|
||||||
this.high = SecureRandomLazyHolder.INSTANCE.nextLong() & MAX_HIGH;
|
this.high = SecureRandomLazyHolder.INSTANCE.nextInt() & MASK_UNSIGNED_SHORT;
|
||||||
} else {
|
} else {
|
||||||
this.low = random.nextLong();
|
this.low = random.nextLong();
|
||||||
this.high = random.nextLong() & MAX_HIGH;
|
this.high = random.nextInt() & MASK_UNSIGNED_SHORT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Save the random values
|
||||||
|
this.firstLow = this.low;
|
||||||
|
this.firstHigh = this.high;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
||||||
|
*
|
||||||
* @throws UlidCreatorException
|
* @throws UlidCreatorException
|
||||||
* if an overflow happens.
|
* if an overrun happens.
|
||||||
*/
|
*/
|
||||||
protected synchronized void increment() {
|
protected synchronized void increment() {
|
||||||
if ((this.low++ == MAX_LOW) && (this.high++ == MAX_HIGH)) {
|
if ((++this.low == this.firstLow) && (++this.high == this.firstHigh)) {
|
||||||
this.high = 0L;
|
this.reset();
|
||||||
// Too many requests
|
// Too many requests
|
||||||
if (enableOverflowException) {
|
if (enableOverrunException) {
|
||||||
throw new UlidCreatorException(OVERFLOW_MESSAGE);
|
throw new UlidCreatorException(OVERRUN_MESSAGE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -251,16 +262,16 @@ public class GuidCreator {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Used to disable the overflow exception.
|
* Used to disable the overrun exception.
|
||||||
*
|
*
|
||||||
* An exception thrown when too many requests within the same millisecond
|
* An exception is thrown when too many requests are made within the same
|
||||||
* causes an overflow while incrementing the random bits of the GUID.
|
* millisecond.
|
||||||
*
|
*
|
||||||
* @return {@link GuidCreator}
|
* @return {@link GuidCreator}
|
||||||
*/
|
*/
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
public synchronized <T extends GuidCreator> T withoutOverflowException() {
|
public synchronized <T extends GuidCreator> T withoutOverrunException() {
|
||||||
this.enableOverflowException = false;
|
this.enableOverrunException = false;
|
||||||
return (T) this;
|
return (T) this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,22 @@
|
||||||
package com.github.f4b6a3.ulid.guid;
|
package com.github.f4b6a3.ulid.guid;
|
||||||
|
|
||||||
import com.github.f4b6a3.ulid.guid.GuidCreator;
|
|
||||||
|
|
||||||
class GuidCreatorMock extends GuidCreator {
|
class GuidCreatorMock extends GuidCreator {
|
||||||
|
|
||||||
public GuidCreatorMock(long low, long high, long previousTimestamp) {
|
public GuidCreatorMock(long low, long high, long previousTimestamp) {
|
||||||
|
this(low, high, low, high, previousTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
public GuidCreatorMock(long low, long high, long firstLow, long firstHigh, long previousTimestamp) {
|
||||||
super();
|
super();
|
||||||
|
|
||||||
|
// Set initial values
|
||||||
this.low = low;
|
this.low = low;
|
||||||
this.high = high;
|
this.high = high;
|
||||||
|
|
||||||
|
// Save the initial values
|
||||||
|
this.firstLow = firstLow;
|
||||||
|
this.firstHigh = firstHigh;
|
||||||
|
|
||||||
this.previousTimestamp = previousTimestamp;
|
this.previousTimestamp = previousTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -6,7 +6,6 @@ import java.util.UUID;
|
||||||
import org.junit.Test;
|
import org.junit.Test;
|
||||||
|
|
||||||
import com.github.f4b6a3.ulid.exception.UlidCreatorException;
|
import com.github.f4b6a3.ulid.exception.UlidCreatorException;
|
||||||
import com.github.f4b6a3.ulid.guid.GuidCreator;
|
|
||||||
import com.github.f4b6a3.ulid.random.Xorshift128PlusRandom;
|
import com.github.f4b6a3.ulid.random.Xorshift128PlusRandom;
|
||||||
import com.github.f4b6a3.ulid.timestamp.FixedTimestampStretegy;
|
import com.github.f4b6a3.ulid.timestamp.FixedTimestampStretegy;
|
||||||
|
|
||||||
|
@ -14,19 +13,17 @@ import static org.junit.Assert.*;
|
||||||
|
|
||||||
public class GuidCreatorTest {
|
public class GuidCreatorTest {
|
||||||
|
|
||||||
private static final long DEFAULT_LOOP_MAX = 100_000;
|
private static final long DEFAULT_LOOP_MAX = 1_000_000;
|
||||||
|
|
||||||
private static final long TIMESTAMP = System.currentTimeMillis();
|
private static final long TIMESTAMP = System.currentTimeMillis();
|
||||||
private static final long MAX_LOW = GuidCreator.MAX_LOW;
|
|
||||||
private static final long MAX_HIGH = GuidCreator.MAX_HIGH;
|
|
||||||
|
|
||||||
private static final Random RANDOM = new Xorshift128PlusRandom();
|
private static final Random RANDOM = new Xorshift128PlusRandom();
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testRandomMostSignificantBits() {
|
public void testRandomMostSignificantBits() {
|
||||||
|
|
||||||
long low = RANDOM.nextInt();
|
long low = RANDOM.nextLong();
|
||||||
long high = RANDOM.nextInt(Short.MAX_VALUE);
|
long high = (short) (RANDOM.nextInt());
|
||||||
|
|
||||||
GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP);
|
GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP);
|
||||||
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
|
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
|
||||||
|
@ -34,7 +31,7 @@ public class GuidCreatorTest {
|
||||||
UUID uuid = creator.create();
|
UUID uuid = creator.create();
|
||||||
long firstMsb = (short) uuid.getMostSignificantBits();
|
long firstMsb = (short) uuid.getMostSignificantBits();
|
||||||
long lastMsb = 0;
|
long lastMsb = 0;
|
||||||
for (int i = 0; i <= DEFAULT_LOOP_MAX; i++) {
|
for (int i = 0; i < DEFAULT_LOOP_MAX; i++) {
|
||||||
uuid = creator.create();
|
uuid = creator.create();
|
||||||
lastMsb = (short) uuid.getMostSignificantBits();
|
lastMsb = (short) uuid.getMostSignificantBits();
|
||||||
}
|
}
|
||||||
|
@ -74,8 +71,8 @@ public class GuidCreatorTest {
|
||||||
@Test
|
@Test
|
||||||
public void testIncrementOfRandomLeastSignificantBits() {
|
public void testIncrementOfRandomLeastSignificantBits() {
|
||||||
|
|
||||||
long low = RANDOM.nextInt();
|
long low = RANDOM.nextLong();
|
||||||
long high = RANDOM.nextInt(Short.MAX_VALUE);
|
long high = (short) RANDOM.nextInt();
|
||||||
|
|
||||||
GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP);
|
GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP);
|
||||||
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
|
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
|
||||||
|
@ -85,16 +82,16 @@ public class GuidCreatorTest {
|
||||||
uuid = creator.create();
|
uuid = creator.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
long expected = low + DEFAULT_LOOP_MAX;
|
long expectedLsb = low + DEFAULT_LOOP_MAX;
|
||||||
long randomLsb = uuid.getLeastSignificantBits();
|
long randomLsb = uuid.getLeastSignificantBits();
|
||||||
assertEquals(String.format("The LSB should be iqual to %s.", expected), expected, randomLsb);
|
assertEquals(String.format("The LSB should be iqual to %s.", expectedLsb), expectedLsb, randomLsb);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
public void testIncrementOfRandomMostSignificantBits() {
|
public void testIncrementOfRandomMostSignificantBits() {
|
||||||
|
|
||||||
long low = MAX_LOW;
|
long low = RANDOM.nextLong();
|
||||||
long high = RANDOM.nextInt(Short.MAX_VALUE);
|
long high = (short) (RANDOM.nextInt());
|
||||||
|
|
||||||
GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP);
|
GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP);
|
||||||
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
|
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
|
||||||
|
@ -104,18 +101,21 @@ public class GuidCreatorTest {
|
||||||
uuid = creator.create();
|
uuid = creator.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
long expected = high + 1;
|
long expected = high;
|
||||||
long randomMsb = uuid.getMostSignificantBits() & MAX_HIGH;
|
long randomMsb = (short) (uuid.getMostSignificantBits());
|
||||||
assertEquals(String.format("The MSB should be iqual to %s.", expected), expected, randomMsb);
|
assertEquals(String.format("The MSB should be iqual to %s.", expected), expected, randomMsb);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test(expected = UlidCreatorException.class)
|
@Test(expected = UlidCreatorException.class)
|
||||||
public void testShouldThrowOverflowException() {
|
public void testShouldThrowOverflowException() {
|
||||||
|
|
||||||
long low = MAX_LOW - DEFAULT_LOOP_MAX;
|
long startLow = RANDOM.nextInt() + DEFAULT_LOOP_MAX;
|
||||||
long high = MAX_HIGH;
|
long startHigh = (short) (RANDOM.nextInt() + 1);
|
||||||
|
|
||||||
GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP);
|
long low = startLow - DEFAULT_LOOP_MAX;
|
||||||
|
long high = (short) (startHigh - 1);
|
||||||
|
|
||||||
|
GuidCreatorMock creator = new GuidCreatorMock(low, high, startLow, startHigh, TIMESTAMP);
|
||||||
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
|
creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP));
|
||||||
|
|
||||||
UUID uuid = new UUID(0, 0);
|
UUID uuid = new UUID(0, 0);
|
||||||
|
@ -123,13 +123,11 @@ public class GuidCreatorTest {
|
||||||
uuid = creator.create();
|
uuid = creator.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
long expected = MAX_LOW;
|
|
||||||
long randomLsb = uuid.getLeastSignificantBits();
|
long randomLsb = uuid.getLeastSignificantBits();
|
||||||
assertEquals(String.format("The LSB should be iqual to %s.", expected), expected, randomLsb);
|
assertEquals(String.format("The LSB should be iqual to %s.", startLow), startLow, randomLsb);
|
||||||
|
|
||||||
expected = MAX_HIGH;
|
long randomMsb = (short) (uuid.getMostSignificantBits());
|
||||||
long randomMsb = uuid.getMostSignificantBits() & MAX_HIGH;
|
assertEquals(String.format("The MSB should be iqual to %s.", startHigh), startHigh, randomMsb);
|
||||||
assertEquals(String.format("The MSB should be iqual to %s.", expected), expected, randomMsb);
|
|
||||||
|
|
||||||
creator.create();
|
creator.create();
|
||||||
fail("It should throw an overflow exception.");
|
fail("It should throw an overflow exception.");
|
||||||
|
|
Loading…
Reference in New Issue