Skip to content

Commit 37a2e41

Browse files
committed
Test UUIDv7 statistics behavior
Refs #70.
1 parent af90231 commit 37a2e41

3 files changed

Lines changed: 278 additions & 0 deletions

File tree

‎property-tests/UuidV7FactoryProperties.fs‎

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ open Xunit
88
open FsCheck
99
open FsCheck.Xunit
1010
open Clockworks
11+
open Clockworks.Instrumentation
1112

1213
type private DeterministicRandomNumberGenerator(seed: int) =
1314
inherit RandomNumberGenerator()
@@ -157,6 +158,47 @@ let ``UUIDs are unique`` (count: byte) =
157158

158159
uniqueCount = safeCount
159160

161+
/// Property: UUIDv7 statistics count every successfully generated UUID exactly once.
162+
[<Property(MaxTest = 50)>]
163+
let ``Statistics generated count matches successful UUID generation`` (count: uint16) =
164+
let safeCount = int (count % 512us) + 1
165+
let statistics = UuidV7FactoryStatistics()
166+
let timeProvider = new SimulatedTimeProvider()
167+
use factory =
168+
new UuidV7Factory(
169+
timeProvider,
170+
null,
171+
CounterOverflowBehavior.IncrementTimestamp,
172+
statistics)
173+
174+
statistics.Reset()
175+
176+
for _ in 1..safeCount do
177+
factory.NewGuid() |> ignore
178+
179+
statistics.Snapshot().GeneratedCount = int64 safeCount
180+
181+
/// Property: UUIDv7 statistics expose the maximum logical drift caused by wall-clock rollback.
182+
[<Property(MaxTest = 50)>]
183+
let ``Statistics max drift tracks wall clock rollback`` (rollbackMs: uint16) =
184+
let safeRollbackMs = int64 (rollbackMs % 1000us) + 1L
185+
let startMs = 1_700_000_000_000L
186+
let statistics = UuidV7FactoryStatistics()
187+
let timeProvider = SimulatedTimeProvider.FromUnixMs(startMs)
188+
use factory = new UuidV7Factory(timeProvider, null, CounterOverflowBehavior.SpinWait, statistics)
189+
190+
statistics.Reset()
191+
192+
factory.NewGuid() |> ignore
193+
timeProvider.SetUnixMs(startMs - safeRollbackMs)
194+
factory.NewGuid() |> ignore
195+
196+
let snapshot = statistics.Snapshot()
197+
snapshot.GeneratedCount = 2L
198+
&& snapshot.ClockRollbackCount = 1L
199+
&& snapshot.LogicalTimestampAdvanceCount = 1L
200+
&& snapshot.MaxLogicalDriftMs = safeRollbackMs
201+
160202
/// Property: UUIDs generated at different times are different
161203
[<Property(MaxTest = 50)>]
162204
let ``UUIDs change with time`` (advanceMs: uint16) =

‎tests/Clockworks.Tests.csproj‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</PropertyGroup>
77

