Skip to content

Commit

Permalink
fix: Monotonicity in UUIDv7 (#150)
Browse files Browse the repository at this point in the history
* Monotonicity in UUIDv7

* fix Monotonicity

* fix comment

* Monotonicity 2

* lock

* fix comment

* fix comment
  • Loading branch information
it512 committed Jan 11, 2024
1 parent c58770e commit a2b2b32
Show file tree
Hide file tree
Showing 2 changed files with 48 additions and 14 deletions.
23 changes: 14 additions & 9 deletions uuid_test.go
Expand Up @@ -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)
Expand Down Expand Up @@ -874,20 +874,25 @@ 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")
}
_, err = NewV7FromReader(r)
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 {
t.Errorf("expected duplicates, got %q and %q", uuid1, uuid3)
}

func TestVersion7Monotonicity(t *testing.T) {
length := 10000
u1 := Must(NewV7()).String()
for i := 0; i < length; i++ {
u2 := Must(NewV7()).String()
if u2 <= u1 {
t.Errorf("monotonicity failed at #%d: %s(next) < %s(before)", i, u2, u1)
break
}
u1 = u2
}
}
39 changes: 34 additions & 5 deletions version7.go
Expand Up @@ -44,15 +44,15 @@ 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
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a |
| unix_ts_ms | ver | rand_a (12 bit seq) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
Expand All @@ -61,7 +61,7 @@ func makeV7(uuid []byte) {
*/
_ = uuid[15] // bounds check

t := timeNow().UnixMilli()
t, s := getV7Time()

uuid[0] = byte(t >> 40)
uuid[1] = byte(t >> 32)
Expand All @@ -70,6 +70,35 @@ 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)
}

// 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) {
timeMu.Lock()
defer timeMu.Unlock()

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
}

0 comments on commit a2b2b32

Please sign in to comment.