Skip to content

Commit

Permalink
Add ISpanParsable on to GrainId (#8565)
Browse files Browse the repository at this point in the history
* Add ISpanParsable on to GrainId

* Add GrainId tests for ISpanParsable and JsonConverter

JsonConverter now uses span parsing path so tests added to verify this works.

* Use constant in stack allocation

Avoids possible stack overflow and speed in the hot path

---------

Co-authored-by: Alex McAuliffe <[email protected]>
  • Loading branch information
Romanx and Alex McAuliffe authored Aug 3, 2023
1 parent d2710e9 commit 3a14313
Show file tree
Hide file tree
Showing 2 changed files with 107 additions and 46 deletions.
71 changes: 60 additions & 11 deletions src/Orleans.Core.Abstractions/IDs/GrainId.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ namespace Orleans.Runtime
/// </summary>
[Serializable, GenerateSerializer, Immutable]
[JsonConverter(typeof(GrainIdJsonConverter))]
public readonly struct GrainId : IEquatable<GrainId>, IComparable<GrainId>, ISerializable, ISpanFormattable
public readonly struct GrainId : IEquatable<GrainId>, IComparable<GrainId>, ISerializable, ISpanFormattable, ISpanParsable<GrainId>
{
[Id(0)]
private readonly GrainType _type;
Expand Down Expand Up @@ -64,36 +64,71 @@ private GrainId(SerializationInfo info, StreamingContext context)
public static GrainId Create(GrainType type, IdSpan key) => new GrainId(type, key);

/// <summary>
/// Creates a new <see cref="GrainType"/> instance.
/// Parses a <see cref="GrainId"/> from the span.
/// </summary>
public static GrainId Parse(string value)
public static GrainId Parse(ReadOnlySpan<char> value, IFormatProvider? provider = null)
{
if (!TryParse(value, out var result))
if (!TryParse(value, provider, out var result))
{
ThrowInvalidGrainId(value);

static void ThrowInvalidGrainId(string value) => throw new ArgumentException($"Unable to parse \"{value}\" as a grain id");
static void ThrowInvalidGrainId(ReadOnlySpan<char> value) => throw new ArgumentException($"Unable to parse \"{value}\" as a grain id");
}

return result;
}

/// <summary>
/// Creates a new <see cref="GrainType"/> instance.
/// Tries to parse a <see cref="GrainId"/> from the span.
/// </summary>
public static bool TryParse(string? value, out GrainId grainId)
/// <returns><see langword="true"/> if a valid <see cref="GrainId"/> was parsed. <see langword="false"/> otherwise</returns>
public static bool TryParse(ReadOnlySpan<char> value, IFormatProvider? provider, out GrainId result)
{
int i;
if (value is null || (i = value.IndexOf('/')) < 0)
if ((i = value.IndexOf('/')) < 0)
{
grainId = default;
result = default;
return false;
}

grainId = new(new GrainType(Encoding.UTF8.GetBytes(value, 0, i)), new IdSpan(Encoding.UTF8.GetBytes(value, i + 1, value.Length - i - 1)));
var typeSpan = value[0..i];
var type = new byte[Encoding.UTF8.GetByteCount(typeSpan)];
Encoding.UTF8.GetBytes(typeSpan, type);

var idSpan = value[(i + 1)..];
var id = new byte[Encoding.UTF8.GetByteCount(idSpan)];
Encoding.UTF8.GetBytes(idSpan, id);

result = new(new GrainType(type), new IdSpan(id));
return true;
}

/// <summary>
/// Parses a <see cref="GrainId"/> from the string.
/// </summary>
public static GrainId Parse(string value)
=> Parse(value.AsSpan(), null);

/// <summary>
/// Parses a <see cref="GrainId"/> from the string.
/// </summary>
public static GrainId Parse(string value, IFormatProvider? provider = null)
=> Parse(value.AsSpan(), provider);

/// <summary>
/// Tries to parse a <see cref="GrainId"/> from the string.
/// </summary>
/// <returns><see langword="true"/> if a valid <see cref="GrainId"/> was parsed. <see langword="false"/> otherwise</returns>
public static bool TryParse(string? value, out GrainId result)
=> TryParse(value.AsSpan(), null, out result);

/// <summary>
/// Tries to parse a <see cref="GrainId"/> from the string.
/// </summary>
/// <returns><see langword="true"/> if a valid <see cref="GrainId"/> was parsed. <see langword="false"/> otherwise</returns>
public static bool TryParse(string? value, IFormatProvider? provider, out GrainId result)
=> TryParse(value.AsSpan(), provider, out result);

/// <summary>
/// <see langword="true"/> if this instance is the default value, <see langword="false"/> if it is not.
/// </summary>
Expand Down Expand Up @@ -167,7 +202,21 @@ bool ISpanFormattable.TryFormat(Span<char> destination, out int charsWritten, Re
public sealed class GrainIdJsonConverter : JsonConverter<GrainId>
{
/// <inheritdoc />
public override GrainId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => GrainId.Parse(reader.GetString()!);
public override GrainId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
{
var valueLength = reader.HasValueSequence
? checked((int)reader.ValueSequence.Length)
: reader.ValueSpan.Length;

Span<char> buf = valueLength <= 128
? (stackalloc char[128])[..valueLength]
: new char[valueLength];

var written = reader.CopyString(buf);
buf = buf[..written];

return GrainId.Parse(buf);
}

/// <inheritdoc />
public override void Write(Utf8JsonWriter writer, GrainId value, JsonSerializerOptions options)
Expand Down
82 changes: 47 additions & 35 deletions test/NonSilo.Tests/General/Identifiertests.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Net;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Orleans;
using Orleans.GrainReferences;
Expand Down Expand Up @@ -97,45 +98,56 @@ public void GrainIdShouldEncodeAndDecodePrimaryKeyGuidCorrectly()
}
}

[Fact, TestCategory("SlowBVT"), TestCategory("Identifiers")]
public void GrainId_ToFromPrintableString()
[Theory, TestCategory("SlowBVT"), TestCategory("Identifiers")]
[MemberData(nameof(TestGrainIds))]
public void GrainId_ToFromPrintableString(GrainId grainId)
{
Guid guid = Guid.NewGuid();
GrainId grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateGuidKey(guid));
GrainId roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Guid key

string extKey = "Guid-ExtKey-1";
guid = Guid.NewGuid();
grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateGuidKey(guid, extKey));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Guid key + Extended Key

grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateGuidKey(guid, (string)null));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Guid key + null Extended Key

