mirror of
https://github.com/DorkSquadRR/DorkNet.git
synced 2026-10-25 15:08:47 -07:00
Add JoinTimeoutService to manage pending room joins and handle timeouts
This commit is contained in:
@@ -36,6 +36,7 @@ public class GoToController(
|
||||
RoomService rooms,
|
||||
DorkNet.Server.Data.DorkNetDbContext db,
|
||||
NotificationService notifications,
|
||||
JoinTimeoutService joinTimeouts,
|
||||
ILogger<GoToController> logger) : ControllerBase
|
||||
{
|
||||
[HttpPost("/goto/room/{roomName}")]
|
||||
@@ -687,8 +688,10 @@ public class GoToController(
|
||||
private async Task RecordResponseAsync(MatchmakingResponseDto response)
|
||||
{
|
||||
if (this.CurrentPlayerId() is not long playerId) return;
|
||||
if (response.RoomInstance is null) return;
|
||||
|
||||
presence.SetRoom(playerId, response.RoomInstance);
|
||||
joinTimeouts.MarkPending(playerId, response.RoomInstance);
|
||||
|
||||
// Real visit tracking: upsert the per-(room, player) row so
|
||||
// we get distinct VisitorCount (unique players ever) AND the
|
||||
|
||||
@@ -20,6 +20,7 @@ public class MatchController(
|
||||
GameSessionService sessionService,
|
||||
PlayerPresenceService presence,
|
||||
PrivateInstanceService privateInstances,
|
||||
JoinTimeoutService joinTimeouts,
|
||||
DorkNetDbContext db,
|
||||
ILogger<MatchController> logger) : ControllerBase
|
||||
{
|
||||
@@ -33,9 +34,11 @@ public class MatchController(
|
||||
/// response body and just needs 2xx (Matchmaking.txt:6668-6699), so
|
||||
/// we mostly use this for visibility into Photon-side issues —
|
||||
/// without it, all we see in the catch-all is an opaque "200, empty
|
||||
/// reply" and no clue why a player crashed back to dorm.</summary>
|
||||
/// reply" and no clue why a player crashed back to dorm. Failure
|
||||
/// reports also clear the pending /goto room and push a ModerationKick
|
||||
/// so the watch leaves the "Joining room" overlay instead of softlocking.</summary>
|
||||
[HttpPost("/roominstance/{instanceId:long}/reportjoinresult")]
|
||||
public IActionResult ReportJoinResult(long instanceId, [FromForm(Name = "result")] int? result)
|
||||
public async Task<IActionResult> ReportJoinResult(long instanceId, [FromForm(Name = "result")] int? result)
|
||||
{
|
||||
var code = result ?? -1;
|
||||
var label = code switch
|
||||
@@ -49,12 +52,14 @@ public class MatchController(
|
||||
var pid = this.RequireCurrentPlayerId();
|
||||
if (code == 0)
|
||||
{
|
||||
joinTimeouts.MarkCompleted(pid, instanceId, label);
|
||||
logger.LogInformation(
|
||||
"[joinresult] player={Player} instance={Instance} OK ({Label})",
|
||||
pid, instanceId, label);
|
||||
}
|
||||
else
|
||||
{
|
||||
await joinTimeouts.MarkFailedAsync(pid, instanceId, label);
|
||||
logger.LogWarning(
|
||||
"[joinresult] player={Player} instance={Instance} FAILED code={Code} ({Label}) — Photon-side error, check Photon AppId/region/auth",
|
||||
pid, instanceId, code, label);
|
||||
|
||||
@@ -0,0 +1,142 @@
|
||||
using System.Collections.Concurrent;
|
||||
using DorkNet.Server.Controllers.Match;
|
||||
|
||||
namespace DorkNet.Server.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Tracks pending /goto joins so a failed Photon join cannot leave the
|
||||
/// 2020 watch stuck forever on the "Joining room" overlay.
|
||||
/// </summary>
|
||||
public sealed class JoinTimeoutService(
|
||||
PlayerPresenceService presence,
|
||||
NotificationService notifications,
|
||||
ILogger<JoinTimeoutService> logger)
|
||||
{
|
||||
private static readonly TimeSpan Timeout = TimeSpan.FromSeconds(30);
|
||||
private readonly ConcurrentDictionary<long, PendingJoin> _pending = new();
|
||||
|
||||
public void MarkPending(long playerId, RoomInstanceDto? room)
|
||||
{
|
||||
if (room is null) return;
|
||||
|
||||
if (IsDorm(room))
|
||||
{
|
||||
ClearForPlayer(playerId, "dorm");
|
||||
return;
|
||||
}
|
||||
|
||||
ClearForPlayer(playerId, "replaced");
|
||||
|
||||
var pending = new PendingJoin(
|
||||
room.RoomInstanceId,
|
||||
room.RoomId,
|
||||
room.Name ?? string.Empty,
|
||||
new CancellationTokenSource());
|
||||
|
||||
_pending[playerId] = pending;
|
||||
logger.LogInformation(
|
||||
"[join-watchdog] pending player={PlayerId} room={RoomId}/{RoomName} instance={InstanceId} timeout={TimeoutSeconds}s",
|
||||
playerId, room.RoomId, room.Name, room.RoomInstanceId, Timeout.TotalSeconds);
|
||||
|
||||
_ = WatchAsync(playerId, pending);
|
||||
}
|
||||
|
||||
public void MarkCompleted(long playerId, long instanceId, string outcome)
|
||||
{
|
||||
if (!_pending.TryGetValue(playerId, out var pending) || pending.InstanceId != instanceId)
|
||||
return;
|
||||
|
||||
if (!RemovePending(playerId, pending))
|
||||
return;
|
||||
|
||||
pending.Cancellation.Cancel();
|
||||
logger.LogInformation(
|
||||
"[join-watchdog] completed player={PlayerId} instance={InstanceId} outcome={Outcome}",
|
||||
playerId, instanceId, outcome);
|
||||
}
|
||||
|
||||
public async Task MarkFailedAsync(long playerId, long instanceId, string outcome)
|
||||
{
|
||||
MarkCompleted(playerId, instanceId, outcome);
|
||||
|
||||
var current = presence.GetRoom(playerId);
|
||||
if (current is null || current.RoomInstanceId != instanceId)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"[join-watchdog] failure ignored player={PlayerId} instance={InstanceId} current={CurrentInstance}",
|
||||
playerId, instanceId, current?.RoomInstanceId);
|
||||
return;
|
||||
}
|
||||
|
||||
presence.Clear(playerId);
|
||||
await notifications.KickPlayerAsync(playerId, "Room join failed. Returning to dorm.");
|
||||
logger.LogWarning(
|
||||
"[join-watchdog] kicked failed join player={PlayerId} room={RoomId}/{RoomName} instance={InstanceId} outcome={Outcome}",
|
||||
playerId, current.RoomId, current.Name, instanceId, outcome);
|
||||
}
|
||||
|
||||
private async Task WatchAsync(long playerId, PendingJoin pending)
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(Timeout, pending.Cancellation.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.LogWarning(ex, "[join-watchdog] delay failed player={PlayerId}", playerId);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!RemovePending(playerId, pending))
|
||||
return;
|
||||
|
||||
var current = presence.GetRoom(playerId);
|
||||
if (current is null || current.RoomInstanceId != pending.InstanceId)
|
||||
{
|
||||
logger.LogInformation(
|
||||
"[join-watchdog] timeout skipped player={PlayerId} pending={PendingInstance} current={CurrentInstance}",
|
||||
playerId, pending.InstanceId, current?.RoomInstanceId);
|
||||
return;
|
||||
}
|
||||
|
||||
presence.Clear(playerId);
|
||||
await notifications.KickPlayerAsync(playerId, "Room join timed out. Returning to dorm.");
|
||||
logger.LogWarning(
|
||||
"[join-watchdog] timed out player={PlayerId} room={RoomId}/{RoomName} instance={InstanceId}",
|
||||
playerId, pending.RoomId, pending.RoomName, pending.InstanceId);
|
||||
}
|
||||
|
||||
private void ClearForPlayer(long playerId, string reason)
|
||||
{
|
||||
while (_pending.TryGetValue(playerId, out var pending))
|
||||
{
|
||||
if (!RemovePending(playerId, pending))
|
||||
continue;
|
||||
|
||||
pending.Cancellation.Cancel();
|
||||
logger.LogDebug(
|
||||
"[join-watchdog] cleared player={PlayerId} instance={InstanceId} reason={Reason}",
|
||||
playerId, pending.InstanceId, reason);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private bool RemovePending(long playerId, PendingJoin pending) =>
|
||||
((ICollection<KeyValuePair<long, PendingJoin>>)_pending)
|
||||
.Remove(new KeyValuePair<long, PendingJoin>(playerId, pending));
|
||||
|
||||
private static bool IsDorm(RoomInstanceDto room) =>
|
||||
room.RoomId == 1 ||
|
||||
string.Equals(room.Name, "DormRoom", StringComparison.OrdinalIgnoreCase) ||
|
||||
(room.PhotonRoomId?.StartsWith("^dormroom_", StringComparison.OrdinalIgnoreCase) ?? false);
|
||||
|
||||
private sealed record PendingJoin(
|
||||
long InstanceId,
|
||||
long RoomId,
|
||||
string RoomName,
|
||||
CancellationTokenSource Cancellation);
|
||||
}
|
||||
@@ -148,6 +148,7 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<NotificationService>();
|
||||
services.AddSingleton<OrphanAccountTracker>();
|
||||
services.AddSingleton<PlayerPresenceService>();
|
||||
services.AddSingleton<JoinTimeoutService>();
|
||||
services.AddSingleton<OnlinePresenceService>();
|
||||
services.AddSingleton<PlayerLogService>();
|
||||
// S3-compatible object storage (Garage in production, MinIO/disk in dev)
|
||||
|
||||
Reference in New Issue
Block a user