Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove dependency on UUIDNext #24

Merged
merged 3 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/FastIDs.TypeId/TypeId.Core/TypeId.Core.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="UUIDNext" Version="2.0.2" />

<None Include="../../../README.md" Pack="true" PackagePath="\" />
</ItemGroup>
Expand Down
11 changes: 6 additions & 5 deletions src/FastIDs.TypeId/TypeId.Core/TypeIdDecoded.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
using System.Runtime.InteropServices;
using System.Text.Unicode;
using UUIDNext;
using UUIDNext.Generator;
using FastIDs.TypeId.Uuid;

namespace FastIDs.TypeId;

[StructLayout(LayoutKind.Auto)]
public readonly struct TypeIdDecoded : IEquatable<TypeIdDecoded>, ISpanFormattable, IUtf8SpanFormattable
{
private static readonly UuidGenerator UuidGenerator = new();

/// <summary>
/// The type part of the TypeId.
/// </summary>
Expand Down Expand Up @@ -66,7 +67,7 @@ public int GetSuffix(Span<byte> utf8Output)
/// <returns>DateTimeOffset representing the ID generation timestamp.</returns>
public DateTimeOffset GetTimestamp()
{
var (timestampMs, _) = UuidV7Generator.Decode(Id);
var timestampMs = UuidDecoder.DecodeTimestamp(Id);
return DateTimeOffset.FromUnixTimeMilliseconds(timestampMs);
}

Expand Down Expand Up @@ -194,7 +195,7 @@ public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnly
/// <remarks>
/// This method validates the type. If you are sure that type is valid use <see cref="New(string, bool)"/> to skip type validation.
/// </remarks>
public static TypeIdDecoded New(string type) => FromUuidV7(type, Uuid.NewSequential());
public static TypeIdDecoded New(string type) => FromUuidV7(type, UuidGenerator.New());

/// <summary>
/// Generates new TypeId with the specified type and random UUIDv7. If <paramref name="validateType"/> is false, type is not validated.
Expand All @@ -207,7 +208,7 @@ public bool TryFormat(Span<byte> utf8Destination, out int bytesWritten, ReadOnly
/// Use this method with <paramref name="validateType"/> set to false when you are sure that <paramref name="type"/> is valid.
/// This method is a bit faster than <see cref="New(string)"/> (especially for longer types) because it skips type validation.
/// </remarks>
public static TypeIdDecoded New(string type, bool validateType) => validateType ? New(type) : new TypeIdDecoded(type, Uuid.NewSequential());
public static TypeIdDecoded New(string type, bool validateType) => validateType ? New(type) : new TypeIdDecoded(type, UuidGenerator.New());

/// <summary>
/// Generates new TypeId with the specified type and UUIDv7.
Expand Down
37 changes: 37 additions & 0 deletions src/FastIDs.TypeId/TypeId.Core/Uuid/GuidConverter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System.Runtime.CompilerServices;

namespace FastIDs.TypeId.Uuid;

// The UUIDv7 implementation is extracted from https://github.com/mareek/UUIDNext to prevent transient dependency.
// TypeID doesn't require any UUID implementations except UUIDv7.
internal static class GuidConverter
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public static Guid CreateGuidFromBigEndianBytes(Span<byte> bytes)
{
SetVersion(bytes);
SetVariant(bytes);
return new Guid(bytes, true);
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetVersion(Span<byte> bytes)
{
const byte uuidVersion = 7;
const int versionByteIndex = 6;
//Erase upper 4 bits
bytes[versionByteIndex] &= 0b0000_1111;
//Set 4 upper bits to version
bytes[versionByteIndex] |= uuidVersion << 4;
}

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetVariant(Span<byte> bytes)
{
const int variantByteIndex = 8;
//Erase upper 2 bits
bytes[variantByteIndex] &= 0b0011_1111;
//Set 2 upper bits to variant
bytes[variantByteIndex] |= 0b1000_0000;
}
}
21 changes: 21 additions & 0 deletions src/FastIDs.TypeId/TypeId.Core/Uuid/UuidDecoder.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Buffers.Binary;

namespace FastIDs.TypeId.Uuid;

// The UUIDv7 implementation is extracted from https://github.com/mareek/UUIDNext to prevent transient dependency.
// TypeID doesn't require any UUID implementations except UUIDv7.
internal static class UuidDecoder
{
public static long DecodeTimestamp(Guid guid)
{
// Allocating 2 bytes more to prepend timestamp data.
Span<byte> bytes = stackalloc byte[18];
guid.TryWriteBytes(bytes[2..], bigEndian: true, out _);

var timestampBytes = bytes[..8];
var timestampMs = BinaryPrimitives.ReadInt64BigEndian(timestampBytes);

return timestampMs;
}

}
117 changes: 117 additions & 0 deletions src/FastIDs.TypeId/TypeId.Core/Uuid/UuidGenerator.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
using System.Buffers.Binary;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;

namespace FastIDs.TypeId.Uuid;

// The UUIDv7 implementation is extracted from https://github.com/mareek/UUIDNext to prevent transient dependency.
// TypeID doesn't require any UUID implementations except UUIDv7.

// All timestamps of type `long` in this class are Unix milliseconds unless stated otherwise.
internal class UuidGenerator
{
private const int SequenceBitSize = 7;
private const int SequenceMaxValue = (1 << SequenceBitSize) - 1;

private long _lastUsedTimestamp;
private long _timestampOffset;
private ushort _monotonicSequence;

public Guid New()
{
// Allocating 2 bytes more to prepend timestamp data.
Span<byte> buffer = stackalloc byte[18];

// Offset bytes that are used in ID.
var idBytes = buffer[2..];

var timestamp = GetCurrentUnixMilliseconds();
SetSequence(idBytes[6..8], ref timestamp);
SetTimestamp(buffer[..8], timestamp); // Using full buffer because we need to account for two zero-bytes in front.
RandomNumberGenerator.Fill(idBytes[8..]);

return GuidConverter.CreateGuidFromBigEndianBytes(idBytes);
}

// The implementation copied from DateTimeOffset.ToUnixTimeMilliseconds()
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private long GetCurrentUnixMilliseconds() => DateTime.UtcNow.Ticks / 10000L - 62135596800000L;

[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void SetTimestamp(Span<byte> bytes, long unixMs)
{
BinaryPrimitives.TryWriteInt64BigEndian(bytes, unixMs);
}

private void SetSequence(Span<byte> bytes, ref long timestamp)
{
ushort sequence;
var originalTimestamp = timestamp;

lock (this)
{
sequence = GetSequenceNumber(ref timestamp);
if (sequence > SequenceMaxValue)
{
// if the sequence is greater than the max value, we take advantage
// of the anti-rewind mechanism to simulate a slight change in clock time
timestamp = originalTimestamp + 1;
sequence = GetSequenceNumber(ref timestamp);
}
}

BinaryPrimitives.TryWriteUInt16BigEndian(bytes, sequence);
}

private ushort GetSequenceNumber(ref long timestamp)
{
EnsureTimestampNeverMoveBackward(ref timestamp);

if (timestamp == _lastUsedTimestamp)
{
_monotonicSequence += 1;
}
else
{
_lastUsedTimestamp = timestamp;
_monotonicSequence = GetSequenceSeed();
}

return _monotonicSequence;
}

private void EnsureTimestampNeverMoveBackward(ref long timestamp)
{
var lastUsedTs = _lastUsedTimestamp;
if (_timestampOffset > 0 && timestamp > lastUsedTs)
{
// reset the offset to reduce the drift with the actual time when possible
_timestampOffset = 0;
return;
}

var offsetTimestamp = timestamp + _timestampOffset;
if (offsetTimestamp < lastUsedTs)
{
// if the computer clock has moved backward since the last generated UUID,
// we add an offset to ensure the timestamp always move forward (See RFC Section 6.2)
_timestampOffset = lastUsedTs - timestamp;
timestamp = lastUsedTs;
return;
}

// Happy path
timestamp = offsetTimestamp;
}


private static ushort GetSequenceSeed()
{
// following section 6.2 on "Fixed-Length Dedicated Counter Seeding", the initial value of the sequence is randomized
Span<byte> buffer = stackalloc byte[2];
RandomNumberGenerator.Fill(buffer);
// Setting the highest bit to 0 mitigate the risk of a sequence overflow (see section 6.2)
buffer[0] &= 0b0000_0111;
return BinaryPrimitives.ReadUInt16BigEndian(buffer);
}
}
Loading