Skip to content

Commit

Permalink
Remove dependency on UUIDNext (#24)
Browse files Browse the repository at this point in the history
  • Loading branch information
firenero authored Aug 8, 2024
1 parent d26055a commit f67eb02
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 6 deletions.
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);
}
}

0 comments on commit f67eb02

Please sign in to comment.