diff --git a/CHANGELOG.md b/CHANGELOG.md
index 536ac9e..8edc7e6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,7 +6,11 @@ All notable changes to this project will be documented in this file.
Nothing unreleased.
-## [4.1.1] - 2021-11-06
+## [4.2.0] - 2022-04-21
+
+Handle clock drift. #18
+
+## [4.1.2] - 2021-11-06
Compare internal fields as unsigned integers.
@@ -278,7 +282,8 @@ Project created as an alternative Java implementation of [ULID spec](https://git
- Added `LICENSE`
- Added test cases
-[unreleased]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-4.1.2...HEAD
+[unreleased]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-4.2.0...HEAD
+[4.2.0]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-4.1.2...ulid-creator-4.2.0
[4.1.2]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-4.1.1...ulid-creator-4.1.2
[4.1.1]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-4.1.0...ulid-creator-4.1.1
[4.1.0]: https://github.com/f4b6a3/ulid-creator/compare/ulid-creator-4.0.0...ulid-creator-4.1.0
diff --git a/README.md b/README.md
index 1d03f96..ca9cf3c 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ Add these lines to your `pom.xml`.
com.github.f4b6a3
ulid-creator
- 4.1.2
+ 4.2.0
```
See more options in [maven.org](https://search.maven.org/artifact/com.github.f4b6a3/ulid-creator).
diff --git a/src/main/java/com/github/f4b6a3/ulid/Ulid.java b/src/main/java/com/github/f4b6a3/ulid/Ulid.java
index 908ead1..da89da1 100644
--- a/src/main/java/com/github/f4b6a3/ulid/Ulid.java
+++ b/src/main/java/com/github/f4b6a3/ulid/Ulid.java
@@ -1,7 +1,7 @@
/*
* MIT License
*
- * Copyright (c) 2020-2021 Fabio Lima
+ * Copyright (c) 2020-2022 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
@@ -584,12 +584,12 @@ public final class Ulid implements Serializable, Comparable {
* millisecond;
*
* (2) This method can generate monotonic increasing ULIDs 99.999999999999992%
- * ((2^80 - 10^9) / (2^80)) of the time, considering an unrealistic rate of
- * 1,000,000,000 ULIDs per millisecond.
+ * ((2^80 - 10^9) / (2^80)) of the time within a single millisecond interval,
+ * considering an unrealistic rate of 1,000,000,000 ULIDs per millisecond.
*
* Due to (1) and (2), it does not throw the error message recommended by the
- * specification. When an overflow occurs in the last 80 bits, the random
- * component simply wraps around.
+ * specification. When an overflow occurs in the random 80 bits, the time
+ * component is simply incremented.
*
* @return a ULID
*/
@@ -599,8 +599,7 @@ public final class Ulid implements Serializable, Comparable {
long newLsb = this.lsb + 1; // increment the LEAST significant bits
if (newLsb == INCREMENT_OVERFLOW) {
- // carrying the extra bit by incrementing the MOST significant bits
- newMsb = (newMsb & 0xffffffffffff0000L) | ((newMsb + 1) & 0x000000000000ffffL);
+ newMsb += 1; // increment the MOST significant bits
}
return new Ulid(newMsb, newLsb);
diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java
index 8446421..ef56a09 100644
--- a/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java
+++ b/src/main/java/com/github/f4b6a3/ulid/UlidCreator.java
@@ -1,7 +1,7 @@
/*
* MIT License
*
- * Copyright (c) 2020-2021 Fabio Lima
+ * Copyright (c) 2020-2022 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
diff --git a/src/main/java/com/github/f4b6a3/ulid/UlidFactory.java b/src/main/java/com/github/f4b6a3/ulid/UlidFactory.java
index d5437c9..e2e4403 100644
--- a/src/main/java/com/github/f4b6a3/ulid/UlidFactory.java
+++ b/src/main/java/com/github/f4b6a3/ulid/UlidFactory.java
@@ -1,7 +1,7 @@
/*
* MIT License
*
- * Copyright (c) 2020-2021 Fabio Lima
+ * Copyright (c) 2020-2022 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
@@ -25,6 +25,7 @@
package com.github.f4b6a3.ulid;
import java.security.SecureRandom;
+import java.time.Clock;
import java.util.Random;
import java.util.function.LongFunction;
import java.util.function.Supplier;
@@ -42,14 +43,20 @@ import java.util.function.Supplier;
*/
public final class UlidFactory {
+ private final Clock clock; // for tests
private final LongFunction ulidFunction;
public UlidFactory() {
- this.ulidFunction = new UlidFunction();
+ this(new UlidFunction(), null);
}
private UlidFactory(LongFunction ulidFunction) {
+ this(ulidFunction, null);
+ }
+
+ private UlidFactory(LongFunction ulidFunction, Clock clock) {
this.ulidFunction = ulidFunction;
+ this.clock = clock != null ? clock : Clock.systemUTC();
}
/**
@@ -85,6 +92,19 @@ public final class UlidFactory {
return new UlidFactory(new UlidFunction(randomSupplier));
}
+ /**
+ * Returns a new factory.
+ *
+ * The given random supplier must return an array of 10 bytes.
+ *
+ * @param randomSupplier a random supplier that returns 10 bytes
+ * @param clock a custom clock instance for tests
+ * @return {@link UlidFactory}
+ */
+ protected static UlidFactory newInstance(Supplier randomSupplier, Clock clock) {
+ return new UlidFactory(new UlidFunction(randomSupplier), clock);
+ }
+
/**
* Returns a new monotonic factory.
*
@@ -116,13 +136,26 @@ public final class UlidFactory {
return new UlidFactory(new MonotonicFunction(randomSupplier));
}
+ /**
+ * Returns a new monotonic factory.
+ *
+ * The given random supplier must return an array of 10 bytes.
+ *
+ * @param randomSupplier a random supplier that returns 10 bytes
+ * @param clock a custom clock instance for tests
+ * @return {@link UlidFactory}
+ */
+ protected static UlidFactory newMonotonicInstance(Supplier randomSupplier, Clock clock) {
+ return new UlidFactory(new MonotonicFunction(randomSupplier), clock);
+ }
+
/**
* Returns a UUID.
*
* @return a ULID
*/
public Ulid create() {
- return create(System.currentTimeMillis());
+ return create(clock.millis());
}
/**
@@ -168,9 +201,14 @@ public final class UlidFactory {
*/
protected static final class MonotonicFunction implements LongFunction {
- private long lastTime = -1;
+ private long lastTime = 0;
private Ulid lastUlid = null;
+ // Used to preserve monotonicity when the system clock is
+ // adjusted by NTP after a small clock drift or when the
+ // system clock jumps back by 1 second due to leap second.
+ protected static final int CLOCK_DRIFT_TOLERANCE = 10_000;
+
// it must return an array of 10 bytes
private Supplier randomSupplier;
@@ -189,13 +227,16 @@ public final class UlidFactory {
@Override
public synchronized Ulid apply(final long time) {
- if (time == this.lastTime) {
+ // Check if the current time is the same as the previous time or has moved
+ // backwards after a small system clock adjustment or after a leap second.
+ // Drift tolerance = (previous_time - 10s) < current_time <= previous_time
+ if ((time > this.lastTime - CLOCK_DRIFT_TOLERANCE) && (time <= this.lastTime)) {
this.lastUlid = lastUlid.increment();
} else {
this.lastUlid = new Ulid(time, this.randomSupplier.get());
}
- this.lastTime = time;
+ this.lastTime = lastUlid.getTime();
return new Ulid(this.lastUlid);
}
}
diff --git a/src/test/java/com/github/f4b6a3/ulid/UlidFactoryDefaultfTest.java b/src/test/java/com/github/f4b6a3/ulid/UlidFactoryDefaultfTest.java
index eca2121..55d9729 100644
--- a/src/test/java/com/github/f4b6a3/ulid/UlidFactoryDefaultfTest.java
+++ b/src/test/java/com/github/f4b6a3/ulid/UlidFactoryDefaultfTest.java
@@ -8,7 +8,11 @@ import com.github.f4b6a3.ulid.UlidFactory;
import static org.junit.Assert.*;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
import java.util.Random;
+import java.util.function.Supplier;
public class UlidFactoryDefaultfTest extends UlidFactoryTest {
@@ -29,6 +33,97 @@ public class UlidFactoryDefaultfTest extends UlidFactoryTest {
checkCreationTime(list, startTime, endTime);
}
+ @Test
+ public void testClockDrift() {
+
+ long diff = UlidFactory.MonotonicFunction.CLOCK_DRIFT_TOLERANCE;
+ long time = Instant.parse("2021-12-31T23:59:59.000Z").toEpochMilli();
+ long times[] = { time, time + 0, time + 1, time + 2, time + 3 - diff, time + 4 - diff, time + 5 };
+
+ Clock clock = new Clock() {
+ private int i;
+
+ @Override
+ public long millis() {
+ return times[i++ % times.length];
+ }
+
+ @Override
+ public ZoneId getZone() {
+ return null;
+ }
+
+ @Override
+ public Clock withZone(ZoneId zone) {
+ return null;
+ }
+
+ @Override
+ public Instant instant() {
+ return null;
+ }
+ };
+
+ Supplier randomSupplier = UlidFactory.getRandomSupplier(new Random());
+ UlidFactory factory = UlidFactory.newInstance(randomSupplier, clock);
+
+ long ms1 = factory.create().getTime(); // time
+ long ms2 = factory.create().getTime(); // time + 0
+ long ms3 = factory.create().getTime(); // time + 1
+ long ms4 = factory.create().getTime(); // time + 2
+ long ms5 = factory.create().getTime(); // time + 3 - 10000 (CLOCK DRIFT)
+ long ms6 = factory.create().getTime(); // time + 4 - 10000 (CLOCK DRIFT)
+ long ms7 = factory.create().getTime(); // time + 5
+ assertEquals(times[0], ms1);
+ assertEquals(times[1], ms2);
+ assertEquals(times[2], ms3);
+ assertEquals(times[3], ms4);
+ assertEquals(times[4], ms5);
+ assertEquals(times[5], ms6);
+ assertEquals(times[6], ms7);
+ }
+
+ @Test
+ public void testLeapSecond() {
+
+ long second = Instant.parse("2021-12-31T23:59:59.000Z").getEpochSecond();
+ long leapSecond = second - 1; // simulate a leap second
+ long times[] = { second, leapSecond };
+
+ Clock clock = new Clock() {
+ private int i;
+
+ @Override
+ public long millis() {
+ return times[i++ % times.length] * 1000;
+ }
+
+ @Override
+ public ZoneId getZone() {
+ return null;
+ }
+
+ @Override
+ public Clock withZone(ZoneId zone) {
+ return null;
+ }
+
+ @Override
+ public Instant instant() {
+ return null;
+ }
+ };
+
+ Supplier randomSupplier = UlidFactory.getRandomSupplier(new Random());
+ UlidFactory factory = UlidFactory.newInstance(randomSupplier, clock);
+
+ long ms1 = factory.create().getTime(); // second
+ long ms2 = factory.create().getTime(); // leap second
+
+ assertEquals(times[0] * 1000, ms1);
+ assertEquals(times[1] * 1000, ms2);
+ }
+
@Test
public void testGetUlidInParallel() throws InterruptedException {
diff --git a/src/test/java/com/github/f4b6a3/ulid/UlidFactoryMonotonicTest.java b/src/test/java/com/github/f4b6a3/ulid/UlidFactoryMonotonicTest.java
index 662787d..582c5f5 100644
--- a/src/test/java/com/github/f4b6a3/ulid/UlidFactoryMonotonicTest.java
+++ b/src/test/java/com/github/f4b6a3/ulid/UlidFactoryMonotonicTest.java
@@ -8,8 +8,12 @@ import com.github.f4b6a3.ulid.UlidFactory;
import static org.junit.Assert.*;
+import java.time.Clock;
+import java.time.Instant;
+import java.time.ZoneId;
import java.util.Arrays;
import java.util.Random;
+import java.util.function.Supplier;
public class UlidFactoryMonotonicTest extends UlidFactoryTest {
@@ -31,6 +35,95 @@ public class UlidFactoryMonotonicTest extends UlidFactoryTest {
checkCreationTime(list, startTime, endTime);
}
+ @Test
+ public void testClockDrift() {
+
+ long diff = UlidFactory.MonotonicFunction.CLOCK_DRIFT_TOLERANCE;
+ long time = Instant.parse("2021-12-31T23:59:59.000Z").toEpochMilli();
+ long times[] = { time, time + 0, time + 1, time + 2, time + 3 - diff, time + 4 - diff, time + 5 };
+
+ Clock clock = new Clock() {
+ private int i;
+
+ @Override
+ public long millis() {
+ return times[i++ % times.length];
+ }
+
+ @Override
+ public ZoneId getZone() {
+ return null;
+ }
+
+ @Override
+ public Clock withZone(ZoneId zone) {
+ return null;
+ }
+
+ @Override
+ public Instant instant() {
+ return null;
+ }
+ };
+
+ Supplier randomSupplier = UlidFactory.getRandomSupplier(new Random());
+ UlidFactory factory = UlidFactory.newMonotonicInstance(randomSupplier, clock);
+
+ long ms1 = factory.create().getTime(); // time
+ long ms2 = factory.create().getTime(); // time + 0
+ long ms3 = factory.create().getTime(); // time + 1
+ long ms4 = factory.create().getTime(); // time + 2
+ long ms5 = factory.create().getTime(); // time + 3 - 10000 (CLOCK DRIFT)
+ long ms6 = factory.create().getTime(); // time + 4 - 10000 (CLOCK DRIFT)
+ long ms7 = factory.create().getTime(); // time + 5
+ assertEquals(ms1 + 0, ms2); // clock repeats.
+ assertEquals(ms1 + 1, ms3); // clock advanced.
+ assertEquals(ms1 + 2, ms4); // clock advanced.
+ assertEquals(ms1 + 2, ms5); // CLOCK DRIFT! DON'T MOVE BACKWARDS!
+ assertEquals(ms1 + 2, ms6); // CLOCK DRIFT! DON'T MOVE BACKWARDS!
+ assertEquals(ms1 + 5, ms7); // clock advanced.
+ }
+
+ @Test
+ public void testLeapSecond() {
+
+ long second = Instant.parse("2021-12-31T23:59:59.000Z").getEpochSecond();
+ long leapSecond = second - 1; // simulate a leap second
+ long times[] = { second, leapSecond };
+
+ Clock clock = new Clock() {
+ private int i;
+
+ @Override
+ public long millis() {
+ return times[i++ % times.length] * 1000;
+ }
+
+ @Override
+ public ZoneId getZone() {
+ return null;
+ }
+
+ @Override
+ public Clock withZone(ZoneId zone) {
+ return null;
+ }
+
+ @Override
+ public Instant instant() {
+ return null;
+ }
+ };
+
+ Supplier randomSupplier = UlidFactory.getRandomSupplier(new Random());
+ UlidFactory factory = UlidFactory.newMonotonicInstance(randomSupplier, clock);
+
+ long ms1 = factory.create().getTime(); // second
+ long ms2 = factory.create().getTime(); // leap second
+
+ assertEquals(ms1, ms2); // LEAP SECOND! DON'T MOVE BACKWARDS!
+ }
+
private void checkOrdering(Ulid[] list) {
Ulid[] other = Arrays.copyOf(list, list.length);
Arrays.sort(other);