From 960071a4f209393e193117ed4234cab9cdb60a46 Mon Sep 17 00:00:00 2001 From: it512 Date: Tue, 9 Jan 2024 23:09:32 +0800 Subject: [PATCH 1/7] Monotonicity in UUIDv7 --- time.go | 12 ++++++++---- uuid_test.go | 23 +++++++++++++++++++++-- version7.go | 28 +++++++++++++++++++++++----- 3 files changed, 52 insertions(+), 11 deletions(-) diff --git a/time.go b/time.go index c351129..b3d77ef 100644 --- a/time.go +++ b/time.go @@ -27,6 +27,8 @@ var ( lasttime uint64 // last time we returned clockSeq uint16 // clock sequence for this run + lasttimev7 int64 // for uuid v7 + timeNow = time.Now // for testing ) @@ -67,12 +69,12 @@ func getTime() (Time, uint16, error) { } // ClockSequence returns the current clock sequence, generating one if not -// already set. The clock sequence is only used for Version 1 UUIDs. +// already set. The clock sequence is used for Version 1 and 7 UUIDs. // // The uuid package does not use global static storage for the clock sequence or // the last time a UUID was generated. Unless SetClockSequence is used, a new // random clock sequence is generated the first time a clock sequence is -// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +// requested by ClockSequence, GetTime, NewV7 or NewUUID. (section 4.2.1.1) func ClockSequence() int { defer timeMu.Unlock() timeMu.Lock() @@ -86,8 +88,9 @@ func clockSequence() int { return int(clockSeq & 0x3fff) } -// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to -// -1 causes a new sequence to be generated. +// SetClockSequence sets the clock sequence to the lower 14 bits of seq +// uuid v1 and v6 use 14 bits seq. uuid v7 use 12 bits seq. +// Setting to -1 causes a new sequence to be generated. func SetClockSequence(seq int) { defer timeMu.Unlock() timeMu.Lock() @@ -104,6 +107,7 @@ func setClockSequence(seq int) { clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant if oldSeq != clockSeq { lasttime = 0 + lasttimev7 = 0 } } diff --git a/uuid_test.go b/uuid_test.go index 83cee2c..f1b9cb7 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -825,7 +825,7 @@ func TestVersion6(t *testing.T) { func TestVersion7(t *testing.T) { SetRand(nil) m := make(map[string]bool) - for x := 1; x < 32; x++ { + for x := 1; x < 128; x++ { uuid, err := NewV7() if err != nil { t.Fatalf("could not create UUID: %v", err) @@ -887,7 +887,26 @@ func TestVersion7FromReader(t *testing.T) { if err != nil { t.Errorf("failed generating UUID from a reader") } - if uuid1 != uuid3 { + if uuid1 == uuid3 { // Montonicity t.Errorf("expected duplicates, got %q and %q", uuid1, uuid3) } } + +func TestVersion7Monotonicity(t *testing.T) { + length := 4097 // [0x000 - 0xfff] + myString := "8059ddhdle77cb52" + + SetClockSequence(0) + + uuids := make([]string, length) + for i := 0; i < length; i++ { + uuidString, _ := NewV7FromReader(bytes.NewReader([]byte(myString))) + uuids[i] = uuidString.String() + } + + for i := 1; i < len(uuids); i++ { + if uuids[i-1] > uuids[i] { + t.Errorf("expected seq got %s > %s", uuids[i-1], uuids[i]) + } + } +} diff --git a/version7.go b/version7.go index ba9dd5e..9d9db41 100644 --- a/version7.go +++ b/version7.go @@ -20,6 +20,7 @@ import ( // NewV7 returns a Version 7 UUID based on the current time(Unix Epoch). // Uses the randomness pool if it was enabled with EnableRandPool. // On error, NewV7 returns Nil and an error +// Note: this implement only has 12 bit seq, maximum of 4096 uuids are generated in 1 milliseconds func NewV7() (UUID, error) { uuid, err := NewRandom() if err != nil { @@ -44,7 +45,7 @@ func NewV7FromReader(r io.Reader) (UUID, error) { // makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6]) // uuid[8] already has the right version number (Variant is 10) -// see function NewV7 and NewV7FromReader +// see function NewV7 and NewV7FromReader func makeV7(uuid []byte) { /* 0 1 2 3 @@ -52,7 +53,7 @@ func makeV7(uuid []byte) { +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | unix_ts_ms | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | unix_ts_ms | ver | rand_a | + | unix_ts_ms | ver |rand_a (12 bit counter)| +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |var| rand_b | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -61,7 +62,7 @@ func makeV7(uuid []byte) { */ _ = uuid[15] // bounds check - t := timeNow().UnixMilli() + t, s := getTimeV7() uuid[0] = byte(t >> 40) uuid[1] = byte(t >> 32) @@ -70,6 +71,23 @@ func makeV7(uuid []byte) { uuid[4] = byte(t >> 8) uuid[5] = byte(t) - uuid[6] = 0x70 | (uuid[6] & 0x0F) - // uuid[8] has already has right version + uuid[6] = 0x70 | (0x0F & byte(s>>8)) + uuid[7] = byte(s) +} + +func getTimeV7() (int64, uint16) { + + defer timeMu.Unlock() + timeMu.Lock() + + if clockSeq == 0 { + setClockSequence(-1) + } + now := timeNow().UnixMilli() + + if now <= lasttimev7 { + clockSeq = clockSeq + 1 + } + lasttimev7 = now + return now, clockSeq } From 654bc6c0d0f45007617661302b0a9dbd2b30915c Mon Sep 17 00:00:00 2001 From: it512 Date: Wed, 10 Jan 2024 15:12:34 +0800 Subject: [PATCH 2/7] fix Monotonicity --- time.go | 4 ---- uuid_test.go | 23 +++++++---------------- version7.go | 23 +++-------------------- 3 files changed, 10 insertions(+), 40 deletions(-) diff --git a/time.go b/time.go index b3d77ef..5e24d3f 100644 --- a/time.go +++ b/time.go @@ -27,8 +27,6 @@ var ( lasttime uint64 // last time we returned clockSeq uint16 // clock sequence for this run - lasttimev7 int64 // for uuid v7 - timeNow = time.Now // for testing ) @@ -89,7 +87,6 @@ func clockSequence() int { } // SetClockSequence sets the clock sequence to the lower 14 bits of seq -// uuid v1 and v6 use 14 bits seq. uuid v7 use 12 bits seq. // Setting to -1 causes a new sequence to be generated. func SetClockSequence(seq int) { defer timeMu.Unlock() @@ -107,7 +104,6 @@ func setClockSequence(seq int) { clockSeq = uint16(seq&0x3fff) | 0x8000 // Set our variant if oldSeq != clockSeq { lasttime = 0 - lasttimev7 = 0 } } diff --git a/uuid_test.go b/uuid_test.go index f1b9cb7..3ea01f0 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -874,8 +874,7 @@ func TestVersion7_pooled(t *testing.T) { func TestVersion7FromReader(t *testing.T) { myString := "8059ddhdle77cb52" r := bytes.NewReader([]byte(myString)) - r2 := bytes.NewReader([]byte(myString)) - uuid1, err := NewV7FromReader(r) + _, err := NewV7FromReader(r) if err != nil { t.Errorf("failed generating UUID from a reader") } @@ -883,30 +882,22 @@ func TestVersion7FromReader(t *testing.T) { if err == nil { t.Errorf("expecting an error as reader has no more bytes. Got uuid. NewV7FromReader may not be using the provided reader") } - uuid3, err := NewV7FromReader(r2) - if err != nil { - t.Errorf("failed generating UUID from a reader") - } - if uuid1 == uuid3 { // Montonicity - t.Errorf("expected duplicates, got %q and %q", uuid1, uuid3) - } } func TestVersion7Monotonicity(t *testing.T) { - length := 4097 // [0x000 - 0xfff] - myString := "8059ddhdle77cb52" - - SetClockSequence(0) + length := 1000 uuids := make([]string, length) for i := 0; i < length; i++ { - uuidString, _ := NewV7FromReader(bytes.NewReader([]byte(myString))) + uuidString, _ := NewV7() uuids[i] = uuidString.String() + time.Sleep(time.Millisecond) + //time.Sleep(time.Millisecond / 50) } for i := 1; i < len(uuids); i++ { - if uuids[i-1] > uuids[i] { - t.Errorf("expected seq got %s > %s", uuids[i-1], uuids[i]) + if uuids[i-1] >= uuids[i] { + t.Errorf("unexpected seq got %s >= %s", uuids[i-1], uuids[i]) } } } diff --git a/version7.go b/version7.go index 9d9db41..f23e267 100644 --- a/version7.go +++ b/version7.go @@ -20,7 +20,6 @@ import ( // NewV7 returns a Version 7 UUID based on the current time(Unix Epoch). // Uses the randomness pool if it was enabled with EnableRandPool. // On error, NewV7 returns Nil and an error -// Note: this implement only has 12 bit seq, maximum of 4096 uuids are generated in 1 milliseconds func NewV7() (UUID, error) { uuid, err := NewRandom() if err != nil { @@ -53,7 +52,7 @@ func makeV7(uuid []byte) { +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ | unix_ts_ms | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | unix_ts_ms | ver |rand_a (12 bit counter)| + | unix_ts_ms | ver | rand_a (12 bit seq) | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ |var| rand_b | +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ @@ -62,7 +61,8 @@ func makeV7(uuid []byte) { */ _ = uuid[15] // bounds check - t, s := getTimeV7() + now := timeNow().UnixMicro() + t, s := now/1000, now&4095 // 2^12-1, 12 bits uuid[0] = byte(t >> 40) uuid[1] = byte(t >> 32) @@ -74,20 +74,3 @@ func makeV7(uuid []byte) { uuid[6] = 0x70 | (0x0F & byte(s>>8)) uuid[7] = byte(s) } - -func getTimeV7() (int64, uint16) { - - defer timeMu.Unlock() - timeMu.Lock() - - if clockSeq == 0 { - setClockSequence(-1) - } - now := timeNow().UnixMilli() - - if now <= lasttimev7 { - clockSeq = clockSeq + 1 - } - lasttimev7 = now - return now, clockSeq -} From 5986a710402ab566aba698c77a605e4b4d9e080d Mon Sep 17 00:00:00 2001 From: it512 Date: Wed, 10 Jan 2024 15:20:42 +0800 Subject: [PATCH 3/7] fix comment --- time.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time.go b/time.go index 5e24d3f..48b962b 100644 --- a/time.go +++ b/time.go @@ -67,12 +67,12 @@ func getTime() (Time, uint16, error) { } // ClockSequence returns the current clock sequence, generating one if not -// already set. The clock sequence is used for Version 1 and 7 UUIDs. +// already set. The clock sequence is only used for Version 1 UUIDs. // // The uuid package does not use global static storage for the clock sequence or // the last time a UUID was generated. Unless SetClockSequence is used, a new // random clock sequence is generated the first time a clock sequence is -// requested by ClockSequence, GetTime, NewV7 or NewUUID. (section 4.2.1.1) +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) func ClockSequence() int { defer timeMu.Unlock() timeMu.Lock() From 4b1aab9e4c0e801b9743069464e7fe1fbd5da161 Mon Sep 17 00:00:00 2001 From: it512 Date: Thu, 11 Jan 2024 11:20:35 +0800 Subject: [PATCH 4/7] Monotonicity 2 --- uuid_test.go | 19 +++++++------------ version7.go | 29 +++++++++++++++++++++++++++-- 2 files changed, 34 insertions(+), 14 deletions(-) diff --git a/uuid_test.go b/uuid_test.go index 3ea01f0..21118cf 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -885,19 +885,14 @@ func TestVersion7FromReader(t *testing.T) { } func TestVersion7Monotonicity(t *testing.T) { - length := 1000 - - uuids := make([]string, length) + length := 10000 + u1 := Must(NewV7()).String() for i := 0; i < length; i++ { - uuidString, _ := NewV7() - uuids[i] = uuidString.String() - time.Sleep(time.Millisecond) - //time.Sleep(time.Millisecond / 50) - } - - for i := 1; i < len(uuids); i++ { - if uuids[i-1] >= uuids[i] { - t.Errorf("unexpected seq got %s >= %s", uuids[i-1], uuids[i]) + u2 := Must(NewV7()).String() + if u2 <= u1 { + t.Errorf("monotonicity failed at #%d: %s(next) < %s(before)", i, u2, u1) + break } + u1 = u2 } } diff --git a/version7.go b/version7.go index f23e267..4120803 100644 --- a/version7.go +++ b/version7.go @@ -61,8 +61,7 @@ func makeV7(uuid []byte) { */ _ = uuid[15] // bounds check - now := timeNow().UnixMicro() - t, s := now/1000, now&4095 // 2^12-1, 12 bits + t, s := getV7Time() uuid[0] = byte(t >> 40) uuid[1] = byte(t >> 32) @@ -74,3 +73,29 @@ func makeV7(uuid []byte) { uuid[6] = 0x70 | (0x0F & byte(s>>8)) uuid[7] = byte(s) } + +// lastV7time is the last last time we returned stored as: +// +// 52 bits of time in milliseconds since epoch +// 12 bits of (fractional nanoseconds) >> 8 +var lastV7time int64 + +const nanoPerMilli = 1000000 + +// getV7Time returns the time in milliseconds and nanoseconds / 256. +// The returned (milli << 12 + seq) is guarenteed to be greater than +// (milli << 12 + seq) returned by any previous call to getV7Time. +func getV7Time() (milli, seq int64) { + nano := timeNow().UnixNano() + milli = nano / nanoPerMilli + // Sequence number is between 0 and 3906 (nanoPerMilli>>8) + seq = (nano - milli*nanoPerMilli) >> 8 + now := milli<<12 + seq + if now <= lastV7time { + now = lastV7time + 1 + milli = now >> 12 + seq = now & 0xfff + } + lastV7time = now + return milli, seq +} From 314e206fa27a67ba1a4306425516a29211a6a6db Mon Sep 17 00:00:00 2001 From: it512 Date: Thu, 11 Jan 2024 17:24:03 +0800 Subject: [PATCH 5/7] lock --- version7.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/version7.go b/version7.go index 4120803..fbc902a 100644 --- a/version7.go +++ b/version7.go @@ -86,6 +86,9 @@ const nanoPerMilli = 1000000 // The returned (milli << 12 + seq) is guarenteed to be greater than // (milli << 12 + seq) returned by any previous call to getV7Time. func getV7Time() (milli, seq int64) { + timeMu.Lock() + defer timeMu.Unlock() + nano := timeNow().UnixNano() milli = nano / nanoPerMilli // Sequence number is between 0 and 3906 (nanoPerMilli>>8) From 8888abce7d46c8b1eae389b0b4706df47dccf792 Mon Sep 17 00:00:00 2001 From: it512 Date: Fri, 12 Jan 2024 01:11:48 +0800 Subject: [PATCH 6/7] fix comment --- time.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time.go b/time.go index 48b962b..01dd404 100644 --- a/time.go +++ b/time.go @@ -86,8 +86,8 @@ func clockSequence() int { return int(clockSeq & 0x3fff) } -// SetClockSequence sets the clock sequence to the lower 14 bits of seq -// Setting to -1 causes a new sequence to be generated. +// SetClockSequence sets the clock sequence to the lower 14 bits of seq. Setting to +// -1 causes a new sequence to be generated. func SetClockSequence(seq int) { defer timeMu.Unlock() timeMu.Lock() From a6e034a86dec6a81916abb04548bb133aca31bc8 Mon Sep 17 00:00:00 2001 From: it512 Date: Fri, 12 Jan 2024 01:35:52 +0800 Subject: [PATCH 7/7] fix comment --- time.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/time.go b/time.go index 01dd404..c351129 100644 --- a/time.go +++ b/time.go @@ -67,12 +67,12 @@ func getTime() (Time, uint16, error) { } // ClockSequence returns the current clock sequence, generating one if not -// already set. The clock sequence is only used for Version 1 UUIDs. +// already set. The clock sequence is only used for Version 1 UUIDs. // // The uuid package does not use global static storage for the clock sequence or // the last time a UUID was generated. Unless SetClockSequence is used, a new // random clock sequence is generated the first time a clock sequence is -// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) +// requested by ClockSequence, GetTime, or NewUUID. (section 4.2.1.1) func ClockSequence() int { defer timeMu.Unlock() timeMu.Lock()