Skip to content

Commit ed3abe0

Browse files
committed
[FIX] - Prevent duplicates on clock sequence wrap;
1 parent 9feab51 commit ed3abe0

2 files changed

Lines changed: 46 additions & 9 deletions

File tree

generator.go

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -431,6 +431,21 @@ func (g *Gen) getClockSequence(useUnixTSMs bool, atTime time.Time) (uint64, uint
431431
} else {
432432
timeNow = g.getEpoch(atTime)
433433
}
434+
now := func() uint64 {
435+
epoch := g.epochFunc()
436+
if useUnixTSMs {
437+
return uint64(epoch.UnixMilli())
438+
}
439+
440+
return g.getEpoch(epoch)
441+
}
442+
443+
// Calls can arrive with stale atTime values (captured before acquiring the
444+
// lock). Clamp backwards timestamps to the latest emitted one to avoid
445+
// reusing older timestamp + clock-sequence pairs after sequence wrap.
446+
if timeNow < g.lastTime {
447+
timeNow = g.lastTime
448+
}
434449
// Clock didn't change since last UUID generation.
435450
// Should increase clock sequence.
436451
if timeNow <= g.lastTime {
@@ -442,15 +457,7 @@ func (g *Gen) getClockSequence(useUnixTSMs bool, atTime time.Time) (uint64, uint
442457
// If the sequence wrapped (back to zero) we MUST wait for the
443458
// timestamp to advance to preserve uniqueness (see RFC-9562 §6.1).
444459
if g.clockSequence == 0 {
445-
for {
446-
if useUnixTSMs {
447-
timeNow = uint64(g.epochFunc().UnixMilli())
448-
} else {
449-
timeNow = g.getEpoch(g.epochFunc())
450-
}
451-
if timeNow > g.lastTime {
452-
break
453-
}
460+
for ; timeNow <= g.lastTime; timeNow = now() {
454461
// Sleep briefly to avoid busy-waiting and reduce CPU usage.
455462
time.Sleep(time.Microsecond)
456463
}

generator_test.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func testNewV1(t *testing.T) {
5454
t.Run("MissingNetworkFaultyRand", testNewV1MissingNetworkFaultyRand)
5555
t.Run("MissingNetworkFaultyRandWithOptions", testNewV1MissingNetworkFaultyRandWithOptions)
5656
t.Run("AtSpecificTime", testNewV1AtTime)
57+
t.Run("AtSpecificTimeClockSequenceWrap", testNewV1AtTimeClockSequenceWrap)
5758
}
5859

5960
func TestNewGenWithHWAF(t *testing.T) {
@@ -273,6 +274,35 @@ func testNewV1AtTime(t *testing.T) {
273274
}
274275
}
275276

277+
func testNewV1AtTimeClockSequenceWrap(t *testing.T) {
278+
atTime := time.Unix(0, 1000000)
279+
280+
g := NewGenWithOptions(
281+
WithHWAddrFunc(func() (net.HardwareAddr, error) {
282+
return net.HardwareAddr{0, 1, 2, 3, 4, 5}, nil
283+
}),
284+
WithEpochFunc(func() time.Time {
285+
return time.Unix(0, 2000000)
286+
}),
287+
WithRandomReader(bytes.NewReader([]byte{0x00, 0x00})),
288+
)
289+
290+
const total = 0x3fff + 3
291+
seen := make(map[UUID]int, total)
292+
293+
for i := 0; i < total; i++ {
294+
u, err := g.NewV1AtTime(atTime)
295+
if err != nil {
296+
t.Fatalf("g.NewV1AtTime() err = %v, want <nil>", err)
297+
}
298+
299+
if prev, ok := seen[u]; ok {
300+
t.Fatalf("duplicate UUID at iteration %d (previous %d): %s", i, prev, u)
301+
}
302+
seen[u] = i
303+
}
304+
}
305+
276306
func testNewV1FaultyRandWithOptions(t *testing.T) {
277307
g := NewGenWithOptions(WithRandomReader(&faultyReader{
278308
readToFail: 0, // fail immediately

0 commit comments

Comments
 (0)