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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -26,6 +26,7 @@ import ( | |
"crypto/rand" | ||
"crypto/sha1" | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"hash" | ||
"io" | ||
|
@@ -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 | ||
|
@@ -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 | ||
|
@@ -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) | ||
|
||
u.SetVersion(V6) | ||
u.SetVariant(VariantRFC4122) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @cameracker |
||
|
||
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() { | ||
|
@@ -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) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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. There was a problem hiding this comment. Choose a reason for hiding this commentThe 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):
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 | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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 theBigEndian
binary encoder to put theuint16
starting atu[8]
. So it writes both bytes out, covering the hi and lo bits.There was a problem hiding this comment.
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!