Skip to content
Open
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 28 additions & 1 deletion generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -431,10 +431,37 @@ func (g *Gen) getClockSequence(useUnixTSMs bool, atTime time.Time) (uint64, uint
} else {
timeNow = g.getEpoch(atTime)
}
now := func() uint64 {
epoch := g.epochFunc()
if useUnixTSMs {
return uint64(epoch.UnixMilli())
}

return g.getEpoch(epoch)
}

// Calls can arrive with stale atTime values (captured before acquiring the
// lock). Clamp backwards timestamps to the latest emitted one to avoid
// reusing older timestamp + clock-sequence pairs after sequence wrap.
if timeNow < g.lastTime {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

could move this inside of the if timeNow <= g.lastTime block below since that check will always also be true. eliminating the branch might also make it slightly faster.

if timeNow <= g.lastTime {
    // Calls can arrive with stale atTime values (captured before acquiring the
    // lock). Clamp backwards timestamps to the latest emitted one to avoid
    // reusing older timestamp + clock-sequence pairs after sequence wrap.
    timeNow = g.lastTime
    ...
}

timeNow = g.lastTime
}
// Clock didn't change since last UUID generation.
// Should increase clock sequence.
if timeNow <= g.lastTime {
g.clockSequence++
// Increment the 14-bit clock sequence (RFC-9562 §6.1).
// Only the lower 14 bits are encoded in the UUID; the upper two
// bits are overridden by the Variant in SetVariant().
g.clockSequence = (g.clockSequence + 1) & 0x3fff

// If the sequence wrapped (back to zero) we MUST wait for the
// timestamp to advance to preserve uniqueness (see RFC-9562 §6.1).
if g.clockSequence == 0 {
for ; timeNow <= g.lastTime; timeNow = now() {
// Sleep briefly to avoid busy-waiting and reduce CPU usage.
time.Sleep(time.Microsecond)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

worst case this has a repeating wait of 10 microseconds.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Yep, that makes sense!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I found some references that say that the minimum OS timer resolution on Linux can be 50+ microseconds and as much as 15ms on WIndows. That could make this loop VERY slow. 😬

This fixes the clock sequence overflow issue, but what does it do to the benchmarks for V1, V6, and V7 values? Since we want to wait a tiny amount of time this seems like a good place for runtime.Gosched() (so we only yield the current time slice on the scheduler).

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@dylan-bourque I'm not sure this solution will give us much of a gain. This loop with time.Sleep(time.Microsecond) is called only when the clockSequence wraps (once every 16,384 UUIDs with the same timestamp), not on every NewV1/NewV6/NewV7.

I compared Sleep vs. runtime.Gosched() locally on the BenchmarkGenerator/NewV1|NewV6|NewV7 benchmarks (3 runs each, Apple M3 Pro).

Here are the benchmarks:

NewV1: Sleep ~44-45 ns/op, Gosched ~45 ns/op
NewV6: Sleep ~119 ns/op, Gosched ~119-120 ns/op
NewV7: Sleep ~145 ns/op, Gosched ~145 ns/op

}
}
}
g.lastTime = timeNow

Expand Down
173 changes: 173 additions & 0 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ func testNewV1(t *testing.T) {
t.Run("MissingNetworkFaultyRand", testNewV1MissingNetworkFaultyRand)
t.Run("MissingNetworkFaultyRandWithOptions", testNewV1MissingNetworkFaultyRandWithOptions)
t.Run("AtSpecificTime", testNewV1AtTime)
t.Run("AtSpecificTimeClockSequenceWrap", testNewV1AtTimeClockSequenceWrap)
}

func TestNewGenWithHWAF(t *testing.T) {
Expand Down Expand Up @@ -273,6 +274,120 @@ func testNewV1AtTime(t *testing.T) {
}
}

func testNewV1AtTimeClockSequenceWrap(t *testing.T) {
atTime := time.Unix(0, 1000000)

g := NewGenWithOptions(
WithHWAddrFunc(func() (net.HardwareAddr, error) {
return net.HardwareAddr{0, 1, 2, 3, 4, 5}, nil
}),
WithEpochFunc(func() time.Time {
return time.Unix(0, 2000000)
}),
WithRandomReader(bytes.NewReader([]byte{0x00, 0x00})),
)

const total = 0x3fff + 3
seen := make(map[UUID]int, total)

for i := 0; i < total; i++ {
u, err := g.NewV1AtTime(atTime)
if err != nil {
t.Fatalf("g.NewV1AtTime() err = %v, want <nil>", err)
}

if prev, ok := seen[u]; ok {
t.Fatalf("duplicate UUID at iteration %d (previous %d): %s", i, prev, u)
}
seen[u] = i
}
}

