From d082ff0a2bd4eb2cbfb5c625ad0c7188e3d2f2c2 Mon Sep 17 00:00:00 2001 From: Andrey Antukh Date: Thu, 25 Aug 2022 11:27:41 +0200 Subject: [PATCH] :sparkles: Improve UUIDv8 implementation --- common/src/app/common/UUIDv8.java | 123 ++++++++++++------ common/src/app/common/uuid_impl.js | 111 ++++++++-------- common/target/classes/app/common/UUIDv8.class | Bin 1284 -> 2190 bytes 3 files changed, 138 insertions(+), 96 deletions(-) diff --git a/common/src/app/common/UUIDv8.java b/common/src/app/common/UUIDv8.java index 1290d91b4..261ee1a93 100644 --- a/common/src/app/common/UUIDv8.java +++ b/common/src/app/common/UUIDv8.java @@ -10,12 +10,14 @@ It has the following characteristics: - time ordered - - 48bits timestamp - - custom epoch: milliseconds since 2022-01-01T00:00:00 - - 14bits monotonic clockseq (allows generate 16k uuids/ms) - - mostly static random 60 bits (initialized at class load or clock regression) + - 48bits timestamp (milliseconds precision, with custom epoch: 2022-01-01T00:00:00) + - 14bits random clockseq (monotonically increasing on timestamp conflict) + - spin locks (blocks) if more than 16384 ids/ms is generated in a single host + - 56bits of randomnes generated statically on load (resets on clock regression) + - 4 bits of user defined tag (defaults to 1 on jvm and 0 on js) - This is results in a constantly increasing, sortable, very fast uuid impl. + This results in a constantly increasing, sortable, very fast + and easy to visually read uuid implementation. */ package app.common; @@ -23,59 +25,98 @@ package app.common; import java.security.SecureRandom; import java.time.Clock; import java.util.UUID; +import java.util.concurrent.locks.Lock; +import java.util.concurrent.locks.ReentrantLock; +import java.time.Instant; public class UUIDv8 { - public static final long timeRef = 1640991600L * 1000L; // ms since 2022-01-01T00:00:00 - public static final long clockSeqMax = 16384L; // 14 bits space - public static final Clock clock = Clock.systemUTC(); + private static final long timeRef = 1640995200000L; // ms since 2022-01-01T00:00:00 + private static final Clock clock = Clock.systemUTC(); + private static final Lock lock = new ReentrantLock(); + private static final long baseMsb = 0x0000_0000_0000_8000L; // Version 8 + private static final long baseLsb = 0x8000_0000_0000_0000L; // Variant 2 + private static final long maxCs = 0x0000_0000_0000_3fffL; - public static long baseMsb; - public static long baseLsb; - public static long clockSeq = 0L; - public static long lastTs = 0L; + private static final SecureRandom srandom = new java.security.SecureRandom(); - public static SecureRandom srandom = new java.security.SecureRandom(); - - public static synchronized void initializeSeed() { - baseMsb = 0x0000_0000_0000_8000L; // Version 8 - baseLsb = srandom.nextLong() & 0x0fff_ffff_ffff_ffffL | 0x8000_0000_0000_0000L; // Variant 2 - } + private static long countCs = 0L; + private static long lastCs = 0L; + private static long lastTs = 0L; + private static long lastRd = 0L; static { - initializeSeed(); + lastRd = (srandom.nextLong() & 0xffff_ffff_ffff_f1ffL); + lastCs = (srandom.nextLong() & maxCs); } - public static synchronized UUID create(final long ts, final long clockSeq) { + public static UUID create(final long ts, final long lastRd, final long lastCs) { long msb = (baseMsb - | ((ts << 16) & 0xffff_ffff_ffff_0000L) - | ((clockSeq >>> 2) & 0x0000_0000_0000_0fffL)); - long lsb = baseLsb | ((clockSeq << 60) & 0x3000_0000_0000_0000L); + | (lastRd & 0xffff_ffff_ffff_0fffL)); + + long lsb = (baseLsb + | ((ts << 14) & 0x3fff_ffff_ffff_c000L) + | lastCs); + return new UUID(msb, lsb); } - public static synchronized UUID create() { - while (true) { - long ts = clock.millis() - timeRef; - - // Protect from clock regression - if ((ts - lastTs) < 0) { - initializeSeed(); - clockSeq = 0; - continue; + public static void setTag(final long tag) { + lock.lock(); + try { + if (tag > 0x0000_0000_0000_000fL) { + throw new IllegalArgumentException("tag value should fit in 4bits"); } - if (lastTs == ts) { - if (clockSeq < clockSeqMax) { - clockSeq++; - } else { + lastRd = (lastRd + & 0xffff_ffff_ffff_f0ffL + | ((tag << 8) & 0x0000_0000_0000_0f00L)); + + } finally { + lock.unlock(); + } + } + + public static Instant getTimestamp(final UUID uuid) { + final long lsb = uuid.getLeastSignificantBits(); + return Instant.EPOCH.plusMillis(timeRef).plusMillis((lsb >>> 14) & 0x0000_ffff_ffff_ffffL); + } + + public static UUID create() { + lock.lock(); + try { + while (true) { + long ts = (clock.millis() - timeRef); // in millis + + // If clock regression happens, regenerate lastRd + if ((ts - lastTs) < 0) { + // Clear and replace the 56 bits of randomness (60bits - 4 bits tag) + lastRd = (lastRd + & 0x0000_0000_0000_0f00L + | (srandom.nextLong() & 0xffff_ffff_ffff_f0ffL)); + + countCs = 0; continue; } - } else { - lastTs = ts; - clockSeq = 0; - } - return create(ts, clockSeq); + // If last timestamp is the same as the current one we proceed + // to increment the counters. + if (lastTs == ts) { + if (countCs < maxCs) { + lastCs = (lastCs + 1L) & maxCs; + countCs++; + } else { + continue; + } + } else { + lastTs = ts; + lastCs = srandom.nextLong() & maxCs; + countCs = 0; + } + + return create(ts, lastRd, lastCs); + } + } finally { + lock.unlock(); } } } diff --git a/common/src/app/common/uuid_impl.js b/common/src/app/common/uuid_impl.js index d3c657a18..3766d3191 100644 --- a/common/src/app/common/uuid_impl.js +++ b/common/src/app/common/uuid_impl.js @@ -43,30 +43,6 @@ goog.scope(function() { } })(); - /* - * The MIT License (MIT) - * - * Copyright (c) 2010-2016 Robert Kieffer and other contributors - * - * 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. - */ - const hexMap = []; for (let i = 0; i < 256; i++) { hexMap[i] = (i + 0x100).toString(16).substr(1); @@ -105,70 +81,95 @@ goog.scope(function() { self.v8 = (function () { const buff = new ArrayBuffer(16); - const buff8 = new Uint8Array(buff); + const int8 = new Uint8Array(buff); const view = new DataView(buff); - const timeRef = 1640991600 * 1000; // ms since 2022-01-01T00:00:00 - const maxClockSeq = 16384n; // 14 bits space + const tmpBuff = new ArrayBuffer(8); + const tmpView = new DataView(tmpBuff); + const tmpInt8 = new Uint8Array(tmpBuff); - let clockSeq = 0n; - let lastTs = 0n; - let baseMsb; - let baseLsb; + const timeRef = 1640995200000; // ms since 2022-01-01T00:00:00 + const maxCs = 0x0000_0000_0000_3fffn; // 14 bits space - function initializeSeed() { - fill(buff8); - baseMsb = 0x0000_0000_0000_8000n; // Version 8; - baseLsb = view.getBigUint64(8, false) & 0x0fff_ffff_ffff_ffffn | 0x8000_0000_0000_0000n; // Variant 2; - } + let countCs = 0n; + let lastRd = 0n; + let lastCs = 0n; + let lastTs = 0n; + let baseMsb = 0x0000_0000_0000_8000n; + let baseLsb = 0x8000_0000_0000_0000n; - function currentTimestamp() { + const currentTimestamp = () => { return BigInt.asUintN(64, "" + (Date.now() - timeRef)); - } + }; - initializeSeed(); + const nextLong = () => { + fill(tmpInt8); + return tmpView.getBigUint64(0, false); + }; + + lastRd = nextLong() & 0xffff_ffff_ffff_f0ffn; + lastCs = nextLong() & maxCs; + + const create = function create(ts, lastRd, lastCs) { + const msb = (baseMsb + | (lastRd & 0xffff_ffff_ffff_0fffn)); + + const lsb = (baseLsb + | ((ts << 14n) & 0x3fff_ffff_ffff_c000n) + | lastCs); - const create = function create(ts, clockSeq) { - let msb = (baseMsb - | ((ts << 16n) & 0xffff_ffff_ffff_0000n) - | ((clockSeq >> 2n) & 0x0000_0000_0000_0fffn)); - let lsb = baseLsb | ((clockSeq << 60n) & 0x3000_0000_0000_0000n); view.setBigUint64(0, msb, false); view.setBigUint64(8, lsb, false); - return core.uuid(toHexString(buff8)); - } + return core.uuid(toHexString(int8)); + }; const factory = function v8() { while (true) { let ts = currentTimestamp(); // Protect from clock regression - if ((ts-lastTs) < 0) { - initializeSeed(); - clockSeq = 0; + if ((ts - lastTs) < 0) { + lastRd = (lastRd + & 0x0000_0000_0000_0f00n + | (nextLong() & 0xffff_ffff_ffff_f0ffn)); + countCs = 0n; continue; } if (lastTs === ts) { - if (clockSeq < maxClockSeq) { - clockSeq++; + if (countCs < maxCs) { + lastCs = (lastCs + 1n) & maxCs; + countCs++; } else { continue; } } else { - clockSeq = 0n; lastTs = ts; + lastCs = nextLong() & maxCs; + countCs = 0; } - return create(ts, clockSeq); + return create(ts, lastRd, lastCs); } }; - factory.create = create - factory.initialize = initializeSeed; + const setTag = (tag) => { + tag = BigInt.asUintN(64, "" + tag); + if (tag > 0x0000_0000_0000_000fn) { + throw new Error("illegal arguments: tag value should fit in 4bits"); + } + + lastRd = (lastRd + & 0xffff_ffff_ffff_f0ffn + | ((tag << 8) & 0x0000_0000_0000_0f00n)); + }; + + factory.create = create; + factory.setTag = setTag; return factory; })(); + self.custom = function formatAsUUID(mostSigBits, leastSigBits) { const most = mostSigBits.toString("16").padStart(16, "0"); const least = leastSigBits.toString("16").padStart(16, "0"); diff --git a/common/target/classes/app/common/UUIDv8.class b/common/target/classes/app/common/UUIDv8.class index 81f01594992f3e33017897610369ca0ad6d6bc9b..284e5e166cfed47aa9021fd93082b29cbda4c824 100644 GIT binary patch literal 2190 zcmah~TT@e46#ll5D~EeTOEpHk1(Zgu*Fr@MSiykEO{t1K35RenIl-J0gti*-(ebH6 zr!yU=ufA%S0b6FO&wcD4Y5U%3`t6-Sn5i?(%*np2wbxqTx4yH_Z~y%AD}c|j;>Rvj zc~Gsv1-C%MyuPSMQo5Cl+=$N`2}i))ZCa*tNuVm+KIws1piy5~h$J%UbjFHIObqlb zUT`rOe^22ms$YH4^;GQut{%1X9v(Ca)T#J6$4se{el#K=Q|}gVg$D=QCw=tWqhT*T z5~xmP5)T9p#wv#r87q;?W(~`UNTVHz@xQAEA7h^f`{g-;IPgFIlIZ@C z8_{b5&7~@L8oOA8c+&L8OZXY5N5f_G3iuaNIeW-VrA#Ji2@ke^_+Y8^vVr{?uHp-3 zN|ZX-xZ^-nZgu%FfI$zgYlulq^_4P}oxsy7pNuxF;l?&;_ibN|iLQXeksERT6GhSyz7*S-ouWs4**08_kqdFe!CR zOI){&Av<24#pKMD)|aAebagZ{LznuPX&J-0blk{}>+uwK+_vG2>q&Z03X=>v=IDtB zL;8ZUd+;@ZsCt$D$LF${N2P-rl{?N-sKfMvz>)H6QsHGM`m$W9;ifHCDvoSZcOq53 zm;7UyTsC1`HDz%%r3>j3SpW{QCA)YJ)u0s_bUF> zx|VBu$NDbU)F03Jr9RY1DX5WBRU_YXj&A1U#maw&tNk1WM_0!N#2Zcoa%Cs*{_sih za8J5k;~_uCZ~(qg05$oQ^X>p@gAH}d0o3PLTB=vhdpFTI)#7gPZlL+~cC``ml&c<*45=swfH}z6^jqi^vR)5w@z(iH(e?~4SYnB` zVKj3-K}v3Oc84mthhw;p)6j7aaa`m@(Th0}XyO*;xs$;xm2Bf78|&a1|DSVw!SQ>N zE9q^9%lkJ@R)+&c1h^KYnhBn4OL0YU1oXd!v4*})m{S{gu!fdROmLQ3!-xdKXPVCt zAA%|&O_PP~g@Rp?a)%Oa8`B)31j3X+2ZA_`Hk_b5I=MQ@56da^5uHF@aq#qCfgACS literal 1284 zcmZ`&T~kw66kP{$^La@kUMVU>>Nr6BAYd(GjiAO_NP{Q=W_-N49G$B-H#GOgkHL<#D1Q4$Ac<5Arime>1y0#JI|Zjv zuQ&X{=H|+SR|^8MnjOg1pe7*73Z6A_3d6JqP21mY)CI;W`nfmW|9w_cQi2U1+*F6BiU$&8rgBj4lRLuD{HQ zG24U3FeQ-IFSbJ0(~^>yL|&EYvVbvNF6XzDsH-NfVTKmRYd9}ih_0m2t;<45nW$qH zb1_^u@rBZn(OLU`7P#(uo*R&b_;L97*L{`trilf`HR{nXZRUlR9Cl< zFEK(!mvBpGU+ZQ6nH*KwDs5bOi-Lt4aw*rwyi6Rv!O$^oB|0ena`G2Vxx=3@+D6gB z(&3wv4?HDpg54ll^LXXUgUZYXK){*)HOzB7dT~8SjHSyP$cp$ z)@WVFW1