From e826b847f82a77680dc1cd6102ccf2a6422190df Mon Sep 17 00:00:00 2001 From: MK Date: Sun, 11 Aug 2024 13:54:34 -0400 Subject: [PATCH] Add "AtTime" generators for V1, V6, and V7 (#142) * 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 --- generator.go | 78 +++++++++++++++++++------ generator_test.go | 142 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 18 deletions(-) diff --git a/generator.go b/generator.go index 7bfe50c..02cba76 100644 --- a/generator.go +++ b/generator.go @@ -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) @@ -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 @@ -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 } @@ -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 @@ -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 } @@ -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 @@ -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 } @@ -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) @@ -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. @@ -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. diff --git a/generator_test.go b/generator_test.go index 8d046e7..2f48668 100644 --- a/generator_test.go +++ b/generator_test.go @@ -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) { @@ -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 @@ -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) { @@ -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()) @@ -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) { @@ -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