long key = random.Next();
grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateIntegerKey(key));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Int64 key

extKey = "Long-ExtKey-2";
key = random.Next();
grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateIntegerKey(key, extKey));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Int64 key + Extended Key

key = UniqueKey.NewKey(key).PrimaryKeyToLong();
grainId = GrainId.Create(GrainType.Create("test"), GrainIdKeyExtensions.CreateIntegerKey(key, extKey));
roundTripped = RoundTripGrainIdToParsable(grainId);
Assert.Equal(grainId, roundTripped); // GrainId.ToPrintableString -- Int64 key + null Extended Key
string str = grainId.ToString();
var roundTripped = GrainId.Parse(str);

Assert.Equal(grainId, roundTripped);
}

[Theory, TestCategory("SlowBVT"), TestCategory("Identifiers")]
[MemberData(nameof(TestGrainIds))]
public void GrainId_TryParseFromPrintableString(GrainId grainId)
{
string str = grainId.ToString();
var success = GrainId.TryParse(str, out var roundTripped);

Assert.True(success);
Assert.Equal(grainId, roundTripped);
}

private GrainId RoundTripGrainIdToParsable(GrainId input)
[Theory, TestCategory("SlowBVT"), TestCategory("Identifiers")]
[MemberData(nameof(TestGrainIds))]
public void GrainId_RoundTripJsonConverter(GrainId grainId)
{
string str = input.ToString();
return GrainId.Parse(str);
var serialized = JsonSerializer.Serialize(grainId);
var deserialized = JsonSerializer.Deserialize<GrainId>(serialized);

Assert.Equal(grainId, deserialized);
}

public static TheoryData<GrainId> TestGrainIds
{
get
{
var td = new TheoryData<GrainId>();
var grainType = GrainType.Create("test");
var guid = Guid.NewGuid();
var integer = Random.Shared.NextInt64();

td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateGuidKey(guid)));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateGuidKey(guid, "Guid-ExtKey-1")));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateGuidKey(guid, (string)null)));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateIntegerKey(integer)));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateIntegerKey(integer, "Long-ExtKey-2")));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateIntegerKey(integer, (string)null)));
td.Add(GrainId.Create(grainType, GrainIdKeyExtensions.CreateIntegerKey(UniqueKey.NewKey(integer).PrimaryKeyToLong(), "Long-ExtKey-2")));

return td;
}
}

[Fact, TestCategory("BVT"), TestCategory("Identifiers")]
Expand Down

0 comments on commit 3a14313

Please sign in to comment.