feat(client): standardize connection outcomes (#13)
quality-gate / quality (push) Successful in 59s

This commit is contained in:
KyuubiYoru
2026-07-16 10:18:41 +02:00
parent b4b6072fe1
commit 94aba8a3bb
47 changed files with 2727 additions and 178 deletions
@@ -15,6 +15,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
private readonly Dictionary<MediationHandle, SessionListingId> _presenceHandles = [];
private readonly Dictionary<MediationHandle, PresenceEntry> _presence = [];
private readonly Dictionary<JoinAttemptId, AttemptEntry> _attempts = [];
private readonly Dictionary<JoinAttemptId, OutcomeReportEntry> _outcomeReports = [];
private readonly Dictionary<MediationHandle, JoinAttemptId> _attemptHandles = [];
private readonly Dictionary<string, IdempotencyEntry> _idempotency = new(StringComparer.Ordinal);
private readonly Dictionary<string, TimeSpan> _replay = new(StringComparer.Ordinal);
@@ -183,7 +184,9 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
|| command.MaximumPlayers is <= 0 or > ContractLimits.SessionCapacityMaxPlayers
|| command.CurrentPlayers < 0
|| command.CurrentPlayers > command.MaximumPlayers
|| !ContractValidation.IsMetadataValid(command.Metadata))
|| !ContractValidation.IsMetadataValid(command.Metadata)
|| command.DedicatedFallback is not null
&& !ContractValidation.IsNetworkEndpointValid(command.DedicatedFallback))
{
throw new ArgumentException("Listing update invariants are invalid.", nameof(command));
}
@@ -213,6 +216,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
CurrentPlayers = command.CurrentPlayers,
MaximumPlayers = command.MaximumPlayers,
Metadata = command.Metadata,
DedicatedFallback = command.DedicatedFallback,
});
entry.Version++;
return new(StoreResultCode.Success, Snapshot(entry));
@@ -362,14 +366,26 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
}
if (!_listings.TryGetValue(command.ListingId, out ListingEntry? listing)
|| listing.Definition.Scope != command.Scope
|| listing.Definition.ProtocolVersion != command.ProtocolVersion
|| !_presence.ContainsKey(listing.Definition.HostPresenceHandle))
|| listing.Definition.Scope != command.Scope)
{
return new(StoreResultCode.NotFound);
}
if (listing.Definition.ProtocolVersion != command.ProtocolVersion)
{
return new(StoreResultCode.IncompatibleProtocol);
}
if (!_presence.ContainsKey(listing.Definition.HostPresenceHandle))
{
return new(StoreResultCode.StaleHost);
}
command = command with
{
DedicatedFallback = StoredListing.CopyEndpoint(listing.Definition.DedicatedFallback),
};
if (_attempts.Count >= _options.MaxJoinAttempts
|| _outcomeReports.Count >= _options.MaxOutcomeReports
|| _idempotency.Count >= _options.MaxIdempotencyEntries
|| _attempts.Values.Count(entry => entry.Command.Scope == command.Scope)
>= command.ScopeAttemptLimit)
@@ -387,6 +403,11 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
now + _options.JoinAttemptLifetime,
WallDeadline(now, _options.JoinAttemptLifetime));
_attempts.Add(command.AttemptId, attempt);
_outcomeReports.Add(command.AttemptId, new(
command.ListingId,
command.ClientSubject,
command.ClientCapabilityFingerprint,
now + _options.JoinAttemptLifetime + _options.IdempotencyLifetime));
_attemptHandles.Add(command.MediationHandle, command.AttemptId);
_idempotency.Add(idempotencyKey, new(
command.RequestFingerprint,
@@ -460,6 +481,42 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
return new(StoreResultCode.Success, true);
}, cancellationToken);
public StoreResult<StoredConnectionOutcome> ReportConnectionOutcome(
ReportConnectionOutcomeCommand command,
CancellationToken cancellationToken = default) => Atomic<StoredConnectionOutcome>(_ =>
{
ArgumentNullException.ThrowIfNull(command);
if (command.AttemptId.Value == Guid.Empty
|| !command.ClientCapabilityFingerprint.IsValid
|| !ContractValidation.IsReportableConnectionOutcome(command.Outcome)
|| !Enum.IsDefined(command.ElapsedBucket))
{
throw new ArgumentException("Connection outcome invariants are invalid.", nameof(command));
}
if (!_available)
{
return new(StoreResultCode.ServiceUnavailable);
}
if (!_outcomeReports.TryGetValue(command.AttemptId, out OutcomeReportEntry? entry)
|| entry.ClientCapabilityFingerprint != command.ClientCapabilityFingerprint)
{
return new(StoreResultCode.NotFound);
}
StoredConnectionOutcome reported = new(command.Outcome, command.ElapsedBucket);
if (entry.Outcome is not null)
{
return entry.Outcome == reported
? new(StoreResultCode.Success, entry.Outcome, true)
: new(StoreResultCode.ReplayRejected);
}
entry.Outcome = reported;
return new(StoreResultCode.Success, reported);
}, cancellationToken);
public StoreResult<StoredJoinAttempt> BindAttemptEndpoint(
BindAttemptEndpointCommand command,
CancellationToken cancellationToken = default) => Atomic<StoredJoinAttempt>(now =>
@@ -681,6 +738,10 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
.Where(item => string.Equals(item.Value.Command.ClientSubject, subject, StringComparison.Ordinal))
.Select(static item => item.Key)
.ToArray();
JoinAttemptId[] outcomeReports = _outcomeReports
.Where(item => string.Equals(item.Value.ClientSubject, subject, StringComparison.Ordinal))
.Select(static item => item.Key)
.ToArray();
foreach (SessionListingId listingId in listings)
{
RemoveListing(listingId);
@@ -690,6 +751,10 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
{
RemoveAttempt(attemptId);
}
foreach (JoinAttemptId attemptId in outcomeReports)
{
_outcomeReports.Remove(attemptId);
}
return new(StoreResultCode.Success, listings.Length + attempts.Length);
}, cancellationToken);
@@ -799,6 +864,14 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
RemoveAttempt(attemptId);
}
foreach (JoinAttemptId attemptId in _outcomeReports
.Where(item => item.Value.Deadline <= now)
.Select(static item => item.Key)
.ToArray())
{
_outcomeReports.Remove(attemptId);
}
foreach (SessionListingId listingId in _listings
.Where(item => item.Value.LeaseDeadline <= now)
.Select(static item => item.Key)
@@ -815,6 +888,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
_presenceHandles.Clear();
_presence.Clear();
_attempts.Clear();
_outcomeReports.Clear();
_attemptHandles.Clear();
_idempotency.Clear();
_replay.Clear();
@@ -837,6 +911,15 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
{
RemoveAttempt(attemptId);
}
foreach (JoinAttemptId attemptId in _outcomeReports
.Where(item => item.Value.ListingId == listingId)
.Select(static item => item.Key)
.ToArray())
{
_outcomeReports.Remove(attemptId);
}
}
private void RemoveAttempt(JoinAttemptId attemptId)
@@ -875,6 +958,7 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
HostCapabilityFingerprint = entry.Command.HostCapabilityFingerprint,
ClientCapabilityFingerprint = entry.Command.ClientCapabilityFingerprint,
ConnectionTicketFingerprint = entry.Command.ConnectionTicketFingerprint,
DedicatedFallback = StoredListing.CopyEndpoint(entry.Command.DedicatedFallback),
ExpiresAt = entry.WallExpiresAt,
ConnectionTicketExpiresAt = entry.TicketWallExpiresAt ?? default,
HostEndpoint = entry.HostEndpoint,
@@ -914,6 +998,8 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
|| listing.CurrentPlayers < 0
|| listing.CurrentPlayers > listing.MaximumPlayers
|| !ContractValidation.IsMetadataValid(listing.Metadata)
|| listing.DedicatedFallback is not null
&& !ContractValidation.IsNetworkEndpointValid(listing.DedicatedFallback)
|| !listing.LeaseFingerprint.IsValid
|| !listing.HostPresenceFingerprint.IsValid
|| !IsDerivationSaltValid(listing.CapabilityDerivationSalt))
@@ -954,6 +1040,8 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
|| !command.HostCapabilityFingerprint.IsValid
|| !command.ClientCapabilityFingerprint.IsValid
|| !command.ConnectionTicketFingerprint.IsValid
|| command.DedicatedFallback is not null
&& !ContractValidation.IsNetworkEndpointValid(command.DedicatedFallback)
|| !IsDerivationSaltValid(command.CapabilityDerivationSalt)
|| command.ScopeAttemptLimit <= 0)
{
@@ -1031,6 +1119,19 @@ internal sealed class InMemoryEphemeralRendezvousStore : IEphemeralRendezvousSto
public bool IsCancelled { get; set; }
}
private sealed class OutcomeReportEntry(
SessionListingId listingId,
string clientSubject,
SecretFingerprint clientCapabilityFingerprint,
TimeSpan deadline)
{
public SessionListingId ListingId { get; } = listingId;
public string ClientSubject { get; } = clientSubject;
public SecretFingerprint ClientCapabilityFingerprint { get; } = clientCapabilityFingerprint;
public TimeSpan Deadline { get; } = deadline;
public StoredConnectionOutcome? Outcome { get; set; }
}
private sealed record IdempotencyEntry(
string RequestFingerprint,
object ResourceId,