Skip to content

Commit

Permalink
Add "AtTime" generators for V1, V6, and V7 (#142)
Browse files Browse the repository at this point in the history
* add "AtTime" generators for V1, V6, and V7

* doc: update doc strings

* fix: convenience methods

* test: add tests for AtTime methods

---------

Co-authored-by: Cameron Ackerman <[email protected]>
  • Loading branch information
kohenkatz and cameracker authored Aug 11, 2024
1 parent 190948b commit e826b84
Show file tree
Hide file tree
Showing 2 changed files with 202 additions and 18 deletions.
78 changes: 60 additions & 18 deletions generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ func NewV1() (UUID, error) {
return DefaultGenerator.NewV1()
}

// NewV1 returns a UUID based on the provided timestamp and MAC address.
func NewV1AtTime(atTime time.Time) (UUID, error) {
return DefaultGenerator.NewV1AtTime(atTime)
}

// NewV3 returns a UUID based on the MD5 hash of the namespace UUID and name.
func NewV3(ns UUID, name string) UUID {
return DefaultGenerator.NewV3(ns, name)
Expand All @@ -66,27 +71,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
// NewV6 returns a k-sortable UUID based on the current 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.
func NewV6() (UUID, error) {
return DefaultGenerator.NewV6()
}

// NewV7 returns a k-sortable UUID based on the current millisecond precision
// 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.
// NewV6 returns a k-sortable UUID based on the provided 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.
func NewV6AtTime(atTime time.Time) (UUID, error) {
return DefaultGenerator.NewV6AtTime(atTime)
}

// NewV7 returns a k-sortable UUID based on the current millisecond-precision
// 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.
func NewV7() (UUID, error) {
return DefaultGenerator.NewV7()
}

// NewV7 returns a k-sortable UUID based on the provided millisecond-precision
// 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.
func NewV7AtTime(atTime time.Time) (UUID, error) {
return DefaultGenerator.NewV7AtTime(atTime)
}

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

// Gen is a reference UUID generator based on the specifications laid out in
Expand Down Expand Up @@ -211,9 +234,14 @@ func WithRandomReader(reader io.Reader) GenOption {

// NewV1 returns a UUID based on the current timestamp and MAC address.
func (g *Gen) NewV1() (UUID, error) {
return g.NewV1AtTime(g.epochFunc())
}

// NewV1AtTime returns a UUID based on the provided timestamp and current MAC address.
func (g *Gen) NewV1AtTime(atTime time.Time) (UUID, error) {
u := UUID{}

timeNow, clockSeq, err := g.getClockSequence(false)
timeNow, clockSeq, err := g.getClockSequence(false, atTime)
if err != nil {
return Nil, err
}
Expand Down Expand Up @@ -264,10 +292,17 @@ func (g *Gen) NewV5(ns UUID, name string) UUID {
return u
}

// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of
// NewV6 returns a k-sortable UUID based on the current 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.
func (g *Gen) NewV6() (UUID, error) {
return g.NewV6AtTime(g.epochFunc())
}

// NewV6 returns a k-sortable UUID based on the provided 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.
func (g *Gen) NewV6AtTime(atTime time.Time) (UUID, error) {
/* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-6
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
Expand All @@ -282,7 +317,7 @@ func (g *Gen) NewV6() (UUID, error) {
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */
var u UUID

timeNow, _, err := g.getClockSequence(false)
timeNow, _, err := g.getClockSequence(false, atTime)
if err != nil {
return Nil, err
}
Expand All @@ -306,9 +341,15 @@ func (g *Gen) NewV6() (UUID, error) {
return u, nil
}

// NewV7 returns a k-sortable UUID based on the current millisecond precision
// NewV7 returns a k-sortable UUID based on the current millisecond-precision
// UNIX epoch and 74 bits of pseudorandom data.
func (g *Gen) NewV7() (UUID, error) {
return g.NewV7AtTime(g.epochFunc())
}

// NewV7 returns a k-sortable UUID based on the provided millisecond-precision
// UNIX epoch and 74 bits of pseudorandom data.
func (g *Gen) NewV7AtTime(atTime time.Time) (UUID, error) {
var u UUID
/* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7
0 1 2 3
Expand All @@ -323,7 +364,7 @@ func (g *Gen) NewV7() (UUID, error) {
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */

ms, clockSeq, err := g.getClockSequence(true)
ms, clockSeq, err := g.getClockSequence(true, atTime)
if err != nil {
return Nil, err
}
Expand Down Expand Up @@ -355,12 +396,13 @@ func (g *Gen) NewV7() (UUID, error) {
return u, nil
}

// getClockSequence returns the epoch and clock sequence for V1,V6 and V7 UUIDs.
//
// When useUnixTSMs is false, it uses the Coordinated Universal Time (UTC) as a count of 100-
// getClockSequence returns the epoch and clock sequence of the provided time,
// used for generating V1,V6 and V7 UUIDs.
//
// nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of Gregorian reform to the Christian calendar).
func (g *Gen) getClockSequence(useUnixTSMs bool) (uint64, uint16, error) {
// When useUnixTSMs is false, it uses the Coordinated Universal Time (UTC) as a count of
// 100-nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of Gregorian
// reform to the Christian calendar).
func (g *Gen) getClockSequence(useUnixTSMs bool, atTime time.Time) (uint64, uint16, error) {
var err error
g.clockSequenceOnce.Do(func() {
buf := make([]byte, 2)
Expand All @@ -378,9 +420,9 @@ func (g *Gen) getClockSequence(useUnixTSMs bool) (uint64, uint16, error) {

var timeNow uint64
if useUnixTSMs {
timeNow = uint64(g.epochFunc().UnixMilli())
timeNow = uint64(atTime.UnixMilli())
} else {
timeNow = g.getEpoch()
timeNow = g.getEpoch(atTime)
}
// Clock didn't change since last UUID generation.
// Should increase clock sequence.
Expand Down Expand Up @@ -417,9 +459,9 @@ func (g *Gen) getHardwareAddr() ([]byte, error) {
}

// Returns the difference between UUID epoch (October 15, 1582)
// and current time in 100-nanosecond intervals.
func (g *Gen) getEpoch() uint64 {
return epochStart + uint64(g.epochFunc().UnixNano()/100)
// and the provided time in 100-nanosecond intervals.
func (g *Gen) getEpoch(atTime time.Time) uint64 {
return epochStart + uint64(atTime.UnixNano()/100)
}

// Returns the UUID based on the hashing of the namespace UUID and name.
Expand Down
142 changes: 142 additions & 0 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ func testNewV1(t *testing.T) {
t.Run("MissingNetworkWithOptions", testNewV1MissingNetworkWithOptions)
t.Run("MissingNetworkFaultyRand", testNewV1MissingNetworkFaultyRand)
t.Run("MissingNetworkFaultyRandWithOptions", testNewV1MissingNetworkFaultyRandWithOptions)
t.Run("AtSpecificTime", testNewV1AtTime)
}

func TestNewGenWithHWAF(t *testing.T) {
Expand Down Expand Up @@ -225,6 +226,53 @@ func testNewV1MissingNetworkFaultyRandWithOptions(t *testing.T) {
}
}

func testNewV1AtTime(t *testing.T) {
atTime := time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)

u1, err := NewV1AtTime(atTime)
if err != nil {
t.Fatal(err)
}

u2, err := NewV1AtTime(atTime)
if err != nil {
t.Fatal(err)
}

// Even with the same timestamp, there is still a monotonically increasing portion,
// so they should not be 100% identical. Bytes 0-7 and 10-16 should be identical.
u1Bytes := u1.Bytes()
u2Bytes := u2.Bytes()
binary.BigEndian.PutUint16(u1Bytes[8:], 0)
binary.BigEndian.PutUint16(u2Bytes[8:], 0)
if !bytes.Equal(u1Bytes, u2Bytes) {
t.Errorf("generated different UUIDs across calls with same timestamp: %v / %v", u1, u2)
}

ts1, err := TimestampFromV1(u1)
if err != nil {
t.Fatal(err)
}
time1, err := ts1.Time()
if err != nil {
t.Fatal(err)
}
if time1.Equal(atTime) {
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
}
ts2, err := TimestampFromV1(u2)
if err != nil {
t.Fatal(err)
}
time2, err := ts2.Time()
if err != nil {
t.Fatal(err)
}
if time2.Equal(atTime) {
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
}
}

func testNewV1FaultyRandWithOptions(t *testing.T) {
g := NewGenWithOptions(WithRandomReader(&faultyReader{
readToFail: 0, // fail immediately
Expand Down Expand Up @@ -423,6 +471,7 @@ func testNewV6(t *testing.T) {
t.Run("ShortRandomRead", testNewV6ShortRandomRead)
t.Run("ShortRandomReadWithOptions", testNewV6ShortRandomReadWithOptions)
t.Run("KSortable", testNewV6KSortable)
t.Run("AtSpecificTime", testNewV6AtTime)
}

func testNewV6Basic(t *testing.T) {
Expand Down Expand Up @@ -601,6 +650,51 @@ func testNewV6KSortable(t *testing.T) {
}
}

func testNewV6AtTime(t *testing.T) {
atTime := time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)

u1, err := NewV6AtTime(atTime)
if err != nil {
t.Fatal(err)
}

u2, err := NewV6AtTime(atTime)
if err != nil {
t.Fatal(err)
}

// Even with the same timestamp, there is still a random portion,
// so they should not be 100% identical. Bytes 0-8 are the timestamp so they should be identical.
u1Bytes := u1.Bytes()[:8]
u2Bytes := u2.Bytes()[:8]
if !bytes.Equal(u1Bytes, u2Bytes) {
t.Errorf("generated different UUIDs across calls with same timestamp: %v / %v", u1, u2)
}

ts1, err := TimestampFromV6(u1)
if err != nil {
t.Fatal(err)
}
time1, err := ts1.Time()
if err != nil {
t.Fatal(err)
}
if time1.Equal(atTime) {
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
}
ts2, err := TimestampFromV6(u2)
if err != nil {
t.Fatal(err)
}
time2, err := ts2.Time()
if err != nil {
t.Fatal(err)
}
if time2.Equal(atTime) {
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
}
}

func testNewV7(t *testing.T) {
t.Run("Basic", makeTestNewV7Basic())
t.Run("TestVector", makeTestNewV7TestVector())
Expand All @@ -614,6 +708,7 @@ func testNewV7(t *testing.T) {
t.Run("ShortRandomReadWithOptions", makeTestNewV7ShortRandomReadWithOptions())
t.Run("KSortable", makeTestNewV7KSortable())
t.Run("ClockSequence", makeTestNewV7ClockSequence())
t.Run("AtSpecificTime", makeTestNewV7AtTime())
}

func makeTestNewV7Basic() func(t *testing.T) {
Expand Down Expand Up @@ -861,6 +956,53 @@ func makeTestNewV7ClockSequence() func(t *testing.T) {
}
}

func makeTestNewV7AtTime() func(t *testing.T) {
return func(t *testing.T) {
atTime := time.Date(2020, 1, 2, 3, 4, 5, 6, time.UTC)

u1, err := NewV7AtTime(atTime)
if err != nil {
t.Fatal(err)
}

u2, err := NewV7AtTime(atTime)
if err != nil {
t.Fatal(err)
}

// Even with the same timestamp, there is still a random portion,
// so they should not be 100% identical. Bytes 0-6 are the timestamp so they should be identical.
u1Bytes := u1.Bytes()[:7]
u2Bytes := u2.Bytes()[:7]
if !bytes.Equal(u1Bytes, u2Bytes) {
t.Errorf("generated different UUIDs across calls with same timestamp: %v / %v", u1, u2)
}

ts1, err := TimestampFromV7(u1)
if err != nil {
t.Fatal(err)
}
time1, err := ts1.Time()
if err != nil {
t.Fatal(err)
}
if time1.Equal(atTime) {
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
}
ts2, err := TimestampFromV7(u2)
if err != nil {
t.Fatal(err)
}
time2, err := ts2.Time()
if err != nil {
t.Fatal(err)
}
if time2.Equal(atTime) {
t.Errorf("extracted time is incorrect: was %v, expected %v", time1, atTime)
}
}
}

func TestDefaultHWAddrFunc(t *testing.T) {
tests := []struct {
n string
Expand Down

0 comments on commit e826b84

Please sign in to comment.