From da14a5dd9c53ce00f9c16f217ab1fb63eb630500 Mon Sep 17 00:00:00 2001 From: Taiizor <41683699+Taiizor@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:09:19 +0300 Subject: [PATCH] Major Changes --- demo/UUID.Demo/Program.cs | 54 +++++++--- src/UUID/Constructor/UUID.cs | 105 ++++++++++++++++++-- test/UUID.Tests/UUIDConstructorTests.cs | 125 ++++++++++++++++++++++++ 3 files changed, 265 insertions(+), 19 deletions(-) diff --git a/demo/UUID.Demo/Program.cs b/demo/UUID.Demo/Program.cs index b611e87..5323d4c 100644 --- a/demo/UUID.Demo/Program.cs +++ b/demo/UUID.Demo/Program.cs @@ -47,7 +47,39 @@ static async Task Main(string[] args) Guid implicitToGuid = id; // Implicit conversion from UUID to Guid Console.WriteLine($"Implicit conversions successful? {implicitFromGuid == id && implicitToGuid == guid}"); - Console.WriteLine("\n5. Compact UUID Operations:"); + Console.WriteLine("\n5. UUIDv7 Format and Structure:"); + Console.WriteLine("Generating multiple UUIDs in the same millisecond to demonstrate ordering:"); + + // Aynı milisaniye içinde üretilen UUID'ler + List uuidsInSameMs = new(); + for (int i = 0; i < 5; i++) + { + uuidsInSameMs.Add(UUID.New()); + } + + Console.WriteLine("\nUUID Format Breakdown:"); + foreach (UUID uuid in uuidsInSameMs) + { + string uuidStr = uuid.ToString(); + Console.WriteLine($"\nUUID: {uuidStr}"); + Console.WriteLine("Structure:"); + Console.WriteLine($" Timestamp (48-bit): {uuidStr[..12]}"); + Console.WriteLine($" Version (4-bit): {uuid.Version} (indicated by: {uuidStr[12..13]})"); + Console.WriteLine($" Counter (12-bit): {uuidStr[13..16]}"); + Console.WriteLine($" Variant (2-bit): {uuid.Variant} (indicated in random bits)"); + Console.WriteLine($" Random: {uuidStr[16..]}"); + Console.WriteLine($" Time: {uuid.Time:yyyy-MM-dd HH:mm:ss.fff}"); + } + + Console.WriteLine("\nDemonstrating monotonic ordering:"); + List orderedUuids = uuidsInSameMs.OrderBy(u => u).ToList(); + Console.WriteLine("UUIDs in chronological order:"); + foreach (UUID uuid in orderedUuids) + { + Console.WriteLine($" {uuid} - Counter: {uuid.ToString()[13..16]}"); + } + + Console.WriteLine("\n6. Compact UUID Operations:"); Console.WriteLine("Creating compact UUIDs (12 characters):"); // Temel kompakt UUID @@ -68,7 +100,7 @@ static async Task Main(string[] args) Console.WriteLine($"Compact UUID #{i + 1}: {compactId.ToInt64()}"); } - Console.WriteLine("\n6. Binary Operations:"); + Console.WriteLine("\n7. Binary Operations:"); // TryWriteBytes example byte[] byteBuffer = new byte[16]; bool writeSuccess = id.TryWriteBytes(byteBuffer); @@ -79,7 +111,7 @@ static async Task Main(string[] args) byte[] byteArray = id.ToByteArray(); Console.WriteLine($"Direct byte array: {BitConverter.ToString(byteArray).Replace("-", "")}"); - Console.WriteLine("\n7. Int64 (Long) Conversions:"); + Console.WriteLine("\n8. Int64 (Long) Conversions:"); // Generate multiple UUIDs to demonstrate conversion consistency UUID[] uuids = new UUID[5]; ArrayExtension.Fill(uuids); @@ -114,7 +146,7 @@ static async Task Main(string[] args) Console.WriteLine($"Second Int64: {long2}"); Console.WriteLine($"Time ordering preserved?: {long2 > long1}"); - Console.WriteLine("\n8. Base64 Operations:"); + Console.WriteLine("\n9. Base64 Operations:"); string base64 = id.ToBase64(); Console.WriteLine($"UUID -> Base64: {base64}"); UUID fromBase64 = UUID.FromBase64(base64); @@ -127,7 +159,7 @@ static async Task Main(string[] args) Console.WriteLine($"Successfully parsed from Base64: {parsedFromBase64}"); } - Console.WriteLine("\n9. Byte Array Operations:"); + Console.WriteLine("\n10. Byte Array Operations:"); byte[] bytes2 = id.ToByteArray(); Console.WriteLine($"UUID -> Bytes: {BitConverter.ToString(bytes2)}"); UUID fromBytes = UUID.FromByteArray(bytes2); @@ -147,7 +179,7 @@ static async Task Main(string[] args) Console.WriteLine($"Successfully wrote to byte array: {BitConverter.ToString(destination)}"); } - Console.WriteLine("\n10. Comparison Operations:"); + Console.WriteLine("\n11. Comparison Operations:"); UUID id1 = new(); // Using parameterless constructor await Task.Delay(1); // Wait to ensure different timestamp UUID id2 = UUID.New(); @@ -159,7 +191,7 @@ static async Task Main(string[] args) Console.WriteLine($"UUID1 > UUID2: {id1 > id2}"); Console.WriteLine($"UUID1 >= UUID2: {id1 >= id2}"); - Console.WriteLine("\n11. Array Extension Methods:"); + Console.WriteLine("\n12. Array Extension Methods:"); // Generate array of UUIDs UUID[] generatedArray = ArrayExtension.Generate(5); Console.WriteLine("Generated array of 5 UUIDs:"); @@ -198,7 +230,7 @@ static async Task Main(string[] args) } } - Console.WriteLine("\n12. Sorting and Thread Safety:"); + Console.WriteLine("\n13. Sorting and Thread Safety:"); List ids = new(); for (int i = 0; i < 5; i++) { @@ -219,7 +251,7 @@ static async Task Main(string[] args) Console.WriteLine($" {uuid} - Time: {uuid.Time:yyyy-MM-dd HH:mm:ss.fff}"); } - Console.WriteLine("\n13. Thread-Safe UUID Generation:"); + Console.WriteLine("\n14. Thread-Safe UUID Generation:"); HashSet set = new(); List tasks = new(); @@ -244,7 +276,7 @@ static async Task Main(string[] args) await Task.WhenAll(tasks); Console.WriteLine($"Generated {set.Count} unique UUIDs across multiple threads"); - Console.WriteLine("\n14. Comparing UUID and Guid initialization behaviors:\n"); + Console.WriteLine("\n15. Comparing UUID and Guid initialization behaviors:\n"); // UUID initialization - always creates a unique identifier UUID uuid3 = new(); @@ -280,7 +312,7 @@ static async Task Main(string[] args) Console.WriteLine($"Are they equal? {newGuid1 == newGuid2}"); Console.WriteLine($"Is first empty? {newGuid1 == default}"); - Console.WriteLine("\n15. Bulk UUID Generation Performance:"); + Console.WriteLine("\n16. Bulk UUID Generation Performance:"); const int batchSize = 1000; Console.WriteLine($"Generating {batchSize} UUIDs in batch..."); diff --git a/src/UUID/Constructor/UUID.cs b/src/UUID/Constructor/UUID.cs index 97d5043..3fee311 100644 --- a/src/UUID/Constructor/UUID.cs +++ b/src/UUID/Constructor/UUID.cs @@ -6,14 +6,15 @@ namespace System /// /// UUID represents a modern and efficient unique identifier implementation, /// designed for high performance and enhanced security in distributed systems. - /// This implementation follows UUIDv7 principles (draft-ietf-uuidrev-rfc4122bis). + /// This implementation follows UUIDv7 standard (draft-ietf-uuidrev-rfc4122bis). /// /// /// This implementation provides: - /// - Time-based ordering: Identifiers are sortable by creation time (48-bit timestamp) - /// - Security: Uses cryptographically secure random numbers - /// - Performance: Optimized for high-performance scenarios - /// - Compatibility: Full integration with .NET ecosystem + /// - Time-based ordering: 48-bit Unix timestamp (milliseconds) + /// - Monotonic counter: 12-bit sequence counter for same-millisecond ordering + /// - Version bits: 4 bits indicating UUIDv7 + /// - Variant bits: 2 bits as per RFC 4122 + /// - Random bits: Remaining bits for uniqueness /// - Thread Safety: All operations are thread-safe /// public readonly partial struct UUID(ulong timestamp, ulong random) : IEquatable, IComparable, IComparable @@ -27,6 +28,49 @@ public readonly partial struct UUID(ulong timestamp, ulong random) : IEquatable< /// private const int SIZE = 16; + /// + /// Thread-safe counter for monotonic sequence within same millisecond. + /// Used to ensure unique and ordered UUIDs when multiple are generated + /// in the same millisecond. + /// + private static int _counter; + + /// + /// Gets the variant number of this UUID. + /// + /// + /// Returns 2 for RFC 4122 variant UUIDs. + /// This is used to indicate the layout of bits in the UUID. + /// + public byte Variant => VARIANT; + + /// + /// Gets the version number of this UUID. + /// + /// + /// Returns 7 for UUIDv7 (time-ordered with additional random bits). + /// The version number indicates how the UUID was generated. + /// + public byte Version => VERSION; + + /// + /// RFC 4122 variant identifier (2). + /// Used to indicate the layout of bits in the UUID. + /// + private const byte VARIANT = 0x02; + + /// + /// UUID version identifier (7). + /// Indicates this is a Version 7 UUID (time-ordered). + /// + private const byte VERSION = 0x07; + + /// + /// Stores the timestamp of the last generated UUID. + /// Used for maintaining monotonic ordering within the same millisecond. + /// + private static long _lastTimestamp; + /// /// Gets the random component of the UUID. /// @@ -42,6 +86,12 @@ public readonly partial struct UUID(ulong timestamp, ulong random) : IEquatable< /// internal readonly ulong _timestamp = timestamp; + /// + /// Lock object for thread-safe counter operations. + /// Ensures monotonic ordering of UUIDs generated within the same millisecond. + /// + private static readonly object _counterLock = new(); + /// /// Characters used in Base32 encoding. /// @@ -132,10 +182,43 @@ public static UUID NewCompactWithTime(long timestamp) /// private static ulong GenerateTimestamp() { - ushort random = (ushort)_rng.Value!.Next(ushort.MaxValue); long unixMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + int counter = GetMonotonicCounter(unixMs); - return (((ulong)unixMs & 0x0000_FFFF_FFFF_FFFF) << 16) | random; + // Format: 48 bits timestamp + 4 bits version + 12 bits counter + ulong timestamp = ((ulong)unixMs & 0x0000_FFFF_FFFF_FFFF) << 16; + timestamp |= (ulong)VERSION << 12; + timestamp |= (uint)counter; + + return timestamp; + } + + /// + /// Gets a monotonically increasing counter for UUIDs generated within the same millisecond. + /// + /// The current timestamp in milliseconds + /// A 12-bit counter value that ensures monotonic ordering + /// + /// This method ensures that UUIDs generated within the same millisecond + /// maintain a strict ordering through the use of a 12-bit counter. + /// The counter resets when moving to a new millisecond. + /// + private static int GetMonotonicCounter(long timestamp) + { + lock (_counterLock) + { + if (timestamp > _lastTimestamp) + { + _counter = 0; + _lastTimestamp = timestamp; + } + else if (timestamp == _lastTimestamp) + { + _counter = (_counter + 1) & 0xFFF; // 12-bit counter + } + + return _counter; + } } /// @@ -148,7 +231,12 @@ private static ulong GenerateTimestamp() /// private static ulong GenerateRandom() { - return ((ulong)_rng.Value!.Next() << 32) | (uint)_rng.Value!.Next(); + ulong random = ((ulong)_rng.Value!.Next() << 32) | (uint)_rng.Value!.Next(); + + // Set the variant bits + random = (random & 0x3FFF_FFFF_FFFF_FFFF) | ((ulong)VARIANT << 62); + + return random; } /// @@ -313,6 +401,7 @@ public string ToBase32() public string ToBase64() { byte[] bytes = new byte[SIZE]; + TryWriteBytes(bytes); return Convert.ToBase64String(bytes); diff --git a/test/UUID.Tests/UUIDConstructorTests.cs b/test/UUID.Tests/UUIDConstructorTests.cs index 6c1dc3f..3078902 100644 --- a/test/UUID.Tests/UUIDConstructorTests.cs +++ b/test/UUID.Tests/UUIDConstructorTests.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Globalization; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Xunit; @@ -368,5 +370,128 @@ public void ToInt64_WithSpecificValues_IsConsistent(ulong timestamp, ulong rando Assert.Equal(value1, value2); Assert.True(value1 >= 0, "Converted long value should be non-negative"); } + + [Fact] + public void TestUUIDVersion() + { + // Arrange & Act + UUID uuid = UUID.New(); + + // Assert + Assert.Equal(0x07, uuid.Version); // UUIDv7 + } + + [Fact] + public void TestUUIDVariant() + { + // Arrange & Act + UUID uuid = UUID.New(); + + // Assert + Assert.Equal(0x02, uuid.Variant); // RFC 4122 + } + + [Fact] + public void TestMonotonicOrderingInSameMillisecond() + { + // Arrange + const int count = 1000; + List uuids = new(); + + // Act - Generate UUIDs as fast as possible to get many in the same millisecond + for (int i = 0; i < count; i++) + { + uuids.Add(UUID.New()); + } + + // Assert + for (int i = 1; i < uuids.Count; i++) + { + Assert.True(uuids[i] > uuids[i - 1], + "UUIDs should maintain monotonic ordering even within the same millisecond"); + } + } + + [Fact] + public void TestUUIDFormatStructure() + { + // Arrange + UUID uuid = UUID.New(); + string uuidStr = uuid.ToString(); + + // Act & Assert + // Check timestamp portion (first 48 bits = 12 hex chars) + string timestampHex = uuidStr[..12]; + Assert.Equal(12, timestampHex.Length); + Assert.True(long.TryParse(timestampHex, NumberStyles.HexNumber, null, out _)); + + // Check version (13th character should be 7) + Assert.Equal('7', uuidStr[12]); + + // Check counter portion (next 12 bits = 3 hex chars) + string counterHex = uuidStr.Substring(13, 3); + Assert.Equal(3, counterHex.Length); + Assert.True(int.TryParse(counterHex, NumberStyles.HexNumber, null, out int counter)); + Assert.True(counter <= 0xFFF); + + // Check remaining random bits + string randomHex = uuidStr[16..]; + Assert.Equal(16, randomHex.Length); + Assert.True(long.TryParse(randomHex, NumberStyles.HexNumber, null, out _)); + } + + [Fact] + public void TestThreadSafetyWithMonotonicCounter() + { + // Arrange + const int threadCount = 10; + const int uuidsPerThread = 1000; + ConcurrentBag allUuids = new ConcurrentBag(); + List tasks = new List(); + + // Act + for (int i = 0; i < threadCount; i++) + { + tasks.Add(Task.Run(() => + { + for (int j = 0; j < uuidsPerThread; j++) + { + allUuids.Add(UUID.New()); + } + })); + } + Task.WaitAll(tasks.ToArray()); + + // Assert + List sortedUuids = allUuids.OrderBy(u => u).ToList(); + + // Check for uniqueness + Assert.Equal(threadCount * uuidsPerThread, sortedUuids.Distinct().Count()); + + // Check for monotonic ordering + for (int i = 1; i < sortedUuids.Count; i++) + { + Assert.True(sortedUuids[i] > sortedUuids[i - 1], + "UUIDs should maintain monotonic ordering across threads"); + } + } + + [Fact] + public void TestTimeAccuracy() + { + // Arrange + DateTimeOffset before = DateTimeOffset.UtcNow; + Thread.Sleep(1); // Ensure we're in a new millisecond + + // Act + UUID uuid = UUID.New(); + + Thread.Sleep(1); // Ensure we're in a new millisecond + DateTimeOffset after = DateTimeOffset.UtcNow; + + // Assert + Assert.True(uuid.Time >= before); + Assert.True(uuid.Time <= after); + } } } \ No newline at end of file