495 lines
15 KiB
C#
495 lines
15 KiB
C#
using LightlessSync.API.Data.Extensions;
|
|
using LightlessSync.API.Dto.User;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace LightlessSync.PlayerData.Pairs;
|
|
|
|
/// <summary>
|
|
/// creates, tracks, and removes pair handlers
|
|
/// </summary>
|
|
public sealed class PairHandlerRegistry : IDisposable
|
|
{
|
|
private readonly object _gate = new();
|
|
private readonly object _pendingGate = new();
|
|
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
|
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new();
|
|
|
|
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
|
private readonly PairManager _pairManager;
|
|
private readonly PairStateCache _pairStateCache;
|
|
private readonly PairPerformanceMetricsCache _pairPerformanceMetricsCache;
|
|
private readonly ILogger<PairHandlerRegistry> _logger;
|
|
|
|
private readonly TimeSpan _deletionGracePeriod = TimeSpan.FromMinutes(5);
|
|
private static readonly TimeSpan _handlerReadyTimeout = TimeSpan.FromMinutes(3);
|
|
private const int _handlerReadyPollDelayMs = 500;
|
|
private readonly Dictionary<string, CancellationTokenSource> _pendingCharacterData = new(StringComparer.Ordinal);
|
|
|
|
public PairHandlerRegistry(
|
|
IPairHandlerAdapterFactory handlerFactory,
|
|
PairManager pairManager,
|
|
PairStateCache pairStateCache,
|
|
PairPerformanceMetricsCache pairPerformanceMetricsCache,
|
|
ILogger<PairHandlerRegistry> logger)
|
|
{
|
|
_handlerFactory = handlerFactory;
|
|
_pairManager = pairManager;
|
|
_pairStateCache = pairStateCache;
|
|
_pairPerformanceMetricsCache = pairPerformanceMetricsCache;
|
|
_logger = logger;
|
|
}
|
|
|
|
public int GetVisibleUsersCount()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return _entriesByHandler.Keys.Count(handler => handler.IsVisible);
|
|
}
|
|
}
|
|
|
|
public bool IsIdentVisible(string ident)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return _entriesByIdent.TryGetValue(ident, out var entry) && entry.Handler.IsVisible;
|
|
}
|
|
}
|
|
|
|
public PairOperationResult<PairUniqueIdentifier> RegisterOnlinePair(PairRegistration registration)
|
|
{
|
|
if (registration.CharacterIdent is null)
|
|
{
|
|
return PairOperationResult<PairUniqueIdentifier>.Fail($"Registration for {registration.PairIdent.UserId} missing ident.");
|
|
}
|
|
|
|
IPairHandlerAdapter handler;
|
|
lock (_gate)
|
|
{
|
|
var entry = GetOrCreateEntry(registration.CharacterIdent);
|
|
handler = entry.Handler;
|
|
handler.ScheduledForDeletion = false;
|
|
entry.AddPair(registration.PairIdent);
|
|
}
|
|
|
|
ApplyPauseStateForHandler(handler);
|
|
|
|
if (handler.LastReceivedCharacterData is null)
|
|
{
|
|
var cachedData = _pairStateCache.TryLoad(registration.CharacterIdent);
|
|
if (cachedData is not null)
|
|
{
|
|
handler.LoadCachedCharacterData(cachedData);
|
|
}
|
|
}
|
|
|
|
if (handler.LastReceivedCharacterData is not null &&
|
|
(handler.LastAppliedApproximateVRAMBytes < 0 || handler.LastAppliedDataTris < 0))
|
|
{
|
|
handler.ApplyLastReceivedData(forced: true);
|
|
}
|
|
|
|
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
|
}
|
|
|
|
public PairOperationResult<PairUniqueIdentifier> DeregisterOfflinePair(PairRegistration registration, bool forceDisposal = false)
|
|
{
|
|
if (registration.CharacterIdent is null)
|
|
{
|
|
return PairOperationResult<PairUniqueIdentifier>.Fail($"Deregister for {registration.PairIdent.UserId} missing ident.");
|
|
}
|
|
|
|
IPairHandlerAdapter? handler = null;
|
|
bool shouldScheduleRemoval = false;
|
|
bool shouldDisposeImmediately = false;
|
|
|
|
lock (_gate)
|
|
{
|
|
if (!_entriesByIdent.TryGetValue(registration.CharacterIdent, out var entry))
|
|
{
|
|
return PairOperationResult<PairUniqueIdentifier>.Fail($"Ident {registration.CharacterIdent} not registered.");
|
|
}
|
|
|
|
handler = entry.Handler;
|
|
entry.RemovePair(registration.PairIdent);
|
|
if (entry.PairCount == 0)
|
|
{
|
|
if (forceDisposal)
|
|
{
|
|
shouldDisposeImmediately = true;
|
|
}
|
|
else
|
|
{
|
|
shouldScheduleRemoval = true;
|
|
handler.ScheduledForDeletion = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (shouldDisposeImmediately && handler is not null)
|
|
{
|
|
if (TryFinalizeHandlerRemoval(handler))
|
|
{
|
|
handler.Dispose();
|
|
}
|
|
}
|
|
else if (shouldScheduleRemoval && handler is not null)
|
|
{
|
|
_ = RemoveAfterGracePeriodAsync(handler);
|
|
}
|
|
|
|
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
|
}
|
|
|
|
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
|
{
|
|
if (registration.CharacterIdent is null)
|
|
{
|
|
return PairOperationResult.Fail($"Character data received without ident for {registration.PairIdent.UserId}.");
|
|
}
|
|
|
|
if (!TryGetHandler(registration.CharacterIdent, out var handler) || handler is null)
|
|
{
|
|
var registerResult = RegisterOnlinePair(registration);
|
|
if (!registerResult.Success)
|
|
{
|
|
return PairOperationResult.Fail(registerResult.Error);
|
|
}
|
|
|
|
if (!TryGetHandler(registration.CharacterIdent, out handler) || handler is null)
|
|
{
|
|
QueuePendingCharacterData(registration, dto);
|
|
return PairOperationResult.Ok();
|
|
}
|
|
}
|
|
|
|
handler.ApplyData(dto.CharaData);
|
|
return PairOperationResult.Ok();
|
|
}
|
|
|
|
public PairOperationResult ApplyLastReceivedData(PairUniqueIdentifier pairIdent, string ident, bool forced = false)
|
|
{
|
|
if (!TryGetHandler(ident, out var handler) || handler is null)
|
|
{
|
|
return PairOperationResult.Fail($"Cannot reapply data: handler for {pairIdent.UserId} not found.");
|
|
}
|
|
|
|
handler.ApplyLastReceivedData(forced);
|
|
return PairOperationResult.Ok();
|
|
}
|
|
|
|
public PairOperationResult SetUploading(PairUniqueIdentifier pairIdent, string ident, bool uploading)
|
|
{
|
|
if (!TryGetHandler(ident, out var handler) || handler is null)
|
|
{
|
|
return PairOperationResult.Fail($"Cannot set uploading for {pairIdent.UserId}: handler not found.");
|
|
}
|
|
|
|
handler.SetUploading(uploading);
|
|
return PairOperationResult.Ok();
|
|
}
|
|
|
|
public PairOperationResult SetPausedState(PairUniqueIdentifier pairIdent, string ident, bool paused)
|
|
{
|
|
if (!TryGetHandler(ident, out var handler) || handler is null)
|
|
{
|
|
return PairOperationResult.Fail($"Cannot update pause state for {pairIdent.UserId}: handler not found.");
|
|
}
|
|
|
|
_ = paused; // value reflected in pair manager already
|
|
ApplyPauseStateForHandler(handler);
|
|
return PairOperationResult.Ok();
|
|
}
|
|
|
|
public PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>> GetPairConnections(string ident)
|
|
{
|
|
PairHandlerEntry? entry;
|
|
lock (_gate)
|
|
{
|
|
_entriesByIdent.TryGetValue(ident, out entry);
|
|
}
|
|
|
|
if (entry is null)
|
|
{
|
|
return PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>>.Fail($"No handler registered for {ident}.");
|
|
}
|
|
|
|
var list = new List<(PairUniqueIdentifier, PairConnection)>();
|
|
foreach (var pairIdent in entry.SnapshotPairs())
|
|
{
|
|
var result = _pairManager.GetPair(pairIdent.UserId);
|
|
if (result.Success)
|
|
{
|
|
list.Add((pairIdent, result.Value));
|
|
}
|
|
}
|
|
|
|
return PairOperationResult<IReadOnlyList<(PairUniqueIdentifier Ident, PairConnection Pair)>>.Ok(list);
|
|
}
|
|
|
|
private void ApplyPauseStateForHandler(IPairHandlerAdapter handler)
|
|
{
|
|
var pairs = _pairManager.GetPairsByIdent(handler.Ident);
|
|
bool paused = pairs.Any(p => p.SelfToOtherPermissions.IsPaused() || p.OtherToSelfPermissions.IsPaused());
|
|
handler.SetPaused(paused);
|
|
}
|
|
|
|
internal bool TryGetHandler(string ident, out IPairHandlerAdapter? handler)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
var success = _entriesByIdent.TryGetValue(ident, out var entry);
|
|
handler = entry?.Handler;
|
|
return success;
|
|
}
|
|
}
|
|
|
|
internal IReadOnlyList<IPairHandlerAdapter> GetHandlerSnapshot()
|
|
{
|
|
lock (_gate)
|
|
{
|
|
return _entriesByHandler.Keys.ToList();
|
|
}
|
|
}
|
|
|
|
internal IReadOnlyCollection<PairUniqueIdentifier> GetRegisteredPairs(IPairHandlerAdapter handler)
|
|
{
|
|
lock (_gate)
|
|
{
|
|
if (_entriesByHandler.TryGetValue(handler, out var entry))
|
|
{
|
|
return entry.SnapshotPairs();
|
|
}
|
|
}
|
|
|
|
return Array.Empty<PairUniqueIdentifier>();
|
|
}
|
|
|
|
internal void ReapplyAll(bool forced = false)
|
|
{
|
|
var handlers = GetHandlerSnapshot();
|
|
foreach (var handler in handlers)
|
|
{
|
|
try
|
|
{
|
|
handler.ApplyLastReceivedData(forced);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
_logger.LogDebug(ex, "Failed to reapply cached data for {Ident}", handler.Ident);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
internal void ResetAllHandlers()
|
|
{
|
|
List<IPairHandlerAdapter> handlers;
|
|
lock (_gate)
|
|
{
|
|
handlers = _entriesByHandler.Keys.ToList();
|
|
_entriesByIdent.Clear();
|
|
_entriesByHandler.Clear();
|
|
}
|
|
|
|
CancelAllPendingCharacterData();
|
|
|
|
foreach (var handler in handlers)
|
|
{
|
|
try
|
|
{
|
|
handler.Dispose();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
if (_logger.IsEnabled(LogLevel.Debug))
|
|
{
|
|
_logger.LogDebug(ex, "Failed to dispose handler for {Ident}", handler.Ident);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
List<IPairHandlerAdapter> handlers;
|
|
lock (_gate)
|
|
{
|
|
handlers = _entriesByHandler.Keys.ToList();
|
|
_entriesByIdent.Clear();
|
|
_entriesByHandler.Clear();
|
|
}
|
|
|
|
CancelAllPendingCharacterData();
|
|
|
|
foreach (var handler in handlers)
|
|
{
|
|
handler.Dispose();
|
|
_pairPerformanceMetricsCache.Clear(handler.Ident);
|
|
}
|
|
}
|
|
|
|
private PairHandlerEntry GetOrCreateEntry(string ident)
|
|
{
|
|
if (_entriesByIdent.TryGetValue(ident, out var entry))
|
|
{
|
|
return entry;
|
|
}
|
|
|
|
var handler = _handlerFactory.Create(ident);
|
|
entry = new PairHandlerEntry(ident, handler);
|
|
_entriesByIdent[ident] = entry;
|
|
_entriesByHandler[handler] = entry;
|
|
return entry;
|
|
}
|
|
|
|
private async Task RemoveAfterGracePeriodAsync(IPairHandlerAdapter handler)
|
|
{
|
|
await Task.Delay(_deletionGracePeriod).ConfigureAwait(false);
|
|
|
|
if (TryFinalizeHandlerRemoval(handler))
|
|
{
|
|
handler.Dispose();
|
|
}
|
|
}
|
|
|
|
private bool TryFinalizeHandlerRemoval(IPairHandlerAdapter handler)
|
|
{
|
|
string? ident = null;
|
|
lock (_gate)
|
|
{
|
|
if (!_entriesByHandler.TryGetValue(handler, out var entry) || entry.HasPairs)
|
|
{
|
|
handler.ScheduledForDeletion = false;
|
|
return false;
|
|
}
|
|
|
|
ident = entry.Ident;
|
|
_entriesByHandler.Remove(handler);
|
|
_entriesByIdent.Remove(entry.Ident);
|
|
}
|
|
|
|
if (ident is not null)
|
|
{
|
|
_pairPerformanceMetricsCache.Clear(ident);
|
|
CancelPendingCharacterData(ident);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
private void QueuePendingCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
|
{
|
|
if (registration.CharacterIdent is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
CancellationTokenSource? previous = null;
|
|
CancellationTokenSource cts;
|
|
lock (_pendingGate)
|
|
{
|
|
if (_pendingCharacterData.TryGetValue(registration.CharacterIdent, out previous))
|
|
{
|
|
previous.Cancel();
|
|
}
|
|
|
|
cts = new CancellationTokenSource();
|
|
_pendingCharacterData[registration.CharacterIdent] = cts;
|
|
}
|
|
|
|
previous?.Dispose();
|
|
cts.CancelAfter(_handlerReadyTimeout);
|
|
_ = Task.Run(() => WaitThenApplyPendingCharacterDataAsync(registration, dto, cts.Token, cts));
|
|
}
|
|
|
|
private void CancelPendingCharacterData(string ident)
|
|
{
|
|
CancellationTokenSource? cts = null;
|
|
lock (_pendingGate)
|
|
{
|
|
if (_pendingCharacterData.TryGetValue(ident, out cts))
|
|
{
|
|
_pendingCharacterData.Remove(ident);
|
|
}
|
|
}
|
|
|
|
if (cts is not null)
|
|
{
|
|
cts.Cancel();
|
|
cts.Dispose();
|
|
}
|
|
}
|
|
|
|
private void CancelAllPendingCharacterData()
|
|
{
|
|
List<CancellationTokenSource>? snapshot = null;
|
|
lock (_pendingGate)
|
|
{
|
|
if (_pendingCharacterData.Count > 0)
|
|
{
|
|
snapshot = _pendingCharacterData.Values.ToList();
|
|
_pendingCharacterData.Clear();
|
|
}
|
|
}
|
|
|
|
if (snapshot is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var cts in snapshot)
|
|
{
|
|
cts.Cancel();
|
|
cts.Dispose();
|
|
}
|
|
}
|
|
|
|
private async Task WaitThenApplyPendingCharacterDataAsync(
|
|
PairRegistration registration,
|
|
OnlineUserCharaDataDto dto,
|
|
CancellationToken token,
|
|
CancellationTokenSource source)
|
|
{
|
|
if (registration.CharacterIdent is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
if (TryGetHandler(registration.CharacterIdent, out var handler) && handler is not null && handler.Initialized)
|
|
{
|
|
handler.ApplyData(dto.CharaData);
|
|
break;
|
|
}
|
|
|
|
await Task.Delay(_handlerReadyPollDelayMs, token).ConfigureAwait(false);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// expected
|
|
}
|
|
finally
|
|
{
|
|
lock (_pendingGate)
|
|
{
|
|
if (_pendingCharacterData.TryGetValue(registration.CharacterIdent, out var current) && ReferenceEquals(current, source))
|
|
{
|
|
_pendingCharacterData.Remove(registration.CharacterIdent);
|
|
}
|
|
}
|
|
|
|
source.Dispose();
|
|
}
|
|
}
|
|
}
|