Files
SnowcloakServer/MareSynchronosServer/MareSynchronosServer/Hubs/MareHub.cs
Professor Fartsalot 3b932420d3 Fix connection issues once and for all
+ Theory behind this is that on connect, they get a connection ID. On disconnect, that connection id is removed from their redis set. If they have no remaining keys, they're disposed, otherwise we can be sure they've reconnected and should not be disposed.
2025-09-09 14:54:18 -04:00

156 lines
6.6 KiB
C#

using MareSynchronos.API.Data;
using MareSynchronos.API.Data.Enum;
using MareSynchronos.API.Dto;
using MareSynchronos.API.SignalR;
using MareSynchronosServer.Services;
using MareSynchronosServer.Utils;
using MareSynchronosShared;
using MareSynchronosShared.Data;
using MareSynchronosShared.Metrics;
using MareSynchronosShared.Services;
using MareSynchronosShared.Utils.Configuration;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using Microsoft.EntityFrameworkCore;
using StackExchange.Redis.Extensions.Core.Abstractions;
namespace MareSynchronosServer.Hubs;
[Authorize(Policy = "Authenticated")]
public partial class MareHub : Hub<IMareHub>, IMareHub
{
private readonly MareMetrics _mareMetrics;
private readonly SystemInfoService _systemInfoService;
private readonly IHttpContextAccessor _contextAccessor;
private readonly MareHubLogger _logger;
private readonly string _shardName;
private readonly int _maxExistingGroupsByUser;
private readonly int _maxJoinedGroupsByUser;
private readonly int _maxGroupUserCount;
private readonly IRedisDatabase _redis;
private readonly GPoseLobbyDistributionService _gPoseLobbyDistributionService;
private readonly Uri _fileServerAddress;
private readonly Version _expectedClientVersion;
private readonly int _maxCharaDataByUser;
private readonly Lazy<MareDbContext> _dbContextLazy;
private MareDbContext DbContext => _dbContextLazy.Value;
public MareHub(MareMetrics mareMetrics,
IDbContextFactory<MareDbContext> mareDbContextFactory, ILogger<MareHub> logger, SystemInfoService systemInfoService,
IConfigurationService<ServerConfiguration> configuration, IHttpContextAccessor contextAccessor,
IRedisDatabase redisDb, GPoseLobbyDistributionService gPoseLobbyDistributionService)
{
_mareMetrics = mareMetrics;
_systemInfoService = systemInfoService;
_shardName = configuration.GetValue<string>(nameof(ServerConfiguration.ShardName));
_maxExistingGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxExistingGroupsByUser), 3);
_maxJoinedGroupsByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxJoinedGroupsByUser), 6);
_maxGroupUserCount = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxGroupUserCount), 100);
_fileServerAddress = configuration.GetValue<Uri>(nameof(ServerConfiguration.CdnFullUrl));
_expectedClientVersion = configuration.GetValueOrDefault(nameof(ServerConfiguration.ExpectedClientVersion), new Version(0, 0, 0));
_maxCharaDataByUser = configuration.GetValueOrDefault(nameof(ServerConfiguration.MaxCharaDataByUser), 10);
_contextAccessor = contextAccessor;
_redis = redisDb;
_gPoseLobbyDistributionService = gPoseLobbyDistributionService;
_logger = new MareHubLogger(this, logger);
_dbContextLazy = new Lazy<MareDbContext>(() => mareDbContextFactory.CreateDbContext());
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
DbContext.Dispose();
}
base.Dispose(disposing);
}
[Authorize(Policy = "Identified")]
public async Task<ConnectionDto> GetConnectionDto()
{
_logger.LogCallInfo();
_mareMetrics.IncCounter(MetricsAPI.CounterInitializedConnections);
await Clients.Caller.Client_UpdateSystemInfo(_systemInfoService.SystemInfoDto).ConfigureAwait(false);
var dbUser = DbContext.Users.SingleOrDefault(f => f.UID == UserUID);
dbUser.LastLoggedIn = DateTime.UtcNow;
await DbContext.SaveChangesAsync().ConfigureAwait(false);
await Clients.Caller.Client_ReceiveServerMessage(MessageSeverity.Information, "Welcome to Snowcloak! Current Online Users: " + _systemInfoService.SystemInfoDto.OnlineUsers).ConfigureAwait(false);
Context.Items["IsClientConnected"] = true;
return new ConnectionDto(new UserData(dbUser.UID, string.IsNullOrWhiteSpace(dbUser.Alias) ? null : dbUser.Alias))
{
CurrentClientVersion = _expectedClientVersion,
ServerVersion = IMareHub.ApiVersion,
IsAdmin = dbUser.IsAdmin,
IsModerator = dbUser.IsModerator,
ServerInfo = new ServerInfo()
{
MaxGroupsCreatedByUser = _maxExistingGroupsByUser,
ShardName = _shardName,
MaxGroupsJoinedByUser = _maxJoinedGroupsByUser,
MaxGroupUserCount = _maxGroupUserCount,
FileServerAddress = _fileServerAddress,
MaxCharaData = _maxCharaDataByUser
},
};
}
[Authorize(Policy = "Authenticated")]
public async Task<bool> CheckClientHealth()
{
// Key missing -> health check failed!
var exists = await _redis.ExistsAsync("UID:" + UserUID).ConfigureAwait(false);
if (!exists)
{
return false;
}
// Key exists -> health is good!
await UpdateUserOnRedis().ConfigureAwait(false);
return true;
}
[Authorize(Policy = "Authenticated")]
public override async Task OnConnectedAsync()
{
_mareMetrics.IncGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
try
{
_logger.LogCallInfo(MareHubLogger.Args("A client reconnected.", _contextAccessor.GetIpAddress(), UserCharaIdent));
await UpdateUserOnRedis().ConfigureAwait(false);
}
catch { }
await base.OnConnectedAsync().ConfigureAwait(false);
}
[Authorize(Policy = "Authenticated")]
public override async Task OnDisconnectedAsync(Exception exception)
{
_mareMetrics.DecGaugeWithLabels(MetricsAPI.GaugeConnections, labels: Continent);
try
{
_logger.LogCallInfo(MareHubLogger.Args(_contextAccessor.GetIpAddress(), UserCharaIdent));
if (exception != null)
_logger.LogCallWarning(MareHubLogger.Args(_contextAccessor.GetIpAddress(), exception.Message, exception.StackTrace));
await _redis.SetRemoveAsync($"connections:{UserCharaIdent}", Context.ConnectionId).ConfigureAwait(false);
var connections = await _redis.SetMembersAsync<string>($"connections:{UserCharaIdent}").ConfigureAwait(false);
if (connections.Length == 0)
{
await GposeLobbyLeave().ConfigureAwait(false);
await RemoveUserFromRedis().ConfigureAwait(false);
await SendOfflineToAllPairedUsers().ConfigureAwait(false);
}
}
catch { }
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
}
}