Rewrite callbacks handling loop to new mechanism

This commit is contained in:
Łukasz Domeradzki 2024-08-06 12:02:38 +02:00
parent 67d9486495
commit 0c3c4c08ea
No known key found for this signature in database
GPG key ID: 6B138B4C64555AEA
3 changed files with 72 additions and 24 deletions

View file

@ -172,7 +172,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
internal bool HasLoginCodeReady => !string.IsNullOrEmpty(TwoFactorCode) || !string.IsNullOrEmpty(AuthCode); internal bool HasLoginCodeReady => !string.IsNullOrEmpty(TwoFactorCode) || !string.IsNullOrEmpty(AuthCode);
private readonly CallbackManager CallbackManager; private readonly CallbackManager CallbackManager;
private readonly SemaphoreSlim CallbackSemaphore = new(1, 1);
private readonly SemaphoreSlim GamesRedeemerInBackgroundSemaphore = new(1, 1); private readonly SemaphoreSlim GamesRedeemerInBackgroundSemaphore = new(1, 1);
private readonly Timer HeartBeatTimer; private readonly Timer HeartBeatTimer;
private readonly SemaphoreSlim InitializationSemaphore = new(1, 1); private readonly SemaphoreSlim InitializationSemaphore = new(1, 1);
@ -295,8 +294,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
private DateTime? AccessTokenValidUntil; private DateTime? AccessTokenValidUntil;
private string? AuthCode; private string? AuthCode;
private string? BackingAccessToken; private string? BackingAccessToken;
private CancellationTokenSource? CallbacksAborted;
private Timer? ConnectionFailureTimer; private Timer? ConnectionFailureTimer;
private bool FirstTradeSent; private bool FirstTradeSent;
private Timer? GamesRedeemerInBackgroundTimer; private Timer? GamesRedeemerInBackgroundTimer;
@ -410,7 +409,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
// Those are objects that are always being created if constructor doesn't throw exception // Those are objects that are always being created if constructor doesn't throw exception
ArchiWebHandler.Dispose(); ArchiWebHandler.Dispose();
BotDatabase.Dispose(); BotDatabase.Dispose();
CallbackSemaphore.Dispose();
GamesRedeemerInBackgroundSemaphore.Dispose(); GamesRedeemerInBackgroundSemaphore.Dispose();
InitializationSemaphore.Dispose(); InitializationSemaphore.Dispose();
MessagingSemaphore.Dispose(); MessagingSemaphore.Dispose();
@ -423,6 +421,8 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
HeartBeatTimer.Dispose(); HeartBeatTimer.Dispose();
// Those are objects that might be null and the check should be in-place // Those are objects that might be null and the check should be in-place
CallbacksAborted?.Cancel();
CallbacksAborted?.Dispose();
ConnectionFailureTimer?.Dispose(); ConnectionFailureTimer?.Dispose();
GamesRedeemerInBackgroundTimer?.Dispose(); GamesRedeemerInBackgroundTimer?.Dispose();
PlayingWasBlockedTimer?.Dispose(); PlayingWasBlockedTimer?.Dispose();
@ -436,7 +436,6 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
// Those are objects that are always being created if constructor doesn't throw exception // Those are objects that are always being created if constructor doesn't throw exception
ArchiWebHandler.Dispose(); ArchiWebHandler.Dispose();
BotDatabase.Dispose(); BotDatabase.Dispose();
CallbackSemaphore.Dispose();
GamesRedeemerInBackgroundSemaphore.Dispose(); GamesRedeemerInBackgroundSemaphore.Dispose();
InitializationSemaphore.Dispose(); InitializationSemaphore.Dispose();
MessagingSemaphore.Dispose(); MessagingSemaphore.Dispose();
@ -449,6 +448,12 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
await HeartBeatTimer.DisposeAsync().ConfigureAwait(false); await HeartBeatTimer.DisposeAsync().ConfigureAwait(false);
// Those are objects that might be null and the check should be in-place // Those are objects that might be null and the check should be in-place
if (CallbacksAborted != null) {
await CallbacksAborted.CancelAsync().ConfigureAwait(false);
CallbacksAborted.Dispose();
}
if (ConnectionFailureTimer != null) { if (ConnectionFailureTimer != null) {
await ConnectionFailureTimer.DisposeAsync().ConfigureAwait(false); await ConnectionFailureTimer.DisposeAsync().ConfigureAwait(false);
} }
@ -1925,7 +1930,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
} }
KeepRunning = true; KeepRunning = true;
Utilities.InBackground(HandleCallbacks, true);
ArchiLogger.LogGenericInfo(Strings.Starting); ArchiLogger.LogGenericInfo(Strings.Starting);
// Support and convert 2FA files // Support and convert 2FA files
@ -1955,6 +1960,13 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
await ImportKeysToRedeem(keysToRedeemFilePath).ConfigureAwait(false); await ImportKeysToRedeem(keysToRedeemFilePath).ConfigureAwait(false);
} }
// If any previous callbacks handling loop is still going, we're going to abort it
await StopHandlingCallbacks().ConfigureAwait(false);
CallbacksAborted = new CancellationTokenSource();
Utilities.InBackground(() => HandleCallbacks(CallbacksAborted.Token), true);
await Connect().ConfigureAwait(false); await Connect().ConfigureAwait(false);
} }
@ -1964,6 +1976,7 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
} }
KeepRunning = false; KeepRunning = false;
ArchiLogger.LogGenericInfo(Strings.BotStopping); ArchiLogger.LogGenericInfo(Strings.BotStopping);
if (SteamClient.IsConnected) { if (SteamClient.IsConnected) {
@ -2037,6 +2050,23 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
} }
} }
// Ensure the handling loop is stopped, but allow a few extra seconds for any lost callbacks to trigger
CancellationTokenSource? callbacksAborted = CallbacksAborted;
if (callbacksAborted is { IsCancellationRequested: false }) {
Utilities.InBackground(
async () => {
await Task.Delay(5000, CancellationToken.None).ConfigureAwait(false);
try {
await callbacksAborted.CancelAsync().ConfigureAwait(false);
} catch {
// Ignored, object already disposed or similar
}
}
);
}
Bots.TryRemove(BotName, out _); Bots.TryRemove(BotName, out _);
await PluginsCore.OnBotDestroy(this).ConfigureAwait(false); await PluginsCore.OnBotDestroy(this).ConfigureAwait(false);
} }
@ -2140,25 +2170,14 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
return result; return result;
} }
private async Task HandleCallbacks() { private async Task HandleCallbacks(CancellationToken cancellationToken = default) {
if (!await CallbackSemaphore.WaitAsync(CallbackSleep).ConfigureAwait(false)) {
if (Debugging.IsUserDebugging) {
ArchiLogger.LogGenericDebug(Strings.FormatWarningFailedWithError(nameof(CallbackSemaphore)));
}
return;
}
try { try {
TimeSpan timeSpan = TimeSpan.FromMilliseconds(CallbackSleep); // Our objective here is to process the callbacks for as long as it's relevant
while (!cancellationToken.IsCancellationRequested) {
while (KeepRunning || SteamClient.IsConnected) { await CallbackManager.RunWaitCallbackAsync(cancellationToken).ConfigureAwait(false);
CallbackManager.RunWaitAllCallbacks(timeSpan);
} }
} catch (Exception e) { } catch (OperationCanceledException) {
ArchiLogger.LogGenericException(e); // Ignored, we were asked to stop processing
} finally {
CallbackSemaphore.Release();
} }
} }
@ -2826,12 +2845,16 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
// If we initiated disconnect, do not attempt to reconnect // If we initiated disconnect, do not attempt to reconnect
if (callback.UserInitiated && !ReconnectOnUserInitiated) { if (callback.UserInitiated && !ReconnectOnUserInitiated) {
await StopHandlingCallbacks().ConfigureAwait(false);
return; return;
} }
switch (lastLogOnResult) { switch (lastLogOnResult) {
case EResult.AccountDisabled: case EResult.AccountDisabled:
// Do not attempt to reconnect, those failures are permanent // Do not attempt to reconnect, those failures are permanent
await StopHandlingCallbacks().ConfigureAwait(false);
return; return;
case EResult.AccessDenied when !string.IsNullOrEmpty(RefreshToken): case EResult.AccessDenied when !string.IsNullOrEmpty(RefreshToken):
case EResult.Expired when !string.IsNullOrEmpty(RefreshToken): case EResult.Expired when !string.IsNullOrEmpty(RefreshToken):
@ -2864,7 +2887,13 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
break; break;
} }
if (!KeepRunning || SteamClient.IsConnected) { if (!KeepRunning) {
await StopHandlingCallbacks().ConfigureAwait(false);
return;
}
if (SteamClient.IsConnected) {
return; return;
} }
@ -2872,7 +2901,13 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
while (RequiredInput != ASF.EUserInputType.None) { while (RequiredInput != ASF.EUserInputType.None) {
await Task.Delay(1000).ConfigureAwait(false); await Task.Delay(1000).ConfigureAwait(false);
if (!KeepRunning || SteamClient.IsConnected) { if (!KeepRunning) {
await StopHandlingCallbacks().ConfigureAwait(false);
return;
}
if (SteamClient.IsConnected) {
return; return;
} }
} }
@ -3847,6 +3882,17 @@ public sealed class Bot : IAsyncDisposable, IDisposable {
ConnectionFailureTimer = null; ConnectionFailureTimer = null;
} }
private async Task StopHandlingCallbacks() {
if (CallbacksAborted == null) {
return;
}
await CallbacksAborted.CancelAsync().ConfigureAwait(false);
CallbacksAborted.Dispose();
CallbacksAborted = null;
}
private void StopPlayingWasBlockedTimer() { private void StopPlayingWasBlockedTimer() {
if (PlayingWasBlockedTimer == null) { if (PlayingWasBlockedTimer == null) {
return; return;

View file

@ -843,6 +843,7 @@ public sealed class ArchiHandler : ClientMsgHandler {
// If we have custom name to display, we must workaround the Steam network broken behaviour and send request on clean non-playing session // If we have custom name to display, we must workaround the Steam network broken behaviour and send request on clean non-playing session
// This ensures that custom name will in fact display properly (if it's not omitted due to MaxGamesPlayedConcurrently, that is) // This ensures that custom name will in fact display properly (if it's not omitted due to MaxGamesPlayedConcurrently, that is)
Client.Send(request); Client.Send(request);
await Task.Delay(Bot.CallbackSleep).ConfigureAwait(false); await Task.Delay(Bot.CallbackSleep).ConfigureAwait(false);
request.Body.games_played.Add( request.Body.games_played.Add(

View file

@ -269,6 +269,7 @@ public sealed class Actions : IAsyncDisposable, IDisposable {
// We add extra delay because OnFarmingStopped() also executes PlayGames() // We add extra delay because OnFarmingStopped() also executes PlayGames()
// Despite of proper order on our end, Steam network might not respect it // Despite of proper order on our end, Steam network might not respect it
await Task.Delay(Bot.CallbackSleep).ConfigureAwait(false); await Task.Delay(Bot.CallbackSleep).ConfigureAwait(false);
await Bot.ArchiHandler.PlayGames([], Bot.BotConfig.CustomGamePlayedWhileIdle).ConfigureAwait(false); await Bot.ArchiHandler.PlayGames([], Bot.BotConfig.CustomGamePlayedWhileIdle).ConfigureAwait(false);
} }