diff --git a/generator.go b/generator.go index d742967..3f1bd48 100644 --- a/generator.go +++ b/generator.go @@ -490,10 +490,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 { + 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) + } + } } g.lastTime = timeNow diff --git a/generator_test.go b/generator_test.go index 7949950..d013330 100644 --- a/generator_test.go +++ b/generator_test.go @@ -55,6 +55,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) { @@ -274,6 +275,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 ", 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 ", 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 ", 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 ", 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 ", 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 @@ -1132,6 +1247,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 ", 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 ", 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 ", 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 ", err) + } + } + }) } type faultyReader struct { diff --git a/race_v1_test.go b/race_v1_test.go new file mode 100644 index 0000000..5a2366c --- /dev/null +++ b/race_v1_test.go @@ -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++ + } 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) + 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++ + } 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) + } +}