88
<ItemGroup>
9+
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.1" />
910
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
1011
<PackageReference Include="xunit" Version="2.9.2" />
1112
<PackageReference Include="xunit.runner.visualstudio" Version="2.8.2" />
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
using Clockworks.Abstractions;
2+
using Clockworks.Instrumentation;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Xunit;
5+
6+
namespace Clockworks.Tests;
7+
8+
public sealed class UuidV7FactoryStatisticsTests
9+
{
10+
[Fact]
11+
public void Constructor_LeavesStatisticsDisabled_WhenStatisticsAreNotSupplied()
12+
{
13+
using var factory = new UuidV7Factory(SimulatedTimeProvider.FromUnixMs(1_700_000_000_000), null);
14+
15+
Assert.Null(factory.Statistics);
16+
}
17+
18+
[Fact]
19+
public void NewGuid_RecordsGeneratedCount_WhenStatisticsAreEnabled()
20+
{
21+
var statistics = new UuidV7FactoryStatistics();
22+
var time = SimulatedTimeProvider.FromUnixMs(1_700_000_000_000);
23+
using var rng = new DeterministicRandomNumberGenerator(seed: 123);
24+
using var factory = new UuidV7Factory(time, rng, CounterOverflowBehavior.SpinWait, statistics);
25+
26+
statistics.Reset();
27+
28+
_ = factory.NewGuid();
29+
_ = factory.NewGuidWithTimestamp();
30+
Span<Guid> batch = stackalloc Guid[7];
31+
factory.NewGuids(batch);
32+
33+
Assert.Same(statistics, factory.Statistics);
34+
Assert.Equal(9, statistics.GeneratedCount);
35+
Assert.Equal(9, statistics.Snapshot().GeneratedCount);
36+
}
37+
38+
[Fact]
39+
public void NewGuid_RecordsClockRollbackAndLogicalDrift_WhenPhysicalTimeMovesBackwards()
40+
{
41+
const long startMs = 1_700_000_000_000;
42+
var statistics = new UuidV7FactoryStatistics();
43+
var time = SimulatedTimeProvider.FromUnixMs(startMs);
44+
using var rng = new DeterministicRandomNumberGenerator(seed: 123);
45+
using var factory = new UuidV7Factory(time, rng, CounterOverflowBehavior.SpinWait, statistics);
46+
47+
statistics.Reset();
48+
49+
_ = factory.NewGuid();
50+
time.SetUnixMs(startMs - 25);
51+
_ = factory.NewGuid();
52+
53+
var snapshot = statistics.Snapshot();
54+
Assert.Equal(2, snapshot.GeneratedCount);
55+
Assert.Equal(1, snapshot.ClockRollbackCount);
56+
Assert.Equal(1, snapshot.LogicalTimestampAdvanceCount);
57+
Assert.Equal(25, snapshot.MaxLogicalDriftMs);
58+
}
59+
60+
[Fact]
61+
public void NewGuid_RecordsCounterOverflowAndLogicalAdvance_WhenOverflowIncrementsTimestamp()
62+
{
63+
var statistics = new UuidV7FactoryStatistics();
64+
var time = SimulatedTimeProvider.FromUnixMs(1_700_000_000_000);
65+
using var rng = new DeterministicRandomNumberGenerator(seed: 1);
66+
using var factory = new UuidV7Factory(
67+
time,
68+
rng,
69+
CounterOverflowBehavior.IncrementTimestamp,
70+
statistics);
71+
72+
statistics.Reset();
73+
74+
for (var i = 0; i < 5000; i++)
75+
_ = factory.NewGuid();
76+
77+
var snapshot = statistics.Snapshot();
78+
Assert.Equal(5000, snapshot.GeneratedCount);
79+
Assert.True(snapshot.CounterOverflowCount >= 1);
80+
Assert.True(snapshot.LogicalTimestampAdvanceCount >= 1);
81+
Assert.True(snapshot.MaxLogicalDriftMs >= 1);
82+
}
83+
84+
[Fact]
85+
public void NewGuids_RecordsRandomBufferRefills_WhenBatchConsumesThreadLocalBuffer()
86+
{
87+
var statistics = new UuidV7FactoryStatistics();
88+
var time = SimulatedTimeProvider.FromUnixMs(1_700_000_000_000);
89+
using var rng = new DeterministicRandomNumberGenerator(seed: 123);
90+
using var factory = new UuidV7Factory(time, rng, CounterOverflowBehavior.SpinWait, statistics);
91+
var batch = new Guid[64];
92+
93+
statistics.Reset();
94+
95+
factory.NewGuids(batch);
96+
97+
var snapshot = statistics.Snapshot();
98+
Assert.Equal(batch.Length, snapshot.GeneratedCount);
99+
Assert.True(snapshot.RandomBufferRefillCount >= 1);
100+
}
101+
102+
[Fact]
103+
public async Task NewGuid_RecordsSpinWait_WhenOverflowWaitsForNextMillisecond()
104+
{
105+
var statistics = new UuidV7FactoryStatistics();
106+
var time = SimulatedTimeProvider.FromUnixMs(1_700_000_000_000);
107+
using var rng = new DeterministicRandomNumberGenerator(seed: 1);
108+
using var factory = new UuidV7Factory(
109+
time,
110+
rng,
111+
CounterOverflowBehavior.SpinWait,
112+
statistics);
113+
114+
while (factory.NewGuid().GetCounter() != 0x0FFF)
115+
{
116+
}
117+
118+
statistics.Reset();
119+
120+
var pending = Task.Run(factory.NewGuid);
121+
122+
var observedSpin = SpinWait.SpinUntil(
123+
() => statistics.CounterOverflowCount > 0 && statistics.SpinWaitCount > 0,
124+
TimeSpan.FromSeconds(2));
125+
126+
try
127+
{
128+
Assert.True(observedSpin);
129+
130+
time.AdvanceMs(1);
131+
132+
await pending.WaitAsync(TimeSpan.FromSeconds(5));
133+
}
134+
finally
135+
{
136+
if (!pending.IsCompleted)
137+
{
138+
time.AdvanceMs(1);
139+
await pending.WaitAsync(TimeSpan.FromSeconds(5));
140+
}
141+
142+
pending.Dispose();
143+
}
144+
145+
var snapshot = statistics.Snapshot();
146+
Assert.Equal(1, snapshot.GeneratedCount);
147+
Assert.Equal(1, snapshot.CounterOverflowCount);
148+
Assert.Equal(1, snapshot.SpinWaitCount);
149+
}
150+
151+
[Fact]
152+
public async Task NewGuid_RecordsCasRetries_WhenConcurrentAllocationsRace()
153+
{
154+
const int participantCount = 8;
155+
var statistics = new UuidV7FactoryStatistics();
156+
var time = new BarrierTimeProvider(1_700_000_000_000);
157+
using var factory = new UuidV7Factory(
158+
time,
159+
rng: null,
160+
CounterOverflowBehavior.SpinWait,
161+
statistics);
162+
163+
statistics.Reset();
164+
time.Arm(participantCount);
165+
166+
var tasks = Enumerable.Range(0, participantCount)
167+
.Select(_ => Task.Run(factory.NewGuid))
168+
.ToArray();
169+
170+
await Task.WhenAll(tasks).WaitAsync(TimeSpan.FromSeconds(5));
171+
172+
var snapshot = statistics.Snapshot();
173+
Assert.Equal(participantCount, snapshot.GeneratedCount);
174+
Assert.True(snapshot.CasRetryCount >= participantCount - 1);
175+
Assert.Equal(participantCount, tasks.Select(static t => t.Result).Distinct().Count());
176+
}
177+
178+
[Fact]
179+
public void Reset_ClearsAllCounters()
180+
{
181+
var statistics = new UuidV7FactoryStatistics();
182+
var time = SimulatedTimeProvider.FromUnixMs(1_700_000_000_000);
183+
using var rng = new DeterministicRandomNumberGenerator(seed: 1);
184+
using var factory = new UuidV7Factory(time, rng, CounterOverflowBehavior.SpinWait, statistics);
185+
186+
_ = factory.NewGuid();
187+
statistics.Reset();
188+
189+
Assert.Equal(default, statistics.Snapshot());
190+
}
191+
192+
[Fact]
193+
public void AddLockFreeGuidFactory_RegistersSharedStatistics()
194+
{
195+
var services = new ServiceCollection();
196+
var statistics = new UuidV7FactoryStatistics();
197+
var time = SimulatedTimeProvider.FromUnixMs(1_700_000_000_000);
198+
using var rng = new DeterministicRandomNumberGenerator(seed: 1);
199+
200+
services.AddLockFreeGuidFactory(time, statistics, rng);
201+
202+
using var provider = services.BuildServiceProvider();
203+
var factory = provider.GetRequiredService<IUuidV7Factory>();
204+
205+
_ = factory.NewGuid();
206+
207+
Assert.Same(statistics, provider.GetRequiredService<UuidV7FactoryStatistics>());
208+
Assert.Equal(1, statistics.GeneratedCount);
209+
}
210+
211+
private sealed class BarrierTimeProvider(long unixMs) : TimeProvider
212+
{
213+
private DateTimeOffset _utcNow = DateTimeOffset.FromUnixTimeMilliseconds(unixMs);
214+
private Barrier? _barrier;
215+
private int _remainingParticipants;
216+
217+
public void Arm(int participantCount)
218+
{
219+
_remainingParticipants = participantCount;
220+
_barrier = new Barrier(participantCount);
221+
}
222+
223+
public override DateTimeOffset GetUtcNow()
224+
{
225+
var barrier = Volatile.Read(ref _barrier);
226+
if (barrier is not null && Interlocked.Decrement(ref _remainingParticipants) >= 0)
227+
{
228+
if (!barrier.SignalAndWait(TimeSpan.FromSeconds(5)))
229+
throw new TimeoutException("Timed out while coordinating UUIDv7 CAS contention test.");
230+
}
231+
232+
return _utcNow;
233+
}
234+
}
235+
}

0 commit comments

Comments
 (0)