func TestGetClockSequence(t *testing.T) {
t.Run("WrapUsesFreshEpoch", testGetClockSequenceWrapUsesFreshEpoch)
t.Run("WrapUsesFreshUnixTSMs", testGetClockSequenceWrapUsesFreshUnixTSMs)
}

func testGetClockSequenceWrapUsesFreshEpoch(t *testing.T) {
atTime := time.Unix(0, 1000000)
advancedTime := time.Unix(0, 2000000)
epochCalls := 0

g := NewGenWithOptions(
WithEpochFunc(func() time.Time {
epochCalls++

return advancedTime
}),
WithRandomReader(bytes.NewReader([]byte{0x3f, 0xff})),
)

firstTime, firstSeq, err := g.getClockSequence(false, atTime)
if err != nil {
t.Fatalf("g.getClockSequence(false, atTime) err = %v, want <nil>", err)
}
if got, want := firstSeq, uint16(0x3fff); got != want {
t.Fatalf("clock sequence = %d, want %d", got, want)
}
if got, want := firstTime, g.getEpoch(atTime); got != want {
t.Fatalf("time = %d, want %d", got, want)
}

secondTime, secondSeq, err := g.getClockSequence(false, atTime)
if err != nil {
t.Fatalf("g.getClockSequence(false, atTime) err = %v, want <nil>", err)
}
if got, want := secondSeq, uint16(0); got != want {
t.Fatalf("clock sequence = %d, want %d", got, want)
}
if got, want := secondTime, g.getEpoch(advancedTime); got != want {
t.Fatalf("time = %d, want %d", got, want)
}
if epochCalls == 0 {
t.Fatal("expected epochFunc() to be called when sequence wraps")
}
}

func testGetClockSequenceWrapUsesFreshUnixTSMs(t *testing.T) {
atTime := time.UnixMilli(1000)
advancedTime := time.UnixMilli(2000)
epochCalls := 0

g := NewGenWithOptions(
WithEpochFunc(func() time.Time {
epochCalls++

return advancedTime
}),
WithRandomReader(bytes.NewReader([]byte{0x3f, 0xff})),
)

firstTime, firstSeq, err := g.getClockSequence(true, atTime)
if err != nil {
t.Fatalf("g.getClockSequence(true, atTime) err = %v, want <nil>", err)
}
if got, want := firstSeq, uint16(0x3fff); got != want {
t.Fatalf("clock sequence = %d, want %d", got, want)
}
if got, want := firstTime, uint64(atTime.UnixMilli()); got != want {
t.Fatalf("time = %d, want %d", got, want)
}

secondTime, secondSeq, err := g.getClockSequence(true, atTime)
if err != nil {
t.Fatalf("g.getClockSequence(true, atTime) err = %v, want <nil>", err)
}
if got, want := secondSeq, uint16(0); got != want {
t.Fatalf("clock sequence = %d, want %d", got, want)
}
if got, want := secondTime, uint64(advancedTime.UnixMilli()); got != want {
t.Fatalf("time = %d, want %d", got, want)
}
if epochCalls == 0 {
t.Fatal("expected epochFunc() to be called when sequence wraps")
}
}

func testNewV1FaultyRandWithOptions(t *testing.T) {
g := NewGenWithOptions(WithRandomReader(&faultyReader{
readToFail: 0, // fail immediately
Expand Down Expand Up @@ -1131,6 +1246,64 @@ func BenchmarkGenerator(b *testing.B) {
NewV7()
}
})
b.Run("ClockSequenceWrapUTC", func(b *testing.B) {
atTime := time.Unix(0, 1000000)
advancedTime := time.Unix(0, 2000000)

g := NewGenWithOptions(
WithEpochFunc(func() time.Time {
return advancedTime
}),
WithRandomReader(bytes.NewReader([]byte{0x00, 0x00})),
)
_, _, err := g.getClockSequence(false, atTime)
if err != nil {
b.Fatalf("g.getClockSequence(false, atTime) err = %v, want <nil>", err)
}
staleTime := g.getEpoch(atTime)

b.ResetTimer()
for i := 0; i < b.N; i++ {
g.storageMutex.Lock()
g.lastTime = staleTime
g.clockSequence = 0x3fff
g.storageMutex.Unlock()

_, _, err = g.getClockSequence(false, atTime)
if err != nil {
b.Fatalf("g.getClockSequence(false, atTime) err = %v, want <nil>", err)
}
}
})
b.Run("ClockSequenceWrapUnixTSMs", func(b *testing.B) {
atTime := time.UnixMilli(1000)
advancedTime := time.UnixMilli(2000)

g := NewGenWithOptions(
WithEpochFunc(func() time.Time {
return advancedTime
}),
WithRandomReader(bytes.NewReader([]byte{0x00, 0x00})),
)
_, _, err := g.getClockSequence(true, atTime)
if err != nil {
b.Fatalf("g.getClockSequence(true, atTime) err = %v, want <nil>", err)
}
staleTime := uint64(atTime.UnixMilli())

b.ResetTimer()
for i := 0; i < b.N; i++ {
g.storageMutex.Lock()
g.lastTime = staleTime
g.clockSequence = 0x3fff
g.storageMutex.Unlock()

_, _, err = g.getClockSequence(true, atTime)
if err != nil {
b.Fatalf("g.getClockSequence(true, atTime) err = %v, want <nil>", err)
}
}
})
}

type faultyReader struct {
Expand Down
125 changes: 125 additions & 0 deletions race_v1_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
package uuid

import (
"os"
"sync"
"sync/atomic"
"testing"
)

// TestV1UniqueConcurrent verifies that Version-1 UUID generation remains
// collision-free under various levels of concurrent load. The test uses
// table-driven subtests to progressively increase the number of goroutines
// and UUIDs generated. We intentionally let the timestamp advance (default
// NewGen) to keep the test quick while still exercising the new
// clock-sequence logic under contention.
func TestV1UniqueConcurrent(t *testing.T) {
cases := []struct {
name string
goroutines int
uuidsPerGor int
}{
{"small", 20, 600}, // 12 000 UUIDs (baseline)
{"medium", 100, 1000}, // 100 000 UUIDs (original failure case)
{"large", 200, 1000}, // 200 000 UUIDs (high contention)
}

for _, tc := range cases {
tc := tc // capture range variable
t.Run(tc.name, func(t *testing.T) {
gen := NewGen()

var (
wg sync.WaitGroup
mu sync.Mutex
seen = make(map[UUID]struct{}, tc.goroutines*tc.uuidsPerGor)
dupCount uint32
genErr uint32
)

for i := 0; i < tc.goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < tc.uuidsPerGor; j++ {
u, err := gen.NewV1()
if err != nil {
atomic.AddUint32(&genErr, 1)
return
}
mu.Lock()
if _, exists := seen[u]; exists {
dupCount++
Comment thread
cameracker marked this conversation as resolved.
} else {
seen[u] = struct{}{}
}
mu.Unlock()
}
}()
}

wg.Wait()

if genErr > 0 {
t.Fatalf("%d errors occurred during UUID generation", genErr)
}
if dupCount > 0 {
t.Fatalf("duplicate UUIDs detected: %d", dupCount)
}
})
}
}

// TestV1UniqueConcurrentStress runs a heavier contention scenario that mirrors
// reported real-world duplication checks (2000 goroutines x 1000 UUIDs).
// It is opt-in to keep default CI runs fast.
func TestV1UniqueConcurrentStress(t *testing.T) {
if os.Getenv("UUID_STRESS_V1") != "1" {
t.Skip("set UUID_STRESS_V1=1 to run this stress test")
}

gen := NewGen()

const (
goroutines = 2000
uuidsPerGor = 1000
)

var (
wg sync.WaitGroup
mu sync.Mutex
seen = make(map[UUID]struct{}, goroutines*uuidsPerGor)
Copy link
Copy Markdown
Member

@dylan-bourque dylan-bourque Mar 16, 2026

Choose a reason for hiding this comment

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

nit: in my testing, I made this map[UUID]int32 so that the failure output could report the actual duplicate values and how many of each were generated.

mu.Lock()
if cnt, exists := seen[u]; exists {
    seen[u] = cnt + 1
} else {
    seen[u] = 1
}
mu.Unlock()

and

for v, n := range seen {
    if n > 1 {
        t.Errorf("duplicate V1 UUID: %s appeared %d time(s)", v, n)
    }
}

definitely not necessary, but it makes the failure output more useful

dupCount uint32
genErr uint32
)

for i := 0; i < goroutines; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for j := 0; j < uuidsPerGor; j++ {
u, err := gen.NewV1()
if err != nil {
atomic.AddUint32(&genErr, 1)
return
}
mu.Lock()
if _, exists := seen[u]; exists {
dupCount++
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nit: should make this atomic.AddUint32() as well, if only for consistency and to avoid someone coming along later and asking why it isn't.

} else {
seen[u] = struct{}{}
}
mu.Unlock()
}
}()
}

wg.Wait()

if genErr > 0 {
t.Fatalf("%d errors occurred during UUID generation", genErr)
}
if dupCount > 0 {
t.Fatalf("duplicate UUIDs detected: %d", dupCount)
}
}
Loading