Skip to content

Commit 0877589

Browse files
Copilotstephentoub
andcommitted
Refactor GuidPolyfills to GuidHelpers with simplified monotonic counter
Co-authored-by: stephentoub <2642209+stephentoub@users.noreply.github.com>
1 parent 676aecb commit 0877589

4 files changed

Lines changed: 82 additions & 98 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
using System.Threading;
2+
3+
namespace System;
4+
5+
/// <summary>
6+
/// Provides helper methods for GUID generation.
7+
/// </summary>
8+
internal static class GuidHelpers
9+
{
10+
private static long s_counter;
11+
12+
/// <summary>
13+
/// Creates a monotonically increasing UUID v7 GUID with the specified timestamp.
14+
/// Uses a globally increasing counter for strict monotonicity.
15+
/// </summary>
16+
/// <param name="timestamp">The timestamp to embed in the GUID.</param>
17+
/// <returns>A new monotonically increasing UUID v7 GUID.</returns>
18+
/// <remarks>
19+
/// <para>
20+
/// This method cannot be replaced with <c>Guid.CreateVersion7(DateTimeOffset)</c> because
21+
/// the built-in .NET implementation uses random bits for intra-millisecond uniqueness,
22+
/// which does not guarantee strict monotonicity. For keyset pagination to work correctly,
23+
/// GUIDs created within the same millisecond must be strictly ordered by creation time.
24+
/// </para>
25+
/// <para>
26+
/// This implementation uses a globally monotonically increasing counter to ensure that
27+
/// all generated GUIDs are strictly ordered by creation time, regardless of timestamp.
28+
/// </para>
29+
/// </remarks>
30+
public static Guid CreateMonotonicUuid(DateTimeOffset timestamp)
31+
{
32+
// UUID v7 format (RFC 9562):
33+
// - 48 bits: Unix timestamp in milliseconds (big-endian)
34+
// - 4 bits: version (0111 = 7)
35+
// - 12 bits: counter/sequence (for intra-millisecond ordering)
36+
// - 2 bits: variant (10)
37+
// - 62 bits: random
38+
39+
long timestampMs = timestamp.ToUnixTimeMilliseconds();
40+
long counter = Interlocked.Increment(ref s_counter);
41+
42+
// Start with a random GUID and twiddle the relevant bits
43+
Guid baseGuid = Guid.NewGuid();
44+
45+
#if NETSTANDARD2_0
46+
byte[] bytes = baseGuid.ToByteArray();
47+
#else
48+
Span<byte> bytes = stackalloc byte[16];
49+
baseGuid.TryWriteBytes(bytes);
50+
#endif
51+
52+
// Guid.ToByteArray() returns bytes in little-endian order for the first 3 components,
53+
// but we need big-endian for UUID v7. The byte layout from ToByteArray() is:
54+
// [0-3]: Data1 (little-endian int)
55+
// [4-5]: Data2 (little-endian short)
56+
// [6-7]: Data3 (little-endian short)
57+
// [8-15]: Data4 (byte array, unchanged)
58+
59+
// Set timestamp (48 bits) - need to account for little-endian layout
60+
// Data1 (bytes 0-3, little-endian) contains timestamp bits 0-31
61+
bytes[0] = (byte)(timestampMs >> 8);
62+
bytes[1] = (byte)(timestampMs >> 16);
63+
bytes[2] = (byte)(timestampMs >> 24);
64+
bytes[3] = (byte)(timestampMs >> 32);
65+
66+
// Data2 (bytes 4-5, little-endian) contains timestamp bits 32-47
67+
bytes[4] = (byte)timestampMs;
68+
bytes[5] = (byte)(timestampMs >> 40);
69+
70+
// Data3 (bytes 6-7, little-endian) contains version (4 bits) + counter high (12 bits)
71+
// Version 7 = 0111, counter uses 12 bits
72+
bytes[6] = (byte)(counter & 0xFF);
73+
bytes[7] = (byte)(0x70 | ((counter >> 8) & 0x0F));
74+
75+
// Set variant (10) in high 2 bits of byte 8
76+
bytes[8] = (byte)((bytes[8] & 0x3F) | 0x80);
77+
78+
return new Guid(bytes);
79+
}
80+
}

src/Common/Polyfills/System/GuidPolyfills.cs

Lines changed: 0 additions & 96 deletions
This file was deleted.

src/ModelContextProtocol.Core/Server/InMemoryMcpTaskStore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -438,7 +438,7 @@ public void Dispose()
438438
}
439439

440440
private string GenerateTaskId() =>
441-
GuidPolyfills.CreateMonotonicUuid(GetUtcNow()).ToString("N");
441+
GuidHelpers.CreateMonotonicUuid(GetUtcNow()).ToString("N");
442442

443443
private static bool IsTerminalStatus(McpTaskStatus status) =>
444444
status is McpTaskStatus.Completed or McpTaskStatus.Failed or McpTaskStatus.Cancelled;

tests/ModelContextProtocol.Tests/ModelContextProtocol.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
<Compile Include="..\..\src\ModelContextProtocol.Core\Server\InMemoryMcpTaskStore.cs" Link="Server\InMemoryMcpTaskStore.cs" />
3434
<!-- Include dependencies for the linked InMemoryMcpTaskStore.cs -->
3535
<Compile Include="..\..\src\Common\Experimentals.cs" Link="Experimentals.cs" />
36-
<Compile Include="..\..\src\Common\Polyfills\System\GuidPolyfills.cs" Link="Polyfills\GuidPolyfills.cs" />
36+
<Compile Include="..\..\src\Common\Polyfills\System\GuidHelpers.cs" Link="Polyfills\GuidHelpers.cs" />
3737
</ItemGroup>
3838

3939
<ItemGroup Condition="'$(TargetFramework)' == 'net472'">

0 commit comments

Comments
 (0)