feat(client): standardize connection outcomes (#13)
quality-gate / quality (push) Successful in 59s
quality-gate / quality (push) Successful in 59s
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
using System.Diagnostics;
|
||||
using System.Security.Cryptography;
|
||||
using FinalFactory.Rendezvous.Contracts;
|
||||
using LiteNetLib;
|
||||
|
||||
namespace FinalFactory.Rendezvous.Client;
|
||||
@@ -15,12 +17,97 @@ public enum RendezvousConnectionState
|
||||
Disposed = 8,
|
||||
}
|
||||
|
||||
public sealed class RendezvousConnectionCompletedEventArgs(
|
||||
RendezvousConnectionState state,
|
||||
NetPeer? peer = null) : EventArgs
|
||||
public sealed class RendezvousConnectionCompletedEventArgs : EventArgs
|
||||
{
|
||||
public RendezvousConnectionState State { get; } = state;
|
||||
public NetPeer? Peer { get; } = peer;
|
||||
[Obsolete("Completion events now expose a typed Outcome. Construct these arguments only for legacy test doubles.")]
|
||||
public RendezvousConnectionCompletedEventArgs(
|
||||
RendezvousConnectionState state,
|
||||
NetPeer? peer)
|
||||
: this(state, RendezvousCompletionInvariant.FromLegacy(state, peer))
|
||||
{
|
||||
}
|
||||
|
||||
internal RendezvousConnectionCompletedEventArgs(
|
||||
RendezvousConnectionState state,
|
||||
RendezvousConnectionOutcome outcome)
|
||||
{
|
||||
RendezvousCompletionInvariant.Validate(state, outcome);
|
||||
State = state;
|
||||
Outcome = outcome;
|
||||
}
|
||||
|
||||
public RendezvousConnectionState State { get; }
|
||||
public RendezvousConnectionOutcome Outcome { get; }
|
||||
public NetPeer? Peer => Outcome.Peer;
|
||||
}
|
||||
|
||||
internal static class RendezvousCompletionInvariant
|
||||
{
|
||||
internal static RendezvousConnectionOutcome FromLegacy(
|
||||
RendezvousConnectionState state,
|
||||
NetPeer? peer) => state switch
|
||||
{
|
||||
RendezvousConnectionState.Connected when peer is not null => RendezvousConnectionOutcome.Create(
|
||||
ConnectionOutcomeKind.Connected,
|
||||
RendezvousConnectionOutcomeSource.LocalTraversal,
|
||||
RendezvousConnectionFailureCategory.None,
|
||||
RendezvousConnectionPhase.Complete,
|
||||
TimeSpan.Zero,
|
||||
peer: peer),
|
||||
RendezvousConnectionState.Cancelled => RendezvousConnectionOutcome.Create(
|
||||
ConnectionOutcomeKind.Cancelled,
|
||||
RendezvousConnectionOutcomeSource.Caller,
|
||||
RendezvousConnectionFailureCategory.Lifecycle,
|
||||
RendezvousConnectionPhase.Complete,
|
||||
TimeSpan.Zero),
|
||||
RendezvousConnectionState.TimedOut => RendezvousConnectionOutcome.Create(
|
||||
ConnectionOutcomeKind.DirectConnectTimedOut,
|
||||
RendezvousConnectionOutcomeSource.LocalTraversal,
|
||||
RendezvousConnectionFailureCategory.DirectConnection,
|
||||
RendezvousConnectionPhase.DirectConnection,
|
||||
TimeSpan.Zero),
|
||||
RendezvousConnectionState.Rejected => RendezvousConnectionOutcome.Create(
|
||||
ConnectionOutcomeKind.HostRejected,
|
||||
RendezvousConnectionOutcomeSource.RemoteHost,
|
||||
RendezvousConnectionFailureCategory.Authorization,
|
||||
RendezvousConnectionPhase.Authorization,
|
||||
TimeSpan.Zero),
|
||||
RendezvousConnectionState.ManagerStopped => RendezvousConnectionOutcome.Create(
|
||||
ConnectionOutcomeKind.ManagerStopped,
|
||||
RendezvousConnectionOutcomeSource.Lifecycle,
|
||||
RendezvousConnectionFailureCategory.Lifecycle,
|
||||
RendezvousConnectionPhase.Complete,
|
||||
TimeSpan.Zero),
|
||||
RendezvousConnectionState.Disposed => RendezvousConnectionOutcome.Create(
|
||||
ConnectionOutcomeKind.Disposed,
|
||||
RendezvousConnectionOutcomeSource.Lifecycle,
|
||||
RendezvousConnectionFailureCategory.Lifecycle,
|
||||
RendezvousConnectionPhase.Complete,
|
||||
TimeSpan.Zero),
|
||||
RendezvousConnectionState.Connected => throw new ArgumentNullException(
|
||||
nameof(peer),
|
||||
"A connected completion requires a peer."),
|
||||
_ => throw new ArgumentOutOfRangeException(
|
||||
nameof(state),
|
||||
state,
|
||||
"A completion event requires a terminal connection state."),
|
||||
};
|
||||
|
||||
internal static void Validate(
|
||||
RendezvousConnectionState state,
|
||||
RendezvousConnectionOutcome outcome)
|
||||
{
|
||||
if (outcome is null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(outcome));
|
||||
}
|
||||
if ((state == RendezvousConnectionState.Connected) != outcome.IsSuccess)
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"The connection state and typed outcome contradict each other.",
|
||||
nameof(outcome));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class RendezvousCoordinatorOptions
|
||||
@@ -29,8 +116,11 @@ public sealed class RendezvousCoordinatorOptions
|
||||
public int MaximumAttemptChecksPerPoll { get; set; } = 128;
|
||||
public TimeSpan InitialPunchRetryDelay { get; set; } = TimeSpan.FromMilliseconds(200);
|
||||
public TimeSpan MaximumPunchRetryDelay { get; set; } = TimeSpan.FromSeconds(2);
|
||||
public TimeSpan PunchTimeout { get; set; } = TimeSpan.FromSeconds(10);
|
||||
public TimeSpan DirectConnectTimeout { get; set; } = TimeSpan.FromSeconds(5);
|
||||
public TimeSpan ConnectionTicketLifetime { get; set; } = TimeSpan.FromSeconds(20);
|
||||
public double JitterRatio { get; set; } = 0.2;
|
||||
public NetworkEndpoint? DedicatedFallbackOverride { get; set; }
|
||||
|
||||
internal RendezvousCoordinatorOptions CopyAndValidate()
|
||||
{
|
||||
@@ -39,9 +129,15 @@ public sealed class RendezvousCoordinatorOptions
|
||||
|| InitialPunchRetryDelay < TimeSpan.FromMilliseconds(10)
|
||||
|| MaximumPunchRetryDelay < InitialPunchRetryDelay
|
||||
|| MaximumPunchRetryDelay > TimeSpan.FromSeconds(10)
|
||||
|| PunchTimeout <= TimeSpan.Zero
|
||||
|| PunchTimeout > TimeSpan.FromSeconds(30)
|
||||
|| DirectConnectTimeout <= TimeSpan.Zero
|
||||
|| DirectConnectTimeout > TimeSpan.FromSeconds(30)
|
||||
|| ConnectionTicketLifetime <= TimeSpan.Zero
|
||||
|| ConnectionTicketLifetime > TimeSpan.FromSeconds(20)
|
||||
|| JitterRatio is < 0 or > 1)
|
||||
|| JitterRatio is < 0 or > 1
|
||||
|| DedicatedFallbackOverride is not null
|
||||
&& !ContractValidation.IsNetworkEndpointValid(DedicatedFallbackOverride))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(RendezvousCoordinatorOptions));
|
||||
}
|
||||
@@ -52,8 +148,11 @@ public sealed class RendezvousCoordinatorOptions
|
||||
MaximumAttemptChecksPerPoll = MaximumAttemptChecksPerPoll,
|
||||
InitialPunchRetryDelay = InitialPunchRetryDelay,
|
||||
MaximumPunchRetryDelay = MaximumPunchRetryDelay,
|
||||
PunchTimeout = PunchTimeout,
|
||||
DirectConnectTimeout = DirectConnectTimeout,
|
||||
ConnectionTicketLifetime = ConnectionTicketLifetime,
|
||||
JitterRatio = JitterRatio,
|
||||
DedicatedFallbackOverride = RendezvousEndpoint.Copy(DedicatedFallbackOverride),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -61,11 +160,16 @@ public sealed class RendezvousCoordinatorOptions
|
||||
internal interface IRendezvousCoordinatorClock
|
||||
{
|
||||
DateTimeOffset UtcNow { get; }
|
||||
TimeSpan Elapsed { get; }
|
||||
}
|
||||
|
||||
internal sealed class SystemRendezvousCoordinatorClock : IRendezvousCoordinatorClock
|
||||
{
|
||||
private readonly long _origin = Stopwatch.GetTimestamp();
|
||||
|
||||
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
|
||||
public TimeSpan Elapsed => TimeSpan.FromSeconds(
|
||||
(Stopwatch.GetTimestamp() - _origin) / (double)Stopwatch.Frequency);
|
||||
}
|
||||
|
||||
internal static class RendezvousManagerGuard
|
||||
@@ -95,11 +199,11 @@ internal sealed class RendezvousPunchRetrySchedule(
|
||||
IRendezvousCoordinatorClock clock)
|
||||
{
|
||||
public int RequestsSent { get; private set; }
|
||||
public DateTimeOffset NextRequestAt { get; private set; } = DateTimeOffset.MinValue;
|
||||
public TimeSpan NextRequestAt { get; private set; } = TimeSpan.Zero;
|
||||
|
||||
public bool IsExhausted => RequestsSent >= options.MaximumPunchRequests;
|
||||
|
||||
public bool IsDue(DateTimeOffset now) => now >= NextRequestAt;
|
||||
public bool IsDue(TimeSpan elapsed) => elapsed >= NextRequestAt;
|
||||
|
||||
public void RecordRequest()
|
||||
{
|
||||
@@ -119,6 +223,6 @@ internal sealed class RendezvousPunchRetrySchedule(
|
||||
options.MaximumPunchRetryDelay.TotalMilliseconds);
|
||||
}
|
||||
|
||||
NextRequestAt = clock.UtcNow + TimeSpan.FromMilliseconds(milliseconds);
|
||||
NextRequestAt = clock.Elapsed + TimeSpan.FromMilliseconds(milliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user