Skip to content
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
174 changes: 155 additions & 19 deletions src/Orleans.Core.Abstractions/IDs/SiloAddress.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@ namespace Orleans.Runtime
[JsonConverter(typeof(SiloAddressConverter))]
[DebuggerDisplay("SiloAddress {ToString()}")]
[SuppressReferenceTracking]
public sealed class SiloAddress : IEquatable<SiloAddress>, IComparable<SiloAddress>, ISpanFormattable
public sealed class SiloAddress :
IEquatable<SiloAddress>,
IComparable<SiloAddress>,
ISpanFormattable,
IParsable<SiloAddress>,
IUtf8SpanParsable<SiloAddress>
{
[NonSerialized]
private int hashCode;
Expand Down Expand Up @@ -171,20 +176,88 @@ internal byte[] ToUtf8String()
/// <param name="addr">String containing the SiloAddress info to be parsed.</param>
/// <returns>New SiloAddress object created from the input data.</returns>
public static SiloAddress FromParsableString(string addr)
=> Parse(addr, null);

/// <summary>
/// Parses a <see cref="SiloAddress"/> from a string in the standard form returned from <see cref="ToParsableString"/>.
/// </summary>
/// <param name="value">String containing the <see cref="SiloAddress"/> info to be parsed.</param>
/// <returns>A <see cref="SiloAddress"/> parsed from the input data.</returns>
public static SiloAddress Parse(string value)
=> Parse(value, null);

/// <summary>
/// Parses a <see cref="SiloAddress"/> from a string in the standard form returned from <see cref="ToParsableString"/>.
/// </summary>
/// <param name="value">String containing the <see cref="SiloAddress"/> info to be parsed.</param>
/// <param name="provider">An object that provides culture-specific formatting information. This parameter is ignored.</param>
/// <returns>A <see cref="SiloAddress"/> parsed from the input data.</returns>
public static SiloAddress Parse(string value, IFormatProvider? provider = null)
{
ArgumentNullException.ThrowIfNull(value);
if (!TryParse(value, provider, out var result))
{
throw new FormatException("Invalid string SiloAddress: " + value);
}

return result;
}

/// <summary>
/// Tries to parse a <see cref="SiloAddress"/> from a string in the standard form returned from <see cref="ToParsableString"/>.
/// </summary>
/// <param name="value">String containing the <see cref="SiloAddress"/> info to be parsed.</param>
/// <param name="result">The parsed <see cref="SiloAddress"/>, or <see langword="null"/> if parsing failed.</param>
/// <returns><see langword="true"/> if parsing succeeded; otherwise, <see langword="false"/>.</returns>
public static bool TryParse([NotNullWhen(true)] string? value, [NotNullWhen(true)] out SiloAddress? result)
=> TryParse(value, null, out result);

/// <summary>
/// Tries to parse a <see cref="SiloAddress"/> from a string in the standard form returned from <see cref="ToParsableString"/>.
/// </summary>
/// <param name="value">String containing the <see cref="SiloAddress"/> info to be parsed.</param>
/// <param name="provider">An object that provides culture-specific formatting information. This parameter is ignored.</param>
/// <param name="result">The parsed <see cref="SiloAddress"/>, or <see langword="null"/> if parsing failed.</param>
/// <returns><see langword="true"/> if parsing succeeded; otherwise, <see langword="false"/>.</returns>
public static bool TryParse([NotNullWhen(true)] string? value, IFormatProvider? provider, [NotNullWhen(true)] out SiloAddress? result)
{
if (value is null)
{
result = null;
return false;
}

return TryParse(value.AsSpan(), out result);
}

private static bool TryParse(ReadOnlySpan<char> addr, [NotNullWhen(true)] out SiloAddress? result)
{
// This must be the "inverse" of ToParsableString, and must be the same across all silos in a deployment.
// Basically, this should never change unless the data content of SiloAddress changes

// First is the IPEndpoint; then '@'; then the generation
int atSign = addr.LastIndexOf(SEPARATOR);
var atSign = addr.LastIndexOf(SEPARATOR);
if (atSign < 0)
{
result = null;
return false;
}

// IPEndpoint is the host, then ':', then the port
int lastColon = addr.LastIndexOf(':', atSign - 1);
if (atSign < 0 || lastColon < 0) throw new FormatException("Invalid string SiloAddress: " + addr);
var endpoint = addr[..atSign];
var lastColon = endpoint.LastIndexOf(':');
if (lastColon < 0
|| !IPAddress.TryParse(endpoint[..lastColon], out var host)
|| !int.TryParse(endpoint[(lastColon + 1)..], NumberStyles.None, CultureInfo.InvariantCulture, out var port)
|| port is < IPEndPoint.MinPort or > IPEndPoint.MaxPort
|| !int.TryParse(addr[(atSign + 1)..], NumberStyles.None, CultureInfo.InvariantCulture, out var gen))
{
result = null;
return false;
}

var host = IPAddress.Parse(addr.AsSpan(0, lastColon));
int port = int.Parse(addr.AsSpan(lastColon + 1, atSign - lastColon - 1), NumberStyles.None);
var gen = int.Parse(addr.AsSpan(atSign + 1), NumberStyles.None);
return New(host, port, gen);
result = New(host, port, gen);
return true;
}

/// <summary>
Expand All @@ -193,36 +266,99 @@ public static SiloAddress FromParsableString(string addr)
/// <param name="addr">String containing the SiloAddress info to be parsed.</param>
/// <returns>New SiloAddress object created from the input data.</returns>
public static SiloAddress FromUtf8String(ReadOnlySpan<byte> addr)
=> Parse(addr, null);

/// <summary>
/// Parses a <see cref="SiloAddress"/> from UTF-8 text in the standard form returned from <see cref="ToParsableString"/>.
/// </summary>
/// <param name="utf8Text">UTF-8 text containing the <see cref="SiloAddress"/> info to be parsed.</param>
/// <returns>A <see cref="SiloAddress"/> parsed from the input data.</returns>
public static SiloAddress Parse(ReadOnlySpan<byte> utf8Text)
=> Parse(utf8Text, null);

/// <summary>
/// Parses a <see cref="SiloAddress"/> from UTF-8 text in the standard form returned from <see cref="ToParsableString"/>.
/// </summary>
/// <param name="utf8Text">UTF-8 text containing the <see cref="SiloAddress"/> info to be parsed.</param>
/// <param name="provider">An object that provides culture-specific formatting information. This parameter is ignored.</param>
/// <returns>A <see cref="SiloAddress"/> parsed from the input data.</returns>
public static SiloAddress Parse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider = null)
{
if (!TryParse(utf8Text, provider, out var result))
{
ThrowInvalidUtf8SiloAddress(utf8Text);
}

return result;
}

/// <summary>
/// Tries to parse a <see cref="SiloAddress"/> from UTF-8 text in the standard form returned from <see cref="ToParsableString"/>.
/// </summary>
/// <param name="utf8Text">UTF-8 text containing the <see cref="SiloAddress"/> info to be parsed.</param>
/// <param name="result">The parsed <see cref="SiloAddress"/>, or <see langword="null"/> if parsing failed.</param>
/// <returns><see langword="true"/> if parsing succeeded; otherwise, <see langword="false"/>.</returns>
public static bool TryParse(ReadOnlySpan<byte> utf8Text, [NotNullWhen(true)] out SiloAddress? result)
=> TryParse(utf8Text, null, out result);

/// <summary>
/// Tries to parse a <see cref="SiloAddress"/> from UTF-8 text in the standard form returned from <see cref="ToParsableString"/>.
/// </summary>
/// <param name="utf8Text">UTF-8 text containing the <see cref="SiloAddress"/> info to be parsed.</param>
/// <param name="provider">An object that provides culture-specific formatting information. This parameter is ignored.</param>
/// <param name="result">The parsed <see cref="SiloAddress"/>, or <see langword="null"/> if parsing failed.</param>
/// <returns><see langword="true"/> if parsing succeeded; otherwise, <see langword="false"/>.</returns>
public static bool TryParse(ReadOnlySpan<byte> utf8Text, IFormatProvider? provider, [NotNullWhen(true)] out SiloAddress? result)
{
// This must be the "inverse" of ToParsableString, and must be the same across all silos in a deployment.
// Basically, this should never change unless the data content of SiloAddress changes

// First is the IPEndpoint; then '@'; then the generation
var atSign = addr.LastIndexOf((byte)SEPARATOR);
if (atSign < 0) ThrowInvalidUtf8SiloAddress(addr);
var atSign = utf8Text.LastIndexOf((byte)SEPARATOR);
if (atSign < 0)
{
result = null;
return false;
}

// IPEndpoint is the host, then ':', then the port
var endpointSlice = addr[..atSign];
var endpointSlice = utf8Text[..atSign];
int lastColon = endpointSlice.LastIndexOf((byte)':');
if (lastColon < 0) ThrowInvalidUtf8SiloAddress(addr);
if (lastColon < 0)
{
result = null;
return false;
}

var ipSlice = endpointSlice[..lastColon];
Span<char> buf = stackalloc char[45];
var hostString = Encoding.UTF8.GetCharCount(ipSlice) is int len && len <= buf.Length
? buf[..Encoding.UTF8.GetChars(ipSlice, buf)]
: Encoding.UTF8.GetString(ipSlice).AsSpan();
if (!IPAddress.TryParse(hostString, out var host))
ThrowInvalidUtf8SiloAddress(addr);
{
result = null;
return false;
}

var portSlice = endpointSlice[(lastColon + 1)..];
if (!Utf8Parser.TryParse(portSlice, out int port, out len) || len < portSlice.Length)
ThrowInvalidUtf8SiloAddress(addr);
if (!Utf8Parser.TryParse(portSlice, out int port, out len)
|| len != portSlice.Length
|| port is < IPEndPoint.MinPort or > IPEndPoint.MaxPort)
{
result = null;
return false;
}

var genSlice = addr[(atSign + 1)..];
if (!Utf8Parser.TryParse(genSlice, out int generation, out len) || len < genSlice.Length)
ThrowInvalidUtf8SiloAddress(addr);
var genSlice = utf8Text[(atSign + 1)..];
if (!Utf8Parser.TryParse(genSlice, out int generation, out len) || len != genSlice.Length)
{
result = null;
return false;
}

return New(host, port, generation);
result = New(host, port, generation);
return true;
}

[DoesNotReturn]
Expand Down
18 changes: 17 additions & 1 deletion src/api/Orleans.Core.Abstractions/Orleans.Core.Abstractions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2932,7 +2932,7 @@ public sealed partial class ResourceOptimizedPlacement : PlacementStrategy
[System.Text.Json.Serialization.JsonConverter(typeof(SiloAddressConverter))]
[System.Diagnostics.DebuggerDisplay("SiloAddress {ToString()}")]
[SuppressReferenceTracking]
public sealed partial class SiloAddress : System.IEquatable<SiloAddress>, System.IComparable<SiloAddress>, System.ISpanFormattable, System.IFormattable
public sealed partial class SiloAddress : System.IEquatable<SiloAddress>, System.IComparable<SiloAddress>, System.ISpanFormattable, System.IFormattable, System.IParsable<SiloAddress>, System.IUtf8SpanParsable<SiloAddress>
{
internal SiloAddress() { }

Expand Down Expand Up @@ -2972,6 +2972,14 @@ internal SiloAddress() { }

public static SiloAddress New(System.Net.IPEndPoint ep, int gen) { throw null; }

public static SiloAddress Parse(string value) { throw null; }

public static SiloAddress Parse(string value, System.IFormatProvider? provider = null) { throw null; }

public static SiloAddress Parse(System.ReadOnlySpan<byte> utf8Text) { throw null; }

public static SiloAddress Parse(System.ReadOnlySpan<byte> utf8Text, System.IFormatProvider? provider = null) { throw null; }

string System.IFormattable.ToString(string? format, System.IFormatProvider? formatProvider) { throw null; }

bool System.ISpanFormattable.TryFormat(System.Span<char> destination, out int charsWritten, System.ReadOnlySpan<char> format, System.IFormatProvider? provider) { throw null; }
Expand All @@ -2981,6 +2989,14 @@ internal SiloAddress() { }
public override string ToString() { throw null; }

public string ToStringWithHashCode() { throw null; }

public static bool TryParse(string? value, out SiloAddress? result) { throw null; }

public static bool TryParse(string? value, System.IFormatProvider? provider, out SiloAddress? result) { throw null; }

public static bool TryParse(System.ReadOnlySpan<byte> utf8Text, out SiloAddress? result) { throw null; }

public static bool TryParse(System.ReadOnlySpan<byte> utf8Text, System.IFormatProvider? provider, out SiloAddress? result) { throw null; }
}

public sealed partial class SiloAddressConverter : System.Text.Json.Serialization.JsonConverter<SiloAddress>
Expand Down
28 changes: 28 additions & 0 deletions test/Orleans.Core.Tests/General/Identifiertests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Net;
using System.Text;
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Orleans;
Expand Down Expand Up @@ -341,6 +342,33 @@ public void SiloAddress_ToFrom_ParsableString()

Assert.Equal(addressStr2, addressStr2Out); // SiloAddress equal after From-To-ParsableString
}

[Fact, TestCategory("BVT"), TestCategory("Identifiers")]
public void SiloAddress_ParseInterfaces()
{
const string addressString = "127.0.0.1:11111@144611139";
var expected = SiloAddress.FromParsableString(addressString);

Assert.Same(expected, SiloAddress.Parse(addressString));
Assert.Same(expected, SiloAddress.Parse(addressString, null));
Assert.True(SiloAddress.TryParse(addressString, out var parsedWithoutProvider));
Assert.Same(expected, parsedWithoutProvider);
Assert.True(SiloAddress.TryParse(addressString, null, out var parsed));
Assert.Same(expected, parsed);

var utf8 = Encoding.UTF8.GetBytes(addressString);
Assert.Same(expected, SiloAddress.Parse(utf8));
Assert.Same(expected, SiloAddress.Parse(utf8, null));
Assert.Same(expected, SiloAddress.FromUtf8String(utf8));
Assert.True(SiloAddress.TryParse(utf8, out var utf8ParsedWithoutProvider));
Assert.Same(expected, utf8ParsedWithoutProvider);
Assert.True(SiloAddress.TryParse(utf8, null, out var utf8Parsed));
Assert.Same(expected, utf8Parsed);

Assert.False(SiloAddress.TryParse("not-a-silo", out _));
Assert.False(SiloAddress.TryParse(Encoding.UTF8.GetBytes("not-a-silo"), null, out _));
}

/// <summary>
/// Tests GrainReference creation, serialization, and round-trip operations.
/// </summary>
Expand Down
Loading