diff --git a/generator.go b/generator.go index 4facd3a..7e250c1 100644 --- a/generator.go +++ b/generator.go @@ -28,6 +28,7 @@ import ( "encoding/binary" "io" "net" + "runtime" "sync" "time" ) @@ -434,7 +435,27 @@ func (g *Gen) getClockSequence(useUnixTSMs bool, atTime time.Time) (uint64, uint // 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 { + if useUnixTSMs { + timeNow = uint64(g.epochFunc().UnixMilli()) + } else { + timeNow = g.getEpoch(g.epochFunc()) + } + if timeNow > g.lastTime { + break + } + // Yield the processor briefly to avoid busy-waiting. + runtime.Gosched() + } + } } g.lastTime = timeNow diff --git a/race_v1_test.go b/race_v1_test.go new file mode 100644 index 0000000..32db22d --- /dev/null +++ b/race_v1_test.go @@ -0,0 +1,70 @@ +package uuid + +import ( + "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) + } + }) + } +}