From cc29619935e8210c1d863ea8cb89d11a99d6ca15 Mon Sep 17 00:00:00 2001 From: Citrinate Date: Thu, 23 May 2024 21:05:53 -0400 Subject: [PATCH] Improve stability --- .../Boosters/BoosterDequeueReason.cs | 3 +- BoosterManager/Boosters/BoosterJob.cs | 80 +++++++----- BoosterManager/Boosters/BoosterQueue.cs | 115 +++++++++--------- BoosterManager/Commands.cs | 9 +- BoosterManager/Handlers/BoosterHandler.cs | 39 +++--- 5 files changed, 144 insertions(+), 102 deletions(-) diff --git a/BoosterManager/Boosters/BoosterDequeueReason.cs b/BoosterManager/Boosters/BoosterDequeueReason.cs index cef7b9f..8a417b2 100644 --- a/BoosterManager/Boosters/BoosterDequeueReason.cs +++ b/BoosterManager/Boosters/BoosterDequeueReason.cs @@ -5,6 +5,7 @@ internal enum BoosterDequeueReason { RemovedByUser, Uncraftable, UnexpectedlyUncraftable, - Unmarketable + Unmarketable, + JobStopped } } diff --git a/BoosterManager/Boosters/BoosterJob.cs b/BoosterManager/Boosters/BoosterJob.cs index 4706312..6f0ae25 100644 --- a/BoosterManager/Boosters/BoosterJob.cs +++ b/BoosterManager/Boosters/BoosterJob.cs @@ -3,7 +3,6 @@ using System.Linq; using ArchiSteamFarm.Steam; using BoosterManager.Localization; -using SteamKit2.GC.Dota.Internal; // Represents the state of a !booster command @@ -15,6 +14,7 @@ internal sealed class BoosterJob { internal StatusReporter StatusReporter; private bool CreatedFromSaveState = false; private readonly object LockObject = new(); + private bool JobStopped = false; private BoosterHandler BoosterHandler => BoosterHandler.BoosterHandlers[Bot.BotName]; private BoosterQueue BoosterQueue => BoosterHandler.BoosterQueue; @@ -128,29 +128,6 @@ private void Start() { BoosterQueue.AddBooster(gameID, this); } - void OnBoosterInfosUpdated(Dictionary boosterInfos) { - try { - // At this point, all boosters that can be added to the queue have been - DateTime? lastBoosterCraftTime = LastBoosterCraftTime; - if (lastBoosterCraftTime == null) { - StatusReporter.Report(Bot, Strings.BoostersUncraftable, log: CreatedFromSaveState); - Finish(); - - return; - } - - BoosterHandler.UpdateBoosterJobs(); - - if (lastBoosterCraftTime.Value.Date == DateTime.Today) { - StatusReporter.Report(Bot, String.Format(Strings.QueueStatusShort, NumBoosters, String.Format("{0:N0}", GemsNeeded), String.Format("{0:t}", lastBoosterCraftTime)), log: CreatedFromSaveState); - } else { - StatusReporter.Report(Bot, String.Format(Strings.QueueStatusShortWithDate, NumBoosters, String.Format("{0:N0}", GemsNeeded), String.Format("{0:d}", lastBoosterCraftTime), String.Format("{0:t}", lastBoosterCraftTime)), log: CreatedFromSaveState); - } - } finally { - BoosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated; - } - } - BoosterQueue.OnBoosterInfosUpdated += OnBoosterInfosUpdated; BoosterQueue.OnBoosterRemoved += OnBoosterRemoved; BoosterQueue.Start(); @@ -164,6 +141,50 @@ internal void Finish() { } } + internal void Stop() { + JobStopped = true; + BoosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated; + BoosterQueue.OnBoosterRemoved -= OnBoosterRemoved; + + lock (LockObject) { + foreach (Booster booster in Boosters.Where(booster => !booster.WasCrafted)) { + BoosterQueue.RemoveBooster(booster.GameID, BoosterDequeueReason.JobStopped); + } + } + } + + private void Update() { + if (JobStopped) { + return; + } + + // Save the current state of this job + BoosterHandler.UpdateBoosterJobs(); + } + + void OnBoosterInfosUpdated(Dictionary boosterInfos) { + try { + // At this point, all boosters that can be added to the queue have been + DateTime? lastBoosterCraftTime = LastBoosterCraftTime; + if (lastBoosterCraftTime == null) { + StatusReporter.Report(Bot, Strings.BoostersUncraftable, log: CreatedFromSaveState); + Finish(); + + return; + } + + Update(); + + if (lastBoosterCraftTime.Value.Date == DateTime.Today) { + StatusReporter.Report(Bot, String.Format(Strings.QueueStatusShort, NumBoosters, String.Format("{0:N0}", GemsNeeded), String.Format("{0:t}", lastBoosterCraftTime)), log: CreatedFromSaveState); + } else { + StatusReporter.Report(Bot, String.Format(Strings.QueueStatusShortWithDate, NumBoosters, String.Format("{0:N0}", GemsNeeded), String.Format("{0:d}", lastBoosterCraftTime), String.Format("{0:t}", lastBoosterCraftTime)), log: CreatedFromSaveState); + } + } finally { + BoosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated; + } + } + internal void OnBoosterRemoved(Booster booster, BoosterDequeueReason reason) { if (!(reason == BoosterDequeueReason.Crafted // Currently we don't prevent user from queing a booster that already exists in the permanent booster job @@ -203,7 +224,8 @@ internal void OnBoosterUnqueueable (uint gameID, BoosterDequeueReason reason) { lock(LockObject) { GameIDsToBooster.RemoveAll(x => x == gameID); } - BoosterHandler.UpdateBoosterJobs(); + + Update(); } internal void OnBoosterDequeued(Booster booster, BoosterDequeueReason reason) { @@ -213,7 +235,7 @@ internal void OnBoosterDequeued(Booster booster, BoosterDequeueReason reason) { Boosters.Remove(booster); } StatusReporter.Report(Bot, String.Format(Strings.BoosterUnexpectedlyUncraftable, booster.Info.Name, booster.GameID)); - BoosterHandler.UpdateBoosterJobs(); + Update(); return; } @@ -238,7 +260,7 @@ internal void OnBoosterDequeued(Booster booster, BoosterDequeueReason reason) { } } - BoosterHandler.UpdateBoosterJobs(); + Update(); return; } @@ -283,7 +305,7 @@ internal bool RemoveUnqueuedBooster(uint gameID) { removed = GameIDsToBooster.Remove(gameID); } - BoosterHandler.UpdateBoosterJobs(); + Update(); return removed; } @@ -319,7 +341,7 @@ internal List RemoveAllBoosters() { } } - BoosterHandler.UpdateBoosterJobs(); + Update(); return gameIDsRemoved; } diff --git a/BoosterManager/Boosters/BoosterQueue.cs b/BoosterManager/Boosters/BoosterQueue.cs index 3f48057..6029c08 100644 --- a/BoosterManager/Boosters/BoosterQueue.cs +++ b/BoosterManager/Boosters/BoosterQueue.cs @@ -4,11 +4,12 @@ using System.Threading; using System.Threading.Tasks; using ArchiSteamFarm.Collections; +using ArchiSteamFarm.Core; using ArchiSteamFarm.Steam; using BoosterManager.Localization; namespace BoosterManager { - internal sealed class BoosterQueue : IDisposable { + internal sealed class BoosterQueue { private readonly Bot Bot; private readonly Timer Timer; private readonly ConcurrentHashSet Boosters = new(new BoosterComparer()); @@ -24,6 +25,7 @@ internal sealed class BoosterQueue : IDisposable { private const int BoosterInfosUpdateBackOffMinMinutes = 1; private const int BoosterInfosUpdateBackOffMaxMinutes = 15; private float BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault; + private SemaphoreSlim RunSemaphore = new SemaphoreSlim(1, 1); internal BoosterQueue(Bot bot) { Bot = bot; @@ -35,82 +37,83 @@ internal BoosterQueue(Bot bot) { ); } - public void Dispose() { - Timer.Dispose(); - } - internal void Start() { - UpdateTimer(DateTime.Now); + Utilities.InBackground(async() => await Run().ConfigureAwait(false)); } private async Task Run() { - if (!Bot.IsConnectedAndLoggedOn) { - UpdateTimer(DateTime.Now.AddSeconds(1)); - - return; - } - - // Reload the booster creator page - if (!await UpdateBoosterInfos().ConfigureAwait(false)) { - // Reload failed, try again later - Bot.ArchiLogger.LogGenericError(Strings.BoosterInfoUpdateFailed); - UpdateTimer(DateTime.Now.AddMinutes(Math.Min(BoosterInfosUpdateBackOffMaxMinutes, BoosterInfosUpdateBackOffMinMinutes * BoosterInfosUpdateBackOffMultiplier))); - BoosterInfosUpdateBackOffMultiplier += BoosterInfosUpdateBackOffMultiplierStep; + await RunSemaphore.WaitAsync().ConfigureAwait(false); + try { + if (!Bot.IsConnectedAndLoggedOn) { + UpdateTimer(DateTime.Now.AddSeconds(1)); - return; - } - - Booster? booster = GetNextCraftableBooster(); - if (booster == null) { - // Booster queue is empty - BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault; + return; + } - return; - } - - if (DateTime.Now >= booster.GetAvailableAtTime()) { - // Attempt to craft the next booster in the queue - if (booster.Info.Price > AvailableGems) { - // Not enough gems, wait until we get more gems - booster.BoosterJob.OnInsufficientGems(booster); - OnBoosterInfosUpdated += ForceUpdateBoosterInfos; + // Reload the booster creator page + if (!await UpdateBoosterInfos().ConfigureAwait(false)) { + // Reload failed, try again later + Bot.ArchiLogger.LogGenericError(Strings.BoosterInfoUpdateFailed); UpdateTimer(DateTime.Now.AddMinutes(Math.Min(BoosterInfosUpdateBackOffMaxMinutes, BoosterInfosUpdateBackOffMinMinutes * BoosterInfosUpdateBackOffMultiplier))); BoosterInfosUpdateBackOffMultiplier += BoosterInfosUpdateBackOffMultiplierStep; return; } - BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault; - - if (!await CraftBooster(booster).ConfigureAwait(false)) { - // Craft failed, decide whether or not to remove this booster from the queue - Bot.ArchiLogger.LogGenericError(String.Format(Strings.BoosterCreationFailed, booster.GameID)); - VerifyCraftBoosterError(booster); - UpdateTimer(DateTime.Now); + Booster? booster = GetNextCraftableBooster(); + if (booster == null) { + // Booster queue is empty + BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault; return; } + + if (DateTime.Now >= booster.GetAvailableAtTime()) { + // Attempt to craft the next booster in the queue + if (booster.Info.Price > AvailableGems) { + // Not enough gems, wait until we get more gems + booster.BoosterJob.OnInsufficientGems(booster); + OnBoosterInfosUpdated += ForceUpdateBoosterInfos; + UpdateTimer(DateTime.Now.AddMinutes(Math.Min(BoosterInfosUpdateBackOffMaxMinutes, BoosterInfosUpdateBackOffMinMinutes * BoosterInfosUpdateBackOffMultiplier))); + BoosterInfosUpdateBackOffMultiplier += BoosterInfosUpdateBackOffMultiplierStep; - Bot.ArchiLogger.LogGenericInfo(String.Format(Strings.BoosterCreationSuccess, booster.GameID)); - RemoveBooster(booster.GameID, BoosterDequeueReason.Crafted); + return; + } - booster = GetNextCraftableBooster(); - if (booster == null) { - // Queue has no more boosters in it - return; + BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault; + + if (!await CraftBooster(booster).ConfigureAwait(false)) { + // Craft failed, decide whether or not to remove this booster from the queue + Bot.ArchiLogger.LogGenericError(String.Format(Strings.BoosterCreationFailed, booster.GameID)); + VerifyCraftBoosterError(booster); + UpdateTimer(DateTime.Now); + + return; + } + + Bot.ArchiLogger.LogGenericInfo(String.Format(Strings.BoosterCreationSuccess, booster.GameID)); + RemoveBooster(booster.GameID, BoosterDequeueReason.Crafted); + + booster = GetNextCraftableBooster(); + if (booster == null) { + // Queue has no more boosters in it + return; + } } - } - BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault; + BoosterInfosUpdateBackOffMultiplier = BoosterInfosUpdateBackOffMultiplierDefault; - // Wait until the next booster is ready to craft - DateTime nextBoosterTime = booster.GetAvailableAtTime(); - if (nextBoosterTime < DateTime.Now.AddSeconds(MinDelayBetweenBoosters)) { - nextBoosterTime = DateTime.Now.AddSeconds(MinDelayBetweenBoosters); - } + // Wait until the next booster is ready to craft + DateTime nextBoosterTime = booster.GetAvailableAtTime(); + if (nextBoosterTime < DateTime.Now.AddSeconds(MinDelayBetweenBoosters)) { + nextBoosterTime = DateTime.Now.AddSeconds(MinDelayBetweenBoosters); + } - UpdateTimer(nextBoosterTime); - Bot.ArchiLogger.LogGenericInfo(String.Format(Strings.NextBoosterCraft, String.Format("{0:T}", nextBoosterTime))); + UpdateTimer(nextBoosterTime); + Bot.ArchiLogger.LogGenericInfo(String.Format(Strings.NextBoosterCraft, String.Format("{0:T}", nextBoosterTime))); + } finally { + RunSemaphore.Release(); + } } internal void AddBooster(uint gameID, BoosterJob boosterJob) { diff --git a/BoosterManager/Commands.cs b/BoosterManager/Commands.cs index 849350f..df5ef9a 100644 --- a/BoosterManager/Commands.cs +++ b/BoosterManager/Commands.cs @@ -550,7 +550,7 @@ internal static class Commands { string twofacMessage = success ? message : String.Format(ArchiSteamFarm.Localization.Strings.WarningFailedWithError, message); if (repeatMessage != null) { - return FormatBotResponse(bot, String.Format("{0}. {1}", twofacMessage, repeatMessage)); + return FormatBotResponse(bot, String.Format("{0} {1}", twofacMessage, repeatMessage)); } return FormatBotResponse(bot, twofacMessage); @@ -1710,10 +1710,15 @@ internal static class Commands { return access >= EAccess.Owner ? FormatStaticResponse(String.Format(ArchiSteamFarm.Localization.Strings.BotNotFound, botNames)) : null; } - if(bots.Any(bot => ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID) < EAccess.Master)) { + if (bots.Any(bot => ArchiSteamFarm.Steam.Interaction.Commands.GetProxyAccess(bot, access, steamID) < EAccess.Master)) { return null; } + Bot? offlineBot = bots.FirstOrDefault(bot => !bot.IsConnectedAndLoggedOn); + if (offlineBot != null) { + return FormatBotResponse(offlineBot, ArchiSteamFarm.Localization.Strings.BotNotConnected); + } + // Parse GameIDs string[] gameIDs = gameIDsAsText.Split(",", StringSplitOptions.RemoveEmptyEntries); diff --git a/BoosterManager/Handlers/BoosterHandler.cs b/BoosterManager/Handlers/BoosterHandler.cs index 0a67814..7c0f10d 100644 --- a/BoosterManager/Handlers/BoosterHandler.cs +++ b/BoosterManager/Handlers/BoosterHandler.cs @@ -7,9 +7,9 @@ using System.Linq; namespace BoosterManager { - internal sealed class BoosterHandler : IDisposable { + internal sealed class BoosterHandler { private readonly Bot Bot; - internal readonly BoosterDatabase BoosterDatabase; + internal BoosterDatabase BoosterDatabase { get; private set; } internal readonly BoosterQueue BoosterQueue; internal static ConcurrentDictionary BoosterHandlers = new(); internal ConcurrentList Jobs = new(); @@ -24,14 +24,14 @@ private BoosterHandler(Bot bot, BoosterDatabase boosterDatabase) { BoosterQueue = new BoosterQueue(Bot); } - public void Dispose() { - BoosterQueue.Dispose(); - } - internal static void AddHandler(Bot bot, BoosterDatabase boosterDatabase) { if (BoosterHandlers.ContainsKey(bot.BotName)) { - BoosterHandlers[bot.BotName].Dispose(); - BoosterHandlers.TryRemove(bot.BotName, out BoosterHandler? _); + // Bot's config was reloaded, cancel and then restore jobs + BoosterHandlers[bot.BotName].CancelBoosterJobs(); + BoosterHandlers[bot.BotName].BoosterDatabase = boosterDatabase; + BoosterHandlers[bot.BotName].RestoreBoosterJobs(); + + return; } BoosterHandler handler = new BoosterHandler(bot, boosterDatabase); @@ -72,6 +72,7 @@ internal static void SmartScheduleBoosters(BoosterJobType jobType, HashSet botStates.Add(bot, (BoosterHandlers[bot.BotName].Jobs.GetNumBoosters(gameID), boosterInfo.AvailableAtTime ?? now)); } + // No bots can craft boosters for this gameID if (botStates.Count == 0) { continue; } @@ -199,6 +200,14 @@ internal void UpdateBoosterJobs() { BoosterDatabase.UpdateBoosterJobs(Jobs.Limited().Unfinised().SaveState()); } + private void CancelBoosterJobs() { + foreach (BoosterJob job in Jobs) { + job.Stop(); + } + + Jobs.Clear(); + } + private void RestoreBoosterJobs() { foreach (BoosterJobState jobState in BoosterDatabase.BoosterJobs) { Jobs.Add(new BoosterJob(Bot, BoosterJobType.Limited, jobState)); @@ -298,20 +307,22 @@ internal void OnGemsRecieved() { } internal static void GetBoosterInfos(HashSet bots, Action>> callback) { - Dictionary> boosterInfos = new(); + ConcurrentDictionary> boosterInfos = new(); foreach (Bot bot in bots) { + BoosterQueue boosterQueue = BoosterHandlers[bot.BotName].BoosterQueue; + void OnBoosterInfosUpdated(Dictionary boosterInfo) { - BoosterHandlers[bot.BotName].BoosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated; - boosterInfos.Add(bot, boosterInfo); + boosterQueue.OnBoosterInfosUpdated -= OnBoosterInfosUpdated; + boosterInfos.TryAdd(bot, boosterInfo); if (boosterInfos.Count == bots.Count) { - callback(boosterInfos); + callback(boosterInfos.ToDictionary()); } } - BoosterHandlers[bot.BotName].BoosterQueue.OnBoosterInfosUpdated += OnBoosterInfosUpdated; - BoosterHandlers[bot.BotName].BoosterQueue.Start(); + boosterQueue.OnBoosterInfosUpdated += OnBoosterInfosUpdated; + boosterQueue.Start(); } } }