diff --git a/backend/build.clj b/backend/build.clj index 9db6eea2f..9a7f3bba2 100644 --- a/backend/build.clj +++ b/backend/build.clj @@ -33,4 +33,4 @@ {:src-dirs ["dev/java"] :class-dir class-dir :basis basis - :javac-opts ["-source" "11" "-target" "11"]})) + :javac-opts ["-source" "17" "-target" "17"]})) diff --git a/backend/deps.edn b/backend/deps.edn index aabbb28a2..509b4372e 100644 --- a/backend/deps.edn +++ b/backend/deps.edn @@ -10,6 +10,7 @@ com.github.luben/zstd-jni {:mvn/version "1.5.2-3"} org.clojure/data.fressian {:mvn/version "1.0.0"} + io.prometheus/simpleclient {:mvn/version "0.15.0"} io.prometheus/simpleclient_hotspot {:mvn/version "0.15.0"} io.prometheus/simpleclient_jetty {:mvn/version "0.15.0" diff --git a/backend/test/app/test_helpers.clj b/backend/test/app/test_helpers.clj index 8f436ea02..6768b2a3e 100644 --- a/backend/test/app/test_helpers.clj +++ b/backend/test/app/test_helpers.clj @@ -34,7 +34,9 @@ [mockery.core :as mk] [promesa.core :as p] [yetti.request :as yrq]) - (:import org.postgresql.ds.PGSimpleDataSource)) + (:import + java.util.UUID + org.postgresql.ds.PGSimpleDataSource)) (def ^:dynamic *system* nil) (def ^:dynamic *pool* nil) @@ -128,8 +130,8 @@ (defn mk-uuid [prefix & args] - (uuid/namespaced uuid/zero (apply str prefix args))) - + (UUID/nameUUIDFromBytes (-> (apply str prefix args) + (.getBytes "UTF-8")))) ;; --- FACTORIES (defn create-profile* diff --git a/common/build.clj b/common/build.clj new file mode 100644 index 000000000..0719c72aa --- /dev/null +++ b/common/build.clj @@ -0,0 +1,15 @@ +(ns build + (:refer-clojure :exclude [compile]) + (:require [clojure.tools.build.api :as b])) + +(def class-dir "target/classes") +(def basis (b/create-basis {:project "deps.edn"})) + +(defn clean [_] + (b/delete {:path "target"})) + +(defn compile [_] + (b/javac {:src-dirs ["src"] + :class-dir class-dir + :basis basis + :javac-opts ["-source" "17" "-target" "17"]})) diff --git a/common/deps.edn b/common/deps.edn index ae4124dc5..202f57b3e 100644 --- a/common/deps.edn +++ b/common/deps.edn @@ -28,7 +28,6 @@ :exclusions [org.clojure/data.json]} frankiesardo/linked {:mvn/version "1.3.0"} - danlentz/clj-uuid {:mvn/version "0.1.9"} commons-io/commons-io {:mvn/version "2.11.0"} com.sun.mail/jakarta.mail {:mvn/version "2.0.1"} @@ -36,7 +35,7 @@ fipp/fipp {:mvn/version "0.6.26"} io.aviso/pretty {:mvn/version "1.1.1"} environ/environ {:mvn/version "1.2.0"}} - :paths ["src"] + :paths ["src" "target/classes"] :aliases {:dev {:extra-deps @@ -48,6 +47,10 @@ mockery/mockery {:mvn/version "RELEASE"}} :extra-paths ["test" "dev"]} + :build + {:extra-deps {io.github.clojure/tools.build {:git/tag "v0.8.1" :git/sha "7d40500"}} + :ns-default build} + :test {:extra-paths ["test"] :extra-deps diff --git a/common/src/app/common/UUIDv8.java b/common/src/app/common/UUIDv8.java new file mode 100644 index 000000000..1290d91b4 --- /dev/null +++ b/common/src/app/common/UUIDv8.java @@ -0,0 +1,81 @@ +/* + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + + Copyright (c) UXBOX Labs SL + + This file contains a UUIDv8 with conformance with + https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format + + 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) + + This is results in a constantly increasing, sortable, very fast uuid impl. +*/ + +package app.common; + +import java.security.SecureRandom; +import java.time.Clock; +import java.util.UUID; + +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(); + + public static long baseMsb; + public static long baseLsb; + public static long clockSeq = 0L; + public static long lastTs = 0L; + + 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 + } + + static { + initializeSeed(); + } + + public static synchronized UUID create(final long ts, final long clockSeq) { + long msb = (baseMsb + | ((ts << 16) & 0xffff_ffff_ffff_0000L) + | ((clockSeq >>> 2) & 0x0000_0000_0000_0fffL)); + long lsb = baseLsb | ((clockSeq << 60) & 0x3000_0000_0000_0000L); + 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; + } + + if (lastTs == ts) { + if (clockSeq < clockSeqMax) { + clockSeq++; + } else { + continue; + } + } else { + lastTs = ts; + clockSeq = 0; + } + + return create(ts, clockSeq); + } + } +} diff --git a/common/src/app/common/uuid.cljc b/common/src/app/common/uuid.cljc index 1a71256ef..966ee0cc9 100644 --- a/common/src/app/common/uuid.cljc +++ b/common/src/app/common/uuid.cljc @@ -7,12 +7,12 @@ (ns app.common.uuid (:refer-clojure :exclude [next uuid zero?]) (:require - #?(:clj [app.common.data.macros :as dm]) - #?(:clj [clj-uuid :as impl]) #?(:clj [clojure.core :as c]) #?(:cljs [app.common.uuid-impl :as impl]) #?(:cljs [cljs.core :as c])) - #?(:clj (:import java.util.UUID))) + #?(:clj (:import + java.util.UUID + app.common.UUIDv8))) (def zero #uuid "00000000-0000-0000-0000-000000000000") @@ -22,32 +22,31 @@ (defn next [] - #?(:clj (impl/v1) - :cljs (impl/v1))) + #?(:clj (UUIDv8/create) + :cljs (impl/v8))) (defn random "Alias for clj-uuid/v4." [] - #?(:clj (impl/v4) + #?(:clj (UUID/randomUUID) :cljs (impl/v4))) -#?(:clj - (defn namespaced - [ns data] - (impl/v5 ns data))) - (defn uuid "Parse string uuid representation into proper UUID instance." [s] #?(:clj (UUID/fromString s) - :cljs (c/uuid s))) + :cljs (c/parse-uuid s))) (defn custom - ([a] #?(:clj (UUID. 0 a) :cljs (c/uuid (impl/custom 0 a)))) - ([b a] #?(:clj (UUID. b a) :cljs (c/uuid (impl/custom b a))))) + ([a] #?(:clj (UUID. 0 a) :cljs (c/parse-uuid (impl/custom 0 a)))) + ([b a] #?(:clj (UUID. b a) :cljs (c/parse-uuid (impl/custom b a))))) #?(:clj - (dm/export impl/get-word-high)) + (defn get-word-high + [id] + (.getMostSignificantBits ^UUID id))) #?(:clj - (dm/export impl/get-word-low)) + (defn get-word-low + [id] + (.getLeastSignificantBits ^UUID id))) diff --git a/common/src/app/common/uuid_impl.js b/common/src/app/common/uuid_impl.js index e05f35853..d3c657a18 100644 --- a/common/src/app/common/uuid_impl.js +++ b/common/src/app/common/uuid_impl.js @@ -92,104 +92,82 @@ goog.scope(function() { hexMap[buf[i++]]); } - const buff = new Uint8Array(16); + self.v4 = (function () { + const buff8 = new Uint8Array(16); - function v4() { - fill(buff); - buff[6] = (buff[6] & 0x0f) | 0x40; - buff[8] = (buff[8] & 0x3f) | 0x80; - return core.uuid(toHexString(buff)); - } + return function v4() { + fill(buff8); + buff8[6] = (buff8[6] & 0x0f) | 0x40; + buff8[8] = (buff8[8] & 0x3f) | 0x80; + return core.uuid(toHexString(buff8)); + }; + })(); - let initialized = false; - let node; - let clockseq; - let lastms = 0; - let lastns = 0; + self.v8 = (function () { + const buff = new ArrayBuffer(16); + const buff8 = new Uint8Array(buff); + const view = new DataView(buff); - function v1() { - let cs = clockseq; + const timeRef = 1640991600 * 1000; // ms since 2022-01-01T00:00:00 + const maxClockSeq = 16384n; // 14 bits space - if (!initialized) { - const seed = new Uint8Array(8) - fill(seed); + let clockSeq = 0n; + let lastTs = 0n; + let baseMsb; + let baseLsb; - // Per 4.5, create and 48-bit node id, (47 random bits + multicast bit = 1) - node = [ - seed[0] | 0x01, - seed[1], - seed[2], - seed[3], - seed[4], - seed[5] - ]; - - // Per 4.2.2, randomize (14 bit) clockseq - cs = clockseq = (seed[6] << 8 | seed[7]) & 0x3fff; - initialized = true; + 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 ms = Date.now(); - let ns = lastns + 1; - let dt = (ms - lastms) + (ns - lastns) / 10000; - - // Per 4.2.1.2, Bump clockseq on clock regression - if (dt < 0) { - cs = cs + 1 & 0x3fff; + function currentTimestamp() { + return BigInt.asUintN(64, "" + (Date.now() - timeRef)); } - // Reset nsecs if clock regresses (new clockseq) or we've moved onto a new - // time interval - if (dt < 0 || ms > lastms) { - ns = 0; + initializeSeed(); + + 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)); } - // Per 4.2.1.2 Throw error if too many uuids are requested - if (ns >= 10000) { - throw new Error("uuid v1 can't create more than 10M uuids/s") - } + const factory = function v8() { + while (true) { + let ts = currentTimestamp(); - lastms = ms; - lastns = ns; - clockseq = cs; + // Protect from clock regression + if ((ts-lastTs) < 0) { + initializeSeed(); + clockSeq = 0; + continue; + } - // Per 4.1.4 - Convert from unix epoch to Gregorian epoch - ms += 12219292800000; + if (lastTs === ts) { + if (clockSeq < maxClockSeq) { + clockSeq++; + } else { + continue; + } + } else { + clockSeq = 0n; + lastTs = ts; + } - let i = 0; + return create(ts, clockSeq); + } + }; - // `time_low` - var tl = ((ms & 0xfffffff) * 10000 + ns) % 0x100000000; - buff[i++] = tl >>> 24 & 0xff; - buff[i++] = tl >>> 16 & 0xff; - buff[i++] = tl >>> 8 & 0xff; - buff[i++] = tl & 0xff; - - // `time_mid` - var tmh = (ms / 0x100000000 * 10000) & 0xfffffff; - buff[i++] = tmh >>> 8 & 0xff; - buff[i++] = tmh & 0xff; - - // `time_high_and_version` - buff[i++] = tmh >>> 24 & 0xf | 0x10; // include version - buff[i++] = tmh >>> 16 & 0xff; - - // `clock_seq_hi_and_reserved` (Per 4.2.2 - include variant) - buff[i++] = cs >>> 8 | 0x80; - - // `clock_seq_low` - buff[i++] = cs & 0xff; - - // `node` - for (var n = 0; n < 6; ++n) { - buff[i + n] = node[n]; - } - - return core.uuid(toHexString(buff)); - } - - self.v1 = v1; - self.v4 = v4; + factory.create = create + factory.initialize = initializeSeed; + return factory; + })(); self.custom = function formatAsUUID(mostSigBits, leastSigBits) { const most = mostSigBits.toString("16").padStart(16, "0"); diff --git a/common/target/classes/app/common/UUIDv8.class b/common/target/classes/app/common/UUIDv8.class new file mode 100644 index 000000000..81f015949 Binary files /dev/null and b/common/target/classes/app/common/UUIDv8.class differ diff --git a/common/test/app/common/uuid_test.cljc b/common/test/app/common/uuid_test.cljc new file mode 100644 index 000000000..9f773ae97 --- /dev/null +++ b/common/test/app/common/uuid_test.cljc @@ -0,0 +1,27 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.uuid-test + (:require + [app.common.uuid :as uuid] + [clojure.test :as t] + [clojure.test.check.clojure-test :refer (defspec)] + [clojure.test.check.generators :as gen] + [clojure.test.check.properties :as props])) + +(def uuid-gen + (->> gen/large-integer (gen/fmap (fn [_] (uuid/next))))) + +(defspec non-repeating-uuid-next-1 100000 + (props/for-all + [uuid1 uuid-gen + uuid2 uuid-gen + uuid3 uuid-gen + uuid4 uuid-gen + uuid5 uuid-gen] + (t/is (not= uuid1 uuid2 uuid3 uuid4 uuid5)))) + + diff --git a/frontend/dev/cljs/user.cljs b/frontend/dev/cljs/user.cljs index 7fb44ff0e..992d45613 100644 --- a/frontend/dev/cljs/user.cljs +++ b/frontend/dev/cljs/user.cljs @@ -62,3 +62,4 @@ "get-in" (bench-get-in) (println "available: str select-keys get-in"))) +