I used this ULID example in a project where I not only needed the uniqueness offered by ULID but also its lexicographic sortability.
I discovered, however, that no matter how much I tried, I could not just get the ids generated in a loop sorted.
e.g.
class Test{
public static void main(String[] args) {
ArrayList<String> ulids = new ArrayList<>();
for (int i = 0; i < 10; i++) {
ulids.add(ULID.generate());
}
System.out.println("Original:\n..." + ulids);
Collections.shuffle(ulids);
System.out.println("Shuffled:\n..." + ulids);
ulids.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
System.out.println("Sorted:\n..." + ulids);
}
}
Sample output:
Original:
...[01edrp4ng81d3mvkp8s7z19znm, 01edrp4ng872nwfj6b9fsxjkkd, 01edrp4ng86v07r6c9sh62ghr7, 01edrp4ng8bpfw3m2q8bynd5st, 01edrp4ng896t1qhsngrz3h251, 01edrp4ng8jne084nsw5saesfe, 01edrp4ng8w8qz9qtgy3958r1v, 01edrp4ng8fdn30qnr2ktddyz4, 01edrp4ng8ekj0vt393tw12x8j, 01edrp4ng80wacxxskgej5d8mm]
Shuffled:
...[01edrp4ng896t1qhsngrz3h251, 01edrp4ng8w8qz9qtgy3958r1v, 01edrp4ng86v07r6c9sh62ghr7, 01edrp4ng8bpfw3m2q8bynd5st, 01edrp4ng8fdn30qnr2ktddyz4, 01edrp4ng80wacxxskgej5d8mm, 01edrp4ng872nwfj6b9fsxjkkd, 01edrp4ng81d3mvkp8s7z19znm, 01edrp4ng8jne084nsw5saesfe, 01edrp4ng8ekj0vt393tw12x8j]
Sorted:
...[01edrp4ng80wacxxskgej5d8mm, 01edrp4ng81d3mvkp8s7z19znm, 01edrp4ng86v07r6c9sh62ghr7, 01edrp4ng872nwfj6b9fsxjkkd, 01edrp4ng896t1qhsngrz3h251, 01edrp4ng8bpfw3m2q8bynd5st, 01edrp4ng8ekj0vt393tw12x8j, 01edrp4ng8fdn30qnr2ktddyz4, 01edrp4ng8jne084nsw5saesfe, 01edrp4ng8w8qz9qtgy3958r1v]
I checked the implementation and figured that since time was a core factor in the generation of ULIDs, and also since the sensitivity of time used was the millisecond i.e. (System.currentTimeMillis())
, I could get them sorted by introducing some delay in my id generation loop.
I introduced a delay of about 5 milliseconds and the ids all came out sorted; e.g:
class TestWithMsDelay{
public static void main(String[] args) {
ArrayList<String> ulids = new ArrayList<>();
for (int i = 0; i < 10; i++) {
try {
Thread.sleep(5L);
ulids.add(ULID.generate());
} catch (Exception ex) {
ex.printStackTrace();
}
}
System.out.println("Original:\n..." + ulids);
Collections.shuffle(ulids);
System.out.println("Shuffled:\n..." + ulids);
ulids.sort(new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
System.out.println("Sorted:\n..." + ulids);
}
}
Sample output:
Original:
...[2rjdme5a5h2ntcd20xq4z487tx, 2rjdme63a23ddsy0km21n6n34a, 2rjdme6pnrenx79zd3jj18est4, 2rjdme70bv45b648p82dbj584n, 2rjdme7d8gx9v9db66ftsxbmqq, 2rjdme7psqdykt24qfymn2e4ba, 2rjdme80as7t1h1rr00m676718, 2rjdme8rztp50bad6ktkhrfhk8, 2rjdme93ngkxkfmf6aegqxer9e, 2rjdme9ea04x22rpx2f3rp5gez]
Shuffled:
...[2rjdme7psqdykt24qfymn2e4ba, 2rjdme6pnrenx79zd3jj18est4, 2rjdme80as7t1h1rr00m676718, 2rjdme63a23ddsy0km21n6n34a, 2rjdme93ngkxkfmf6aegqxer9e, 2rjdme70bv45b648p82dbj584n, 2rjdme9ea04x22rpx2f3rp5gez, 2rjdme8rztp50bad6ktkhrfhk8, 2rjdme7d8gx9v9db66ftsxbmqq, 2rjdme5a5h2ntcd20xq4z487tx]
Sorted:
...[2rjdme5a5h2ntcd20xq4z487tx, 2rjdme63a23ddsy0km21n6n34a, 2rjdme6pnrenx79zd3jj18est4, 2rjdme70bv45b648p82dbj584n, 2rjdme7d8gx9v9db66ftsxbmqq, 2rjdme7psqdykt24qfymn2e4ba, 2rjdme80as7t1h1rr00m676718, 2rjdme8rztp50bad6ktkhrfhk8, 2rjdme93ngkxkfmf6aegqxer9e, 2rjdme9ea04x22rpx2f3rp5gez]
This is not good enough for my work... I don't want to wait for any length of time to generate ulids(even if it is 10us - 100us), the concept of artificial delay bothers me very much, lol.
So, I modified ULID.java and changed the time source from System.currentTimeMillis()
to System.nanoTime()
To my surprise, I no longer needed any time delay in the loop to get the output ULIDs sortable.
I feel there must be a snag somewhere though; because the Java Spec warns that System.nanoTime()
is not necessarily more accurate than System.currentTimeMillis()
e.g in the Javadoc for System.nanoTime()
, it says:
This method provides nanosecond precision, but not necessarily nanosecond resolution (that is, how frequently the value changes) - no guarantees are made except that the resolution is at least as good as that of currentTimeMillis().
Also, the Javadoc for System.nanoTime()
seems to indicate that it is not relative to the Epoch (as is System.currentTimeMillis()
)
I believe that this may cause bad behaviour(messed up sortability over time and maybe affect uniqueness) in using System.nanoTime()
in ULID.java instead of
System.currentTimeMillis()
QUESTION
- Are my fears legitimate
- What can I do if (1.) is true, to improve the ULID's time-sensitivity beyond 1 millisecond without destroying its strong points?