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

initial implementation of UUIDv6 and UUIDv7 based on RFC Draft Rev 2 #93

Merged
merged 1 commit into from Oct 16, 2021
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
1 change: 1 addition & 0 deletions README.md
Expand Up @@ -106,3 +106,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)
310 changes: 309 additions & 1 deletion generator.go
Expand Up @@ -26,6 +26,7 @@ import (
"crypto/rand"
"crypto/sha1"
"encoding/binary"
"errors"
"fmt"
"hash"
"io"
Expand Down Expand Up @@ -66,12 +67,45 @@ func NewV5(ns UUID, name string) UUID {
return DefaultGenerator.NewV5(ns, name)
}

// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of
// 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
// 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 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.
//
// 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
// 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)
}

// Generator provides an interface for generating UUIDs.
type Generator interface {
NewV1() (UUID, error)
NewV3(ns UUID, name string) UUID
NewV4() (UUID, error)
NewV5(ns UUID, name string) UUID
NewV6() (UUID, error)
NewV7(Precision) (UUID, error)
}

// Gen is a reference UUID generator based on the specifications laid out in
Expand All @@ -97,6 +131,10 @@ 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 @@ -182,7 +220,39 @@ func (g *Gen) NewV5(ns UUID, name string) UUID {
return u
}

// getClockSequence returns the epoch and clock sequence.
// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of
// 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
// 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) NewV6() (UUID, error) {
var u UUID

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

timeNow, clockSeq, err := g.getClockSequence()
if err != nil {
return Nil, err
}

binary.BigEndian.PutUint32(u[0:], uint32(timeNow>>28)) // set time_high
binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>12)) // set time_mid
binary.BigEndian.PutUint16(u[6:], uint16(timeNow&0xfff)) // set time_low (minus four version bits)
binary.BigEndian.PutUint16(u[8:], clockSeq&0x3fff) // set clk_seq_hi_res (minus two variant bits)

Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need to set u[9:] to clk_sec_low_res

Copy link
Member Author

Choose a reason for hiding this comment

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

This does that implicitly. uint16 is represented as two bytes, so we use the BigEndian binary encoder to put the uint16 starting at u[8]. So it writes both bytes out, covering the hi and lo bits.

Copy link
Contributor

Choose a reason for hiding this comment

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

I've approved the PR, but maybe a comment to that effect would be nice!

u.SetVersion(V6)
u.SetVariant(VariantRFC4122)
Copy link
Contributor

Choose a reason for hiding this comment

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

is this variant correct? It's looking for the value, but can't remember of go has iotas 0 indexed (which would make this 1) or if this is indeed the right value.

Copy link
Member Author

Choose a reason for hiding this comment

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

@cameracker SetVariant does the translation.


return u, nil
}

// getClockSequence returns the epoch and clock sequence for V1 and V6 UUIDs.
func (g *Gen) getClockSequence() (uint64, uint16, error) {
var err error
g.clockSequenceOnce.Do(func() {
Expand Down Expand Up @@ -210,6 +280,244 @@ 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.
//
// 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
// 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) {
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 {
return Nil, err
}

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)
Copy link
Contributor

Choose a reason for hiding this comment

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

is grouping it this way going to result in the msec and seq bits being stored BigEndian? That may be a deviation from the spec, because only the nano bits are stored as big endian (at least according to my reading).

Note, this concern applies to all the v7 generators.

Copy link
Member Author

Choose a reason for hiding this comment

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

The original spec (RFC 4122) specifies that values should be in network byte order (Big Endian):

The fields are encoded as 16 octets, with the sizes and order of the fields defined above, and with each field encoded with the Most Significant Byte first (known as network byte order). Note that the field names, particularly for multiplexed fields, follow historical practice.

This draft doesn't make any changes to that.


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