Attempt to have a minute grace whenever collection get removed.
This commit is contained in:
@@ -1,36 +1,38 @@
|
||||
using LightlessSync.API.Data;
|
||||
using LightlessSync.API.Data;
|
||||
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
namespace LightlessSync.PlayerData.Pairs;
|
||||
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
/// <summary>
|
||||
/// orchestrates the lifecycle of a paired character
|
||||
/// </summary>
|
||||
public interface IPairHandlerAdapter : IDisposable, IPairPerformanceSubject
|
||||
{
|
||||
new string Ident { get; }
|
||||
bool Initialized { get; }
|
||||
bool IsVisible { get; }
|
||||
bool ScheduledForDeletion { get; set; }
|
||||
CharacterData? LastReceivedCharacterData { get; }
|
||||
long LastAppliedDataBytes { get; }
|
||||
new string? PlayerName { get; }
|
||||
string PlayerNameHash { get; }
|
||||
uint PlayerCharacterId { get; }
|
||||
DateTime? LastDataReceivedAt { get; }
|
||||
DateTime? LastApplyAttemptAt { get; }
|
||||
DateTime? LastSuccessfulApplyAt { get; }
|
||||
string? LastFailureReason { get; }
|
||||
IReadOnlyList<string> LastBlockingConditions { get; }
|
||||
bool IsApplying { get; }
|
||||
bool IsDownloading { get; }
|
||||
int PendingDownloadCount { get; }
|
||||
int ForbiddenDownloadCount { get; }
|
||||
DateTime? InvisibleSinceUtc { get; }
|
||||
DateTime? VisibilityEvictionDueAtUtc { get; }
|
||||
|
||||
void Initialize();
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
void ApplyData(CharacterData data);
|
||||
void ApplyLastReceivedData(bool forced = false);
|
||||
bool FetchPerformanceMetricsFromCache();
|
||||
void LoadCachedCharacterData(CharacterData data);
|
||||
void SetUploading(bool uploading);
|
||||
void SetPaused(bool paused);
|
||||
}
|
||||
|
||||
@@ -194,9 +194,13 @@ public class Pair
|
||||
{
|
||||
var handler = TryGetHandler();
|
||||
if (handler is null)
|
||||
{
|
||||
return PairDebugInfo.Empty;
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
var dueAt = handler.VisibilityEvictionDueAtUtc;
|
||||
var remainingSeconds = dueAt.HasValue
|
||||
? Math.Max(0, (dueAt.Value - now).TotalSeconds)
|
||||
: (double?)null;
|
||||
|
||||
return new PairDebugInfo(
|
||||
true,
|
||||
@@ -206,6 +210,9 @@ public class Pair
|
||||
handler.LastDataReceivedAt,
|
||||
handler.LastApplyAttemptAt,
|
||||
handler.LastSuccessfulApplyAt,
|
||||
handler.InvisibleSinceUtc,
|
||||
handler.VisibilityEvictionDueAtUtc,
|
||||
remainingSeconds,
|
||||
handler.LastFailureReason,
|
||||
handler.LastBlockingConditions,
|
||||
handler.IsApplying,
|
||||
|
||||
@@ -8,6 +8,9 @@ public sealed record PairDebugInfo(
|
||||
DateTime? LastDataReceivedAt,
|
||||
DateTime? LastApplyAttemptAt,
|
||||
DateTime? LastSuccessfulApplyAt,
|
||||
DateTime? InvisibleSinceUtc,
|
||||
DateTime? VisibilityEvictionDueAtUtc,
|
||||
double? VisibilityEvictionRemainingSeconds,
|
||||
string? LastFailureReason,
|
||||
IReadOnlyList<string> BlockingConditions,
|
||||
bool IsApplying,
|
||||
@@ -24,6 +27,9 @@ public sealed record PairDebugInfo(
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
Array.Empty<string>(),
|
||||
false,
|
||||
false,
|
||||
|
||||
@@ -70,7 +70,14 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
private DateTime? _lastSuccessfulApplyAt;
|
||||
private string? _lastFailureReason;
|
||||
private IReadOnlyList<string> _lastBlockingConditions = Array.Empty<string>();
|
||||
private readonly object _visibilityGraceGate = new();
|
||||
private CancellationTokenSource? _visibilityGraceCts;
|
||||
private static readonly TimeSpan VisibilityEvictionGrace = TimeSpan.FromMinutes(1);
|
||||
private DateTime? _invisibleSinceUtc;
|
||||
private DateTime? _visibilityEvictionDueAtUtc;
|
||||
|
||||
public DateTime? InvisibleSinceUtc => _invisibleSinceUtc;
|
||||
public DateTime? VisibilityEvictionDueAtUtc => _visibilityEvictionDueAtUtc;
|
||||
public string Ident { get; }
|
||||
public bool Initialized { get; private set; }
|
||||
public bool ScheduledForDeletion { get; set; }
|
||||
@@ -80,24 +87,37 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
get => _isVisible;
|
||||
private set
|
||||
{
|
||||
if (_isVisible != value)
|
||||
if (_isVisible == value) return;
|
||||
|
||||
_isVisible = value;
|
||||
|
||||
if (!_isVisible)
|
||||
{
|
||||
_isVisible = value;
|
||||
if (!_isVisible)
|
||||
{
|
||||
DisableSync();
|
||||
ResetPenumbraCollection(reason: "VisibilityLost");
|
||||
}
|
||||
else if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
||||
{
|
||||
_ = EnsurePenumbraCollection();
|
||||
}
|
||||
var user = GetPrimaryUserData();
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
||||
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
||||
Mediator.Publish(new RefreshUiMessage());
|
||||
Mediator.Publish(new VisibilityChange());
|
||||
DisableSync();
|
||||
|
||||
_invisibleSinceUtc = DateTime.UtcNow;
|
||||
_visibilityEvictionDueAtUtc = _invisibleSinceUtc.Value.Add(VisibilityEvictionGrace);
|
||||
|
||||
StartVisibilityGraceTask();
|
||||
}
|
||||
else
|
||||
{
|
||||
CancelVisibilityGraceTask();
|
||||
|
||||
_invisibleSinceUtc = null;
|
||||
_visibilityEvictionDueAtUtc = null;
|
||||
|
||||
ScheduledForDeletion = false;
|
||||
|
||||
if (_charaHandler is not null && _charaHandler.Address != nint.Zero)
|
||||
_ = EnsurePenumbraCollection();
|
||||
}
|
||||
|
||||
var user = GetPrimaryUserData();
|
||||
Mediator.Publish(new EventMessage(new Event(PlayerName, user, nameof(PairHandlerAdapter),
|
||||
EventSeverity.Informational, "User Visibility Changed, now: " + (_isVisible ? "Is Visible" : "Is not Visible"))));
|
||||
Mediator.Publish(new RefreshUiMessage());
|
||||
Mediator.Publish(new VisibilityChange());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -918,6 +938,46 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
}
|
||||
|
||||
private void CancelVisibilityGraceTask()
|
||||
{
|
||||
lock (_visibilityGraceGate)
|
||||
{
|
||||
_visibilityGraceCts?.CancelDispose();
|
||||
_visibilityGraceCts = null;
|
||||
}
|
||||
}
|
||||
|
||||
private void StartVisibilityGraceTask()
|
||||
{
|
||||
CancellationToken token;
|
||||
lock (_visibilityGraceGate)
|
||||
{
|
||||
_visibilityGraceCts = _visibilityGraceCts?.CancelRecreate() ?? new CancellationTokenSource();
|
||||
token = _visibilityGraceCts.Token;
|
||||
}
|
||||
|
||||
_visibilityGraceTask = Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
await Task.Delay(VisibilityEvictionGrace, token).ConfigureAwait(false);
|
||||
token.ThrowIfCancellationRequested();
|
||||
if (IsVisible) return;
|
||||
|
||||
ScheduledForDeletion = true;
|
||||
ResetPenumbraCollection(reason: "VisibilityLostTimeout");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
// operation cancelled, do nothing
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogDebug(ex, "Visibility grace task failed for {handler}", GetLogIdentifier());
|
||||
}
|
||||
}, CancellationToken.None);
|
||||
}
|
||||
|
||||
protected override void Dispose(bool disposing)
|
||||
{
|
||||
base.Dispose(disposing);
|
||||
@@ -936,7 +996,10 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
_downloadCancellationTokenSource = null;
|
||||
_downloadManager.Dispose();
|
||||
_charaHandler?.Dispose();
|
||||
CancelVisibilityGraceTask();
|
||||
_charaHandler = null;
|
||||
_invisibleSinceUtc = null;
|
||||
_visibilityEvictionDueAtUtc = null;
|
||||
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
@@ -1265,6 +1328,7 @@ internal sealed class PairHandlerAdapter : DisposableMediatorSubscriberBase, IPa
|
||||
}
|
||||
|
||||
private Task? _pairDownloadTask;
|
||||
private Task _visibilityGraceTask;
|
||||
|
||||
private async Task DownloadAndApplyCharacterAsync(Guid applicationBase, CharacterData charaData, Dictionary<ObjectKind, HashSet<PlayerChanges>> updatedData,
|
||||
bool updateModdedPaths, bool updateManip, Dictionary<(string GamePath, string? Hash), string>? cachedModdedPaths, CancellationToken downloadToken)
|
||||
|
||||
@@ -11,7 +11,9 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly object _pendingGate = new();
|
||||
private readonly object _visibilityGate = new();
|
||||
private readonly Dictionary<string, PairHandlerEntry> _entriesByIdent = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<string, CancellationTokenSource> _pendingInvisibleEvictions = new(StringComparer.Ordinal);
|
||||
private readonly Dictionary<IPairHandlerAdapter, PairHandlerEntry> _entriesByHandler = new(ReferenceEqualityComparer.Instance);
|
||||
|
||||
private readonly IPairHandlerAdapterFactory _handlerFactory;
|
||||
@@ -144,6 +146,37 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
return PairOperationResult<PairUniqueIdentifier>.Ok(registration.PairIdent);
|
||||
}
|
||||
|
||||
private PairOperationResult CancelAllInvisibleEvictions()
|
||||
{
|
||||
List<CancellationTokenSource> snapshot;
|
||||
lock (_visibilityGate)
|
||||
{
|
||||
snapshot = [.. _pendingInvisibleEvictions.Values];
|
||||
_pendingInvisibleEvictions.Clear();
|
||||
}
|
||||
|
||||
List<string>? errors = null;
|
||||
|
||||
foreach (var cts in snapshot)
|
||||
{
|
||||
try { cts.Cancel(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
(errors ??= new List<string>()).Add($"Cancel: {ex.Message}");
|
||||
}
|
||||
|
||||
try { cts.Dispose(); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
(errors ??= new List<string>()).Add($"Dispose: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return errors is null
|
||||
? PairOperationResult.Ok()
|
||||
: PairOperationResult.Fail($"CancelAllInvisibleEvictions had error(s): {string.Join(" | ", errors)}");
|
||||
}
|
||||
|
||||
public PairOperationResult ApplyCharacterData(PairRegistration registration, OnlineUserCharaDataDto dto)
|
||||
{
|
||||
if (registration.CharacterIdent is null)
|
||||
@@ -300,6 +333,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
CancelAllInvisibleEvictions();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
@@ -332,6 +366,7 @@ public sealed class PairHandlerRegistry : IDisposable
|
||||
lock (_gate)
|
||||
{
|
||||
handlers = _entriesByHandler.Keys.ToList();
|
||||
CancelAllInvisibleEvictions();
|
||||
_entriesByIdent.Clear();
|
||||
_entriesByHandler.Clear();
|
||||
}
|
||||
|
||||
@@ -1463,7 +1463,10 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
DrawPairPropertyRow("Has Handler", FormatBool(debugInfo.HasHandler));
|
||||
DrawPairPropertyRow("Handler Initialized", FormatBool(debugInfo.HandlerInitialized));
|
||||
DrawPairPropertyRow("Handler Visible", FormatBool(debugInfo.HandlerVisible));
|
||||
DrawPairPropertyRow("Last Time person rendered in", FormatTimestamp(debugInfo.InvisibleSinceUtc));
|
||||
DrawPairPropertyRow("Handler Timer Temp Collection removal", FormatCountdown(debugInfo.VisibilityEvictionRemainingSeconds));
|
||||
DrawPairPropertyRow("Handler Scheduled For Deletion", FormatBool(debugInfo.HandlerScheduledForDeletion));
|
||||
|
||||
DrawPairPropertyRow("Note", pair.GetNote() ?? "(none)");
|
||||
ImGui.EndTable();
|
||||
}
|
||||
@@ -1698,6 +1701,19 @@ public class SettingsUi : WindowMediatorSubscriberBase
|
||||
return value is null ? "n/a" : value.Value.ToLocalTime().ToString("G", CultureInfo.CurrentCulture);
|
||||
}
|
||||
|
||||
private static string? FormatCountdown(double? remainingSeconds)
|
||||
{
|
||||
if (!remainingSeconds.HasValue)
|
||||
return "No";
|
||||
|
||||
var secs = Math.Max(0, remainingSeconds.Value);
|
||||
var t = TimeSpan.FromSeconds(secs);
|
||||
|
||||
return t.TotalHours >= 1
|
||||
? $"{(int)t.TotalHours:00}:{t.Minutes:00}:{t.Seconds:00}"
|
||||
: $"{(int)t.TotalMinutes:00}:{t.Seconds:00}";
|
||||
}
|
||||
|
||||
private static string FormatBytes(long value) => value < 0 ? "n/a" : UiSharedService.ByteToString(value);
|
||||
|
||||
private static string FormatCharacterId(uint id) => id == uint.MaxValue ? "n/a" : $"{id} (0x{id:X8})";
|
||||
|
||||
Reference in New Issue
Block a user