diff --git a/pom.xml b/pom.xml index 4f5a509..a9f8daa 100644 --- a/pom.xml +++ b/pom.xml @@ -26,7 +26,7 @@ UTF-8 - 1.7 + 8 ${jdk.version} ${jdk.version} diff --git a/src/main/java/com/github/f4b6a3/ulid/guid/GuidCreator.java b/src/main/java/com/github/f4b6a3/ulid/guid/GuidCreator.java index 28c443d..df3f714 100644 --- a/src/main/java/com/github/f4b6a3/ulid/guid/GuidCreator.java +++ b/src/main/java/com/github/f4b6a3/ulid/guid/GuidCreator.java @@ -44,18 +44,19 @@ import com.github.f4b6a3.ulid.timestamp.DefaultTimestampStrategy; */ public class GuidCreator { - protected long low; - protected long high; + protected long randomMsb = 0; + protected long randomLsb = 0; - protected long firstLow; - protected long firstHigh; + protected long randomLsbMax; + protected long randomMsbMax; + + protected static final long HALF_RANDOM_COMPONENT = 0x000000ffffffffffL; + protected static final long INCREMENT_MAX = 0x0000010000000000L; protected long previousTimestamp; 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; @@ -126,8 +127,11 @@ public class GuidCreator { final long timestamp = this.getTimestamp(); - final long msb = (timestamp << 16) | high; - final long lsb = low; + final long randomHi = truncate(randomMsb); + final long randomLo = truncate(randomLsb); + + final long msb = (timestamp << 16) | (randomHi >>> 24); + final long lsb = (randomHi << 40) | randomLo; return new UUID(msb, lsb); } @@ -178,29 +182,29 @@ public class GuidCreator { // Get random values if (random == null) { - this.low = SecureRandomLazyHolder.INSTANCE.nextLong(); - this.high = SecureRandomLazyHolder.INSTANCE.nextInt() & MASK_UNSIGNED_SHORT; + this.randomMsb = truncate(SecureRandomLazyHolder.INSTANCE.nextLong()); + this.randomLsb = truncate(SecureRandomLazyHolder.INSTANCE.nextLong()); } else { - this.low = random.nextLong(); - this.high = random.nextInt() & MASK_UNSIGNED_SHORT; + this.randomMsb = truncate(random.nextLong()); + this.randomLsb = truncate(random.nextLong()); } // Save the random values - this.firstLow = this.low; - this.firstHigh = this.high; + this.randomMsbMax = this.randomMsb | INCREMENT_MAX; + this.randomLsbMax = this.randomLsb | INCREMENT_MAX; } /** * Increment the random part of the GUID. * - * An exception is thrown when more than 2^80 increment operations are made - * in the same millisecond. + * An exception is thrown when more than 2^80 increment operations are made. * * @throws UlidCreatorException * if an overrun happens. */ + protected synchronized void increment() { - if ((++this.low == this.firstLow) && (++this.high == this.firstHigh)) { + if ((++this.randomLsb == this.randomLsbMax) && (++this.randomMsb == this.randomMsbMax)) { this.reset(); throw new UlidCreatorException(OVERRUN_MESSAGE); } @@ -258,6 +262,31 @@ public class GuidCreator { return (T) this; } + /** + * Truncate long to half random component. + * + * @param value + * a value to be truncated. + * @return truncated value + */ + protected synchronized long truncate(final long value) { + return (value & HALF_RANDOM_COMPONENT); + } + + /** + * For unit tests + */ + protected long extractRandomLsb(UUID uuid) { + return uuid.getLeastSignificantBits() & HALF_RANDOM_COMPONENT; + } + + /** + * For unit tests + */ + protected long extractRandomMsb(UUID uuid) { + return ((uuid.getMostSignificantBits() & 0xffff) << 24) | (uuid.getLeastSignificantBits() >>> 40); + } + private static class SecureRandomLazyHolder { static final Random INSTANCE = new SecureRandom(); } diff --git a/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorMock.java b/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorMock.java index 7c1799f..fdf7570 100644 --- a/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorMock.java +++ b/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorMock.java @@ -2,21 +2,35 @@ package com.github.f4b6a3.ulid.guid; class GuidCreatorMock extends GuidCreator { - public GuidCreatorMock(long low, long high, long previousTimestamp) { - this(low, high, low, high, previousTimestamp); + public GuidCreatorMock(long previousTimestamp) { + super(); + this.previousTimestamp = previousTimestamp; } - public GuidCreatorMock(long low, long high, long firstLow, long firstHigh, long previousTimestamp) { - super(); + public GuidCreatorMock(long randomMsb, long randomLsb, long randomMsbMax, long randomLsbMax, long previousTimestamp) { - // Set initial values - this.low = low; - this.high = high; + this.randomMsb = randomMsb; + this.randomLsb = randomLsb; - // Save the initial values - this.firstLow = firstLow; - this.firstHigh = firstHigh; + this.randomMsbMax = randomMsbMax; + this.randomLsbMax = randomLsbMax; this.previousTimestamp = previousTimestamp; } + + public long getRandomMsb() { + return this.randomMsb; + } + + public long getRandomLsb() { + return this.randomLsb; + } + + public long getRandomHiMax() { + return this.randomMsb; + } + + public long getRandomLoMax() { + return this.randomLsb; + } } \ No newline at end of file diff --git a/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorTest.java b/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorTest.java index 111102e..a1ff0d7 100644 --- a/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorTest.java +++ b/src/test/java/com/github/f4b6a3/ulid/guid/GuidCreatorTest.java @@ -13,7 +13,7 @@ import static org.junit.Assert.*; public class GuidCreatorTest { - private static final long DEFAULT_LOOP_MAX = 1_048_576; // 2^20 + private static final long DEFAULT_LOOP_MAX = 1_000_000; private static final long TIMESTAMP = System.currentTimeMillis(); @@ -22,42 +22,39 @@ public class GuidCreatorTest { @Test public void testRandomMostSignificantBits() { - long low = RANDOM.nextLong(); - long high = (short) (RANDOM.nextInt()); - - GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP); + GuidCreatorMock creator = new GuidCreatorMock(TIMESTAMP); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); UUID uuid = creator.create(); - long firstMsb = (short) uuid.getMostSignificantBits(); - long lastMsb = 0; + long firstMsb = creator.extractRandomMsb(uuid); for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { uuid = creator.create(); - lastMsb = (short) uuid.getMostSignificantBits(); + } - assertEquals(String.format("The last MSB should be iqual to the first %s.", firstMsb), firstMsb, lastMsb); + long lastMsb = creator.extractRandomMsb(uuid); + long expectedMsb = firstMsb; + assertEquals(String.format("The last MSB should be iqual to the first %s.", expectedMsb), expectedMsb, lastMsb); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP + 1)); uuid = creator.create(); - lastMsb = (short) uuid.getMostSignificantBits(); - assertNotEquals("The last MSB should be be random after timestamp changed.", firstMsb, lastMsb); + lastMsb = uuid.getMostSignificantBits(); + assertNotEquals("The last MSB should be random after timestamp changed.", firstMsb, lastMsb); } @Test public void testRandomLeastSignificantBits() { - GuidCreator creator = new GuidCreator(); + GuidCreatorMock creator = new GuidCreatorMock(TIMESTAMP); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); UUID uuid = creator.create(); - long firstLsb = uuid.getLeastSignificantBits(); - long lastLsb = 0; + long firstLsb = creator.extractRandomLsb(uuid); for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { uuid = creator.create(); - lastLsb = uuid.getLeastSignificantBits(); } + long lastLsb = creator.extractRandomLsb(uuid); long expected = firstLsb + DEFAULT_LOOP_MAX; assertEquals(String.format("The last LSB should be iqual to %s.", expected), expected, lastLsb); @@ -71,51 +68,81 @@ public class GuidCreatorTest { @Test public void testIncrementOfRandomLeastSignificantBits() { - long low = RANDOM.nextLong(); - long high = (short) RANDOM.nextInt(); - - GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP); + GuidCreatorMock creator = new GuidCreatorMock(TIMESTAMP); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + long lsb = creator.getRandomLsb(); + UUID uuid = new UUID(0, 0); for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { uuid = creator.create(); } - long expectedLsb = low + DEFAULT_LOOP_MAX; - long randomLsb = uuid.getLeastSignificantBits(); - assertEquals(String.format("The LSB should be iqual to %s.", expectedLsb), expectedLsb, randomLsb); + long expectedLsb = lsb + DEFAULT_LOOP_MAX; + long randomLsb = creator.getRandomLsb(); + assertEquals("Wrong LSB after loop.", expectedLsb, randomLsb); + + randomLsb = creator.extractRandomLsb(uuid); + assertEquals("Wrong LSB after loop.", expectedLsb, randomLsb); } @Test public void testIncrementOfRandomMostSignificantBits() { - long low = RANDOM.nextLong(); - long high = (short) (RANDOM.nextInt()); - - GuidCreatorMock creator = new GuidCreatorMock(low, high, TIMESTAMP); + GuidCreatorMock creator = new GuidCreatorMock(TIMESTAMP); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + long msb = creator.getRandomMsb(); + UUID uuid = new UUID(0, 0); for (int i = 0; i < DEFAULT_LOOP_MAX; i++) { uuid = creator.create(); } - long expected = high; - long randomMsb = (short) (uuid.getMostSignificantBits()); - assertEquals(String.format("The MSB should be iqual to %s.", expected), expected, randomMsb); + long expectedMsb = msb; + long randomMsb = creator.getRandomMsb(); + assertEquals("Wrong MSB after loop.", expectedMsb, randomMsb); + + randomMsb = creator.extractRandomMsb(uuid); + assertEquals("Wrong MSB after loop.", expectedMsb, randomMsb); } @Test - public void testShouldThrowOverflowException() { + public void testShouldThrowOverflowException1() { - long startLow = RANDOM.nextInt() + DEFAULT_LOOP_MAX; - long startHigh = (short) (RANDOM.nextInt() + 1); + long msbMax = 0x000001ffffffffffL; + long lsbMax = 0x000001ffffffffffL; - long low = startLow - DEFAULT_LOOP_MAX; - long high = (short) (startHigh - 1); + long msb = msbMax - 1; + long lsb = lsbMax - DEFAULT_LOOP_MAX; - GuidCreatorMock creator = new GuidCreatorMock(low, high, startLow, startHigh, TIMESTAMP); + GuidCreatorMock creator = new GuidCreatorMock(msb, lsb, msbMax, lsbMax, TIMESTAMP); + creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); + + for (int i = 0; i < DEFAULT_LOOP_MAX - 1; i++) { + creator.create(); + } + + try { + creator.create(); + fail("It should throw an overflow exception."); + } catch (UlidCreatorException e) { + // success + } + } + + @Test + public void testShouldThrowOverflowException2() { + + long msbMax = (RANDOM.nextLong() & GuidCreatorMock.HALF_RANDOM_COMPONENT) + | GuidCreatorMock.INCREMENT_MAX; + long lsbMax = (RANDOM.nextLong() & GuidCreatorMock.HALF_RANDOM_COMPONENT) + | GuidCreatorMock.INCREMENT_MAX; + + long msb = msbMax - 1; + long lsb = lsbMax - DEFAULT_LOOP_MAX; + + GuidCreatorMock creator = new GuidCreatorMock(msb, lsb, msbMax, lsbMax, TIMESTAMP); creator.withTimestampStrategy(new FixedTimestampStretegy(TIMESTAMP)); UUID uuid = new UUID(0, 0); @@ -123,19 +150,19 @@ public class GuidCreatorTest { uuid = creator.create(); } - long expectedLsb = startLow - 1; - long randomLsb = uuid.getLeastSignificantBits(); - assertEquals(String.format("The LSB should be iqual to %s.", expectedLsb), expectedLsb, randomLsb); + long expectedLsb = (lsbMax - 1) & GuidCreatorMock.HALF_RANDOM_COMPONENT; + long randomLsb = creator.extractRandomLsb(uuid); + assertEquals("Incorrect LSB after loop.", expectedLsb, randomLsb); - long expectedMsb = startHigh - 1; - long randomMsb = (short) (uuid.getMostSignificantBits()); - assertEquals(String.format("The MSB should be iqual to %s.", expectedMsb), expectedMsb, randomMsb); + long expectedMsb = (msbMax - 1) & GuidCreatorMock.HALF_RANDOM_COMPONENT; + long randomMsb = creator.extractRandomMsb(uuid); + assertEquals("Incorrect MSB after loop.", expectedMsb, randomMsb); try { creator.create(); fail("It should throw an overflow exception."); } catch (UlidCreatorException e) { - // Success + // success } } }