Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

update UUIDv7 implementation with RFC Draft Rev 03 spec #99

Merged
merged 2 commits into from
Sep 9, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ This package supports the following UUID versions:
* Version 5, based on SHA-1 hashing of a named value (RFC-4122)

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

The v6 and v7 IDs are **not** considered a part of the stable API, and may be subject to behavior or API changes as part of minor releases
to this package. They will be updated as the draft RFC changes, and will become stable if and when the draft RFC is accepted.
Expand Down Expand Up @@ -114,4 +114,4 @@ func main() {

* [RFC-4122](https://tools.ietf.org/html/rfc4122)
* [DCE 1.1: Authentication and Security Services](http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01)
* [New UUID Formats RFC Draft (Peabody) Rev 02](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-02)
* [New UUID Formats RFC Draft (Peabody) Rev 03](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03)
263 changes: 22 additions & 241 deletions generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ import (
"crypto/rand"
"crypto/sha1"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
Expand Down Expand Up @@ -71,7 +70,7 @@ func NewV5(ns UUID, name string) UUID {
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
// order being adjusted to allow the UUID to be k-sortable.
//
// This is implemented based on revision 02 of the Peabody UUID draft, and may
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
Expand All @@ -80,22 +79,16 @@ func NewV6() (UUID, error) {
return DefaultGenerator.NewV6()
}

// NewV7 returns a k-sortable UUID based on the current UNIX epoch, with the
// ability to configure the timestamp's precision from millisecond all the way
// to nanosecond. The additional precision is supported by reducing the amount
// of pseudorandom data that makes up the rest of the UUID.
// NewV7 returns a k-sortable UUID based on the current millisecond precision
// UNIX epoch and 74 bits of pseudorandom data.
//
// If an unknown Precision argument is passed to this method it will panic. As
// such it's strongly encouraged to use the package-provided constants for this
// value.
//
// This is implemented based on revision 02 of the Peabody UUID draft, and may
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func NewV7(p Precision) (UUID, error) {
return DefaultGenerator.NewV7(p)
func NewV7() (UUID, error) {
return DefaultGenerator.NewV7()
}

// Generator provides an interface for generating UUIDs.
Expand All @@ -105,7 +98,7 @@ type Generator interface {
NewV4() (UUID, error)
NewV5(ns UUID, name string) UUID
NewV6() (UUID, error)
NewV7(Precision) (UUID, error)
NewV7() (UUID, error)
}

// Gen is a reference UUID generator based on the specifications laid out in
Expand All @@ -131,10 +124,6 @@ type Gen struct {
lastTime uint64
clockSequence uint16
hardwareAddr [6]byte

v7LastTime uint64
v7LastSubsec uint64
v7ClockSequence uint16
}

// interface check -- build will fail if *Gen doesn't satisfy Generator
Expand Down Expand Up @@ -224,7 +213,7 @@ func (g *Gen) NewV5(ns UUID, name string) UUID {
// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit
// order being adjusted to allow the UUID to be k-sortable.
//
// This is implemented based on revision 02 of the Peabody UUID draft, and may
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
Expand Down Expand Up @@ -280,244 +269,36 @@ func (g *Gen) getClockSequence() (uint64, uint16, error) {
return timeNow, g.clockSequence, nil
}

// Precision is used to configure the V7 generator, to specify how precise the
// timestamp within the UUID should be.
type Precision byte

const (
NanosecondPrecision Precision = iota
MicrosecondPrecision
MillisecondPrecision
)

func (p Precision) String() string {
switch p {
case NanosecondPrecision:
return "nanosecond"

case MicrosecondPrecision:
return "microsecond"

case MillisecondPrecision:
return "millisecond"

default:
return "unknown"
}
}

// Duration returns the time.Duration for a specific precision. If the Precision
// value is not known, this returns 0.
func (p Precision) Duration() time.Duration {
switch p {
case NanosecondPrecision:
return time.Nanosecond

case MicrosecondPrecision:
return time.Microsecond

case MillisecondPrecision:
return time.Millisecond

default:
return 0
}
}

// NewV7 returns a k-sortable UUID based on the current UNIX epoch, with the
// ability to configure the timestamp's precision from millisecond all the way
// to nanosecond. The additional precision is supported by reducing the amount
// of pseudorandom data that makes up the rest of the UUID.
// NewV7 returns a k-sortable UUID based on the current millisecond precision
// UNIX epoch and 74 bits of pseudorandom data.
//
// If an unknown Precision argument is passed to this method it will panic. As
// such it's strongly encouraged to use the package-provided constants for this
// value.
//
// This is implemented based on revision 02 of the Peabody UUID draft, and may
// This is implemented based on revision 03 of the Peabody UUID draft, and may
// be subject to change pending further revisions. Until the final specification
// revision is finished, changes required to implement updates to the spec will
// not be considered a breaking change. They will happen as a minor version
// releases until the spec is final.
func (g *Gen) NewV7(p Precision) (UUID, error) {
func (g *Gen) NewV7() (UUID, error) {
var u UUID
var err error

switch p {
case NanosecondPrecision:
u, err = g.newV7Nano()

case MicrosecondPrecision:
u, err = g.newV7Micro()

case MillisecondPrecision:
u, err = g.newV7Milli()

default:
panic(fmt.Sprintf("unknown precision value %d", p))
}

if err != nil {
if _, err := io.ReadFull(g.rand, u[6:]); err != nil {
return Nil, err
}

tn := g.epochFunc()
ms := uint64(tn.UnixMilli())
u[0] = byte(ms >> 40)
u[1] = byte(ms >> 32)
u[2] = byte(ms >> 24)
u[3] = byte(ms >> 16)
u[4] = byte(ms >> 8)
u[5] = byte(ms)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think we benefit any from using the binary.BigEndian.PutUint32 interface that we use for the v6? As written makes sense given the lack of a PutUint48 to cover the size of the time in milliseconds..

Copy link
Contributor Author

@convto convto Sep 9, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As you commented, I wrote this code because PutUint48 is not supported. v6 may have similar conditions as far as the specification is concerned.

However, there is a slight difference in v6.
The v6 specification specifies 32bit time_high , 16bit time_mid, 16bit time_low_and_version separately, which may have the effect of expressing the layout more explicitly.

(v7 is specified in the specification as a single field, unix_ts_ms, so I have taken the approach described in the code.)


u.SetVersion(V7)
u.SetVariant(VariantRFC4122)

return u, nil
}

func (g *Gen) newV7Milli() (UUID, error) {
var u UUID

if _, err := io.ReadFull(g.rand, u[8:]); err != nil {
return Nil, err
}

sec, nano, seq, err := g.getV7ClockSequence(MillisecondPrecision)
if err != nil {
return Nil, err
}

msec := (nano / 1000000) & 0xfff

d := (sec << 28) // set unixts field
d |= (msec << 16) // set msec field
d |= (uint64(seq) & 0xfff) // set seq field

binary.BigEndian.PutUint64(u[:], d)

return u, nil
}

func (g *Gen) newV7Micro() (UUID, error) {
var u UUID

if _, err := io.ReadFull(g.rand, u[10:]); err != nil {
return Nil, err
}

sec, nano, seq, err := g.getV7ClockSequence(MicrosecondPrecision)
if err != nil {
return Nil, err
}

usec := nano / 1000
usech := (usec << 4) & 0xfff0000
usecl := usec & 0xfff

d := (sec << 28) // set unixts field
d |= usech | usecl // set usec fields

binary.BigEndian.PutUint64(u[:], d)
binary.BigEndian.PutUint16(u[8:], seq)

return u, nil
}

func (g *Gen) newV7Nano() (UUID, error) {
var u UUID

if _, err := io.ReadFull(g.rand, u[11:]); err != nil {
return Nil, err
}

sec, nano, seq, err := g.getV7ClockSequence(NanosecondPrecision)
if err != nil {
return Nil, err
}

nano &= 0x3fffffffff
nanoh := nano >> 26
nanom := (nano >> 14) & 0xfff
nanol := uint16(nano & 0x3fff)

d := (sec << 28) // set unixts field
d |= (nanoh << 16) | nanom // set nsec high and med fields

binary.BigEndian.PutUint64(u[:], d)
binary.BigEndian.PutUint16(u[8:], nanol) // set nsec low field

u[10] = byte(seq) // set seq field

return u, nil
}

const (
maxSeq14 = (1 << 14) - 1
maxSeq12 = (1 << 12) - 1
maxSeq8 = (1 << 8) - 1
)

// getV7ClockSequence returns the unix epoch, nanoseconds of current second, and
// the sequence for V7 UUIDs.
func (g *Gen) getV7ClockSequence(p Precision) (epoch uint64, nano uint64, seq uint16, err error) {
g.storageMutex.Lock()
defer g.storageMutex.Unlock()

tn := g.epochFunc()
unix := uint64(tn.Unix())
nsec := uint64(tn.Nanosecond())

// V7 UUIDs have more precise requirements around how the clock sequence
// value is generated and used. Specifically they require that the sequence
// be zero, unless we've already generated a UUID within this unit of time
// (millisecond, microsecond, or nanosecond) at which point you should
// increment the sequence. Likewise if time has warped backwards for some reason (NTP
// adjustment?), we also increment the clock sequence to reduce the risk of a
// collision.
switch {
case unix < g.v7LastTime:
g.v7ClockSequence++

case unix > g.v7LastTime:
g.v7ClockSequence = 0

case unix == g.v7LastTime:
switch p {
case NanosecondPrecision:
if nsec <= g.v7LastSubsec {
if g.v7ClockSequence >= maxSeq8 {
return 0, 0, 0, errors.New("generating nanosecond precision UUIDv7s too fast: internal clock sequence would roll over")
}

g.v7ClockSequence++
} else {
g.v7ClockSequence = 0
}

case MicrosecondPrecision:
if nsec/1000 <= g.v7LastSubsec/1000 {
if g.v7ClockSequence >= maxSeq14 {
return 0, 0, 0, errors.New("generating microsecond precision UUIDv7s too fast: internal clock sequence would roll over")
}

g.v7ClockSequence++
} else {
g.v7ClockSequence = 0
}

case MillisecondPrecision:
if nsec/1000000 <= g.v7LastSubsec/1000000 {
if g.v7ClockSequence >= maxSeq12 {
return 0, 0, 0, errors.New("generating millisecond precision UUIDv7s too fast: internal clock sequence would roll over")
}

g.v7ClockSequence++
} else {
g.v7ClockSequence = 0
}

default:
panic(fmt.Sprintf("unknown precision value %d", p))
}
}

g.v7LastTime = unix
g.v7LastSubsec = nsec

return unix, nsec, g.v7ClockSequence, nil
}

// Returns the hardware address.
func (g *Gen) getHardwareAddr() ([]byte, error) {
var err error
Expand Down