Skip to content

Commit 8345c9a

Browse files
authored
Updated V7 generator to Draft04. (#112)
* Updated V7 generator to Draft04. * comment fixes * extend test coverage for failing new rand call * update readme * fix more comments
1 parent 7b40032 commit 8345c9a

File tree

3 files changed

+118
-71
lines changed

3 files changed

+118
-71
lines changed

README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ This package supports the following UUID versions:
1717
* Version 5, based on SHA-1 hashing of a named value (RFC-4122)
1818

1919
This package also supports experimental Universally Unique Identifier implementations based on a
20-
[draft RFC](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03) that updates RFC-4122
20+
[draft RFC](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html) that updates RFC-4122
2121
* Version 6, a k-sortable id based on timestamp, and field-compatible with v1 (draft-peabody-dispatch-new-uuid-format, RFC-4122)
2222
* Version 7, a k-sortable id based on timestamp (draft-peabody-dispatch-new-uuid-format, RFC-4122)
2323

@@ -114,4 +114,4 @@ func main() {
114114

115115
* [RFC-4122](https://tools.ietf.org/html/rfc4122)
116116
* [DCE 1.1: Authentication and Security Services](http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01)
117-
* [New UUID Formats RFC Draft (Peabody) Rev 03](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03)
117+
* [New UUID Formats RFC Draft (Peabody) Rev 04](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#)

generator.go

Lines changed: 46 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ func NewV6() (UUID, error) {
8181
}
8282

8383
// NewV7 returns a k-sortable UUID based on the current millisecond precision
84-
// UNIX epoch and 74 bits of pseudorandom data.
84+
// UNIX epoch and 74 bits of pseudorandom data. It supports single-node batch generation (multiple UUIDs in the same timestamp) with a Monotonic Random counter.
8585
//
86-
// This is implemented based on revision 03 of the Peabody UUID draft, and may
86+
// This is implemented based on revision 04 of the Peabody UUID draft, and may
8787
// be subject to change pending further revisions. Until the final specification
8888
// revision is finished, changes required to implement updates to the spec will
8989
// not be considered a breaking change. They will happen as a minor version
@@ -226,7 +226,7 @@ func WithRandomReader(reader io.Reader) GenOption {
226226
func (g *Gen) NewV1() (UUID, error) {
227227
u := UUID{}
228228

229-
timeNow, clockSeq, err := g.getClockSequence()
229+
timeNow, clockSeq, err := g.getClockSequence(false)
230230
if err != nil {
231231
return Nil, err
232232
}
@@ -293,7 +293,7 @@ func (g *Gen) NewV6() (UUID, error) {
293293
return Nil, err
294294
}
295295

296-
timeNow, clockSeq, err := g.getClockSequence()
296+
timeNow, clockSeq, err := g.getClockSequence(false)
297297
if err != nil {
298298
return Nil, err
299299
}
@@ -309,8 +309,12 @@ func (g *Gen) NewV6() (UUID, error) {
309309
return u, nil
310310
}
311311

312-
// getClockSequence returns the epoch and clock sequence for V1 and V6 UUIDs.
313-
func (g *Gen) getClockSequence() (uint64, uint16, error) {
312+
// getClockSequence returns the epoch and clock sequence for V1,V6 and V7 UUIDs.
313+
//
314+
// When useUnixTSMs is false, it uses the Coordinated Universal Time (UTC) as a count of 100-
315+
//
316+
// nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of Gregorian reform to the Christian calendar).
317+
func (g *Gen) getClockSequence(useUnixTSMs bool) (uint64, uint16, error) {
314318
var err error
315319
g.clockSequenceOnce.Do(func() {
316320
buf := make([]byte, 2)
@@ -326,7 +330,12 @@ func (g *Gen) getClockSequence() (uint64, uint16, error) {
326330
g.storageMutex.Lock()
327331
defer g.storageMutex.Unlock()
328332

329-
timeNow := g.getEpoch()
333+
var timeNow uint64
334+
if useUnixTSMs {
335+
timeNow = uint64(g.epochFunc().UnixMilli())
336+
} else {
337+
timeNow = g.getEpoch()
338+
}
330339
// Clock didn't change since last UUID generation.
331340
// Should increase clock sequence.
332341
if timeNow <= g.lastTime {
@@ -340,28 +349,51 @@ func (g *Gen) getClockSequence() (uint64, uint16, error) {
340349
// NewV7 returns a k-sortable UUID based on the current millisecond precision
341350
// UNIX epoch and 74 bits of pseudorandom data.
342351
//
343-
// This is implemented based on revision 03 of the Peabody UUID draft, and may
352+
// This is implemented based on revision 04 of the Peabody UUID draft, and may
344353
// be subject to change pending further revisions. Until the final specification
345354
// revision is finished, changes required to implement updates to the spec will
346355
// not be considered a breaking change. They will happen as a minor version
347356
// releases until the spec is final.
348357
func (g *Gen) NewV7() (UUID, error) {
349358
var u UUID
350-
351-
if _, err := io.ReadFull(g.rand, u[6:]); err != nil {
359+
/* https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
360+
0 1 2 3
361+
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
362+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
363+
| unix_ts_ms |
364+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
365+
| unix_ts_ms | ver | rand_a |
366+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
367+
|var| rand_b |
368+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
369+
| rand_b |
370+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */
371+
372+
ms, clockSeq, err := g.getClockSequence(true)
373+
if err != nil {
352374
return Nil, err
353375
}
354-
355-
tn := g.epochFunc()
356-
ms := uint64(tn.Unix())*1e3 + uint64(tn.Nanosecond())/1e6
357-
u[0] = byte(ms >> 40)
376+
//UUIDv7 features a 48 bit timestamp. First 32bit (4bytes) represents seconds since 1970, followed by 2 bytes for the ms granularity.
377+
u[0] = byte(ms >> 40) //1-6 bytes: big-endian unsigned number of Unix epoch timestamp
358378
u[1] = byte(ms >> 32)
359379
u[2] = byte(ms >> 24)
360380
u[3] = byte(ms >> 16)
361381
u[4] = byte(ms >> 8)
362382
u[5] = byte(ms)
363383

384+
//support batching by using a monotonic pseudo-random sequence
385+
//The 6th byte contains the version and partially rand_a data.
386+
//We will lose the most significant bites from the clockSeq (with SetVersion), but it is ok, we need the least significant that contains the counter to ensure the monotonic property
387+
binary.BigEndian.PutUint16(u[6:8], clockSeq) // set rand_a with clock seq which is random and monotonic
388+
389+
//override first 4bits of u[6].
364390
u.SetVersion(V7)
391+
392+
//set rand_b 64bits of pseudo-random bits (first 2 will be overridden)
393+
if _, err = io.ReadFull(g.rand, u[8:16]); err != nil {
394+
return Nil, err
395+
}
396+
//override first 2 bits of byte[8] for the variant
365397
u.SetVariant(VariantRFC4122)
366398

367399
return u, nil

generator_test.go

Lines changed: 70 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -603,6 +603,7 @@ func testNewV6KSortable(t *testing.T) {
603603

604604
func testNewV7(t *testing.T) {
605605
t.Run("Basic", makeTestNewV7Basic())
606+
t.Run("TestVector", makeTestNewV7TestVector())
606607
t.Run("Basic10000000", makeTestNewV7Basic10000000())
607608
t.Run("DifferentAcrossCalls", makeTestNewV7DifferentAcrossCalls())
608609
t.Run("StaleEpoch", makeTestNewV7StaleEpoch())
@@ -611,6 +612,7 @@ func testNewV7(t *testing.T) {
611612
t.Run("FaultyRandWithOptions", makeTestNewV7FaultyRandWithOptions())
612613
t.Run("ShortRandomRead", makeTestNewV7ShortRandomRead())
613614
t.Run("KSortable", makeTestNewV7KSortable())
615+
t.Run("ClockSequence", makeTestNewV7ClockSequence())
614616
}
615617

616618
func makeTestNewV7Basic() func(t *testing.T) {
@@ -628,6 +630,37 @@ func makeTestNewV7Basic() func(t *testing.T) {
628630
}
629631
}
630632

633+
// makeTestNewV7TestVector as defined in Draft04
634+
func makeTestNewV7TestVector() func(t *testing.T) {
635+
return func(t *testing.T) {
636+
pRand := make([]byte, 10)
637+
//first 2 bytes will be read by clockSeq. First 4 bits will be overridden by Version. The next bits should be 0xCC3(3267)
638+
binary.LittleEndian.PutUint16(pRand[:2], uint16(0xCC3))
639+
//8bytes will be read for rand_b. First 2 bits will be overridden by Variant
640+
binary.LittleEndian.PutUint64(pRand[2:], uint64(0x18C4DC0C0C07398F))
641+
642+
g := &Gen{
643+
epochFunc: func() time.Time {
644+
return time.UnixMilli(1645557742000)
645+
},
646+
rand: bytes.NewReader(pRand),
647+
}
648+
u, err := g.NewV7()
649+
if err != nil {
650+
t.Fatal(err)
651+
}
652+
if got, want := u.Version(), V7; got != want {
653+
t.Errorf("got version %d, want %d", got, want)
654+
}
655+
if got, want := u.Variant(), VariantRFC4122; got != want {
656+
t.Errorf("got variant %d, want %d", got, want)
657+
}
658+
if got, want := u.String()[:15], "017f22e2-79b0-7"; got != want {
659+
t.Errorf("got version %q, want %q", got, want)
660+
}
661+
}
662+
}
663+
631664
func makeTestNewV7Basic10000000() func(t *testing.T) {
632665
return func(t *testing.T) {
633666
if testing.Short() {
@@ -717,12 +750,23 @@ func makeTestNewV7FaultyRand() func(t *testing.T) {
717750
g := &Gen{
718751
epochFunc: time.Now,
719752
rand: &faultyReader{
720-
readToFail: 0, // fail immediately
753+
readToFail: 0,
721754
},
722755
}
723756
u, err := g.NewV7()
724757
if err == nil {
725-
t.Errorf("got %v, nil error", u)
758+
t.Errorf("got %v, nil error for clockSequence", u)
759+
}
760+
761+
g = &Gen{
762+
epochFunc: time.Now,
763+
rand: &faultyReader{
764+
readToFail: 1,
765+
},
766+
}
767+
u, err = g.NewV7()
768+
if err == nil {
769+
t.Errorf("got %v, nil error rand_b", u)
726770
}
727771
}
728772
}
@@ -787,61 +831,32 @@ func makeTestNewV7KSortable() func(t *testing.T) {
787831
}
788832
}
789833

790-
func testNewV7ClockSequence(t *testing.T) {
791-
if testing.Short() {
792-
t.Skip("skipping test in short mode.")
793-
}
794-
795-
g := NewGen()
796-
797-
// hack to try and reduce race conditions based on when the test starts
798-
nsec := time.Now().Nanosecond()
799-
sleepDur := int(time.Second) - nsec
800-
time.Sleep(time.Duration(sleepDur))
801-
802-
u1, err := g.NewV7()
803-
if err != nil {
804-
t.Fatalf("failed to generate V7 UUID #1: %v", err)
805-
}
806-
807-
u2, err := g.NewV7()
808-
if err != nil {
809-
t.Fatalf("failed to generate V7 UUID #2: %v", err)
810-
}
811-
812-
time.Sleep(time.Millisecond)
813-
814-
u3, err := g.NewV7()
815-
if err != nil {
816-
t.Fatalf("failed to generate V7 UUID #3: %v", err)
817-
}
818-
819-
time.Sleep(time.Second)
820-
821-
u4, err := g.NewV7()
822-
if err != nil {
823-
t.Fatalf("failed to generate V7 UUID #3: %v", err)
824-
}
825-
826-
s1 := binary.BigEndian.Uint16(u1[6:8]) & 0xfff
827-
s2 := binary.BigEndian.Uint16(u2[6:8]) & 0xfff
828-
s3 := binary.BigEndian.Uint16(u3[6:8]) & 0xfff
829-
s4 := binary.BigEndian.Uint16(u4[6:8]) & 0xfff
830-
831-
if s1 != 0 {
832-
t.Errorf("sequence 1 should be zero, was %d", s1)
833-
}
834-
835-
if s2 != s1+1 {
836-
t.Errorf("sequence 2 expected to be one above sequence 1; seq 1: %d, seq 2: %d", s1, s2)
837-
}
834+
func makeTestNewV7ClockSequence() func(t *testing.T) {
835+
return func(t *testing.T) {
836+
if testing.Short() {
837+
t.Skip("skipping test in short mode.")
838+
}
838839

839-
if s3 != 0 {
840-
t.Errorf("sequence 3 should be zero, was %d", s3)
841-
}
840+
g := NewGen()
841+
//always return the same TS
842+
g.epochFunc = func() time.Time {
843+
return time.UnixMilli(1645557742000)
844+
}
845+
//by being KSortable with the same timestamp, it means the sequence is Not empty, and it is monotonic
846+
uuids := make([]UUID, 10)
847+
for i := range uuids {
848+
u, err := g.NewV7()
849+
testErrCheck(t, "NewV7()", "", err)
850+
uuids[i] = u
851+
}
842852

843-
if s4 != 0 {
844-
t.Errorf("sequence 4 should be zero, was %d", s4)
853+
for i := 1; i < len(uuids); i++ {
854+
p, n := uuids[i-1], uuids[i]
855+
isLess := p.String() < n.String()
856+
if !isLess {
857+
t.Errorf("uuids[%d] (%s) not less than uuids[%d] (%s)", i-1, p, i, n)
858+
}
859+
}
845860
}
846861
}
847862

0 commit comments

Comments
 (0)