From eb33e55aad944186690c0d5dca7a2ef77f23c781 Mon Sep 17 00:00:00 2001 From: Noi Date: Tue, 15 Jun 2021 19:29:35 -0700 Subject: [PATCH] Add support for separate shard ranges per instance -New config values ShardRange and ShardTotal --ShardTotal replaces ShardCount -New config value QuitOnFails: --Program quits after enough shards have been removed --- BirthdayBot.csproj | 8 +++---- Configuration.cs | 55 ++++++++++++++++++++++++++++++++++++---------- Program.cs | 2 ++ ShardManager.cs | 51 +++++++++++++++++++++++------------------- 4 files changed, 79 insertions(+), 37 deletions(-) diff --git a/BirthdayBot.csproj b/BirthdayBot.csproj index 529955a..2c582c1 100644 --- a/BirthdayBot.csproj +++ b/BirthdayBot.csproj @@ -3,7 +3,7 @@ Exe netcoreapp3.1 - 3.0.3 + 3.1.0 BirthdayBot NoiTheCat BirthdayBot @@ -20,9 +20,9 @@ - - - + + + diff --git a/Configuration.cs b/Configuration.cs index ac02a25..01f2e4e 100644 --- a/Configuration.cs +++ b/Configuration.cs @@ -4,6 +4,7 @@ using Npgsql; using System; using System.IO; using System.Reflection; +using System.Text.RegularExpressions; namespace BirthdayBot { @@ -15,7 +16,14 @@ namespace BirthdayBot public string BotToken { get; } public string LogWebhook { get; } public string DBotsToken { get; } - public int ShardCount { get; } + + public const string ShardLenConfKey = "ShardRange"; + public int ShardStart { get; } + public int ShardAmount { get; } + + public int ShardTotal { get; } + + public bool QuitOnFails { get; } public Configuration() { @@ -31,15 +39,15 @@ namespace BirthdayBot var jc = JObject.Parse(File.ReadAllText(confPath)); - BotToken = jc["BotToken"]?.Value(); + BotToken = jc[nameof(BotToken)]?.Value(); if (string.IsNullOrWhiteSpace(BotToken)) - throw new Exception("'BotToken' must be specified."); + throw new Exception($"'{nameof(BotToken)}' must be specified."); - LogWebhook = jc["LogWebhook"]?.Value(); + LogWebhook = jc[nameof(LogWebhook)]?.Value(); if (string.IsNullOrWhiteSpace(LogWebhook)) - throw new Exception("'LogWebhook' must be specified."); + throw new Exception($"'{nameof(LogWebhook)}' must be specified."); - var dbj = jc["DBotsToken"]; + var dbj = jc[nameof(DBotsToken)]; if (dbj != null) { DBotsToken = dbj.Value(); @@ -64,16 +72,41 @@ namespace BirthdayBot if (sqldb != null) csb.Database = sqldb; // Optional database setting Database.DBConnectionString = csb.ToString(); - int? sc = jc["ShardCount"]?.Value(); - if (!sc.HasValue) ShardCount = 1; + int? sc = jc[nameof(ShardTotal)]?.Value(); + if (!sc.HasValue) ShardTotal = 1; else { - ShardCount = sc.Value; - if (ShardCount <= 0) + ShardTotal = sc.Value; + if (ShardTotal <= 0) { - throw new Exception("'ShardCount' must be a positive integer."); + throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer."); } } + + string srVal = jc[ShardLenConfKey]?.Value(); + if (!string.IsNullOrWhiteSpace(srVal)) + { + Regex srPicker = new Regex(@"(?\d{1,2})[-,]{1}(?\d{1,2})"); + var m = srPicker.Match(srVal); + if (m.Success) + { + ShardStart = int.Parse(m.Groups["low"].Value); + int high = int.Parse(m.Groups["high"].Value); + ShardAmount = high - (ShardStart - 1); + } + else + { + throw new Exception($"Shard range not properly formatted in '{ShardLenConfKey}'."); + } + } + else + { + // Default: this instance handles all shards from ShardTotal + ShardStart = 0; + ShardAmount = ShardTotal; + } + + QuitOnFails = jc[nameof(QuitOnFails)]?.Value() ?? false; } } } diff --git a/Program.cs b/Program.cs index f51d2ef..2260102 100644 --- a/Program.cs +++ b/Program.cs @@ -36,6 +36,7 @@ namespace BirthdayBot private static void OnCancelKeyPressed(object sender, ConsoleCancelEventArgs e) { e.Cancel = true; + Log("Shutdown", "Captured cancel key; sending shutdown."); ProgramStop(); } @@ -44,6 +45,7 @@ namespace BirthdayBot { if (_stopping) return; _stopping = true; + Log("Shutdown", "Commencing shutdown..."); var dispose = Task.Run(_bot.Dispose); if (!dispose.Wait(90000)) diff --git a/ShardManager.cs b/ShardManager.cs index 752c4e7..3f055a5 100644 --- a/ShardManager.cs +++ b/ShardManager.cs @@ -29,6 +29,12 @@ namespace BirthdayBot /// private const int MaxDestroyedShards = 5; + /// + /// Amount of time without a completed background service run before a shard instance + /// is considered "dead" and tasked to be removed. + /// + private static readonly TimeSpan DeadShardThreshold = new TimeSpan(0, 20, 0); + /// /// A dictionary with shard IDs as its keys and shard instances as its values. /// When initialized, all keys will be created as configured. If an instance is removed, @@ -70,8 +76,8 @@ namespace BirthdayBot foreach (var item in _cmdsMods.Commands) _dispatchCommands.Add(item.Item1, item.Item2); _shards = new Dictionary(); - // TODO implement more flexible sharding configuration here - for (int i = 0; i < Config.ShardCount; i++) + // Create only the specified shards as needed by this instance + for (int i = Config.ShardStart; i < (Config.ShardStart + Config.ShardAmount); i++) { _shards.Add(i, null); } @@ -84,7 +90,6 @@ namespace BirthdayBot public void Dispose() { - Log("Captured cancel key. Shutting down shard status watcher..."); _watchdogCancel.Cancel(); _watchdogTask.Wait(5000); if (!_watchdogTask.IsCompleted) @@ -117,7 +122,7 @@ namespace BirthdayBot var clientConf = new DiscordSocketConfig() { ShardId = shardId, - TotalShards = Config.ShardCount, + TotalShards = Config.ShardTotal, LogLevel = LogSeverity.Info, DefaultRetryMode = RetryMode.RetryRatelimit, MessageCacheSize = 0, // not needed at all @@ -142,7 +147,6 @@ namespace BirthdayBot // Iterate through shard list, extract data var guildInfo = new Dictionary(); var now = DateTimeOffset.UtcNow; - ulong? botId = null; var nullShards = new List(); foreach (var item in _shards) { @@ -152,7 +156,6 @@ namespace BirthdayBot continue; } var shard = item.Value; - botId ??= shard.DiscordClient.CurrentUser?.Id; var guildCount = shard.DiscordClient.Guilds.Count; var connScore = shard.ConnectionScore; @@ -181,7 +184,7 @@ namespace BirthdayBot badShards.Add(item.Key); // Consider a shard dead after a long span without background activity - if (lastRun > new TimeSpan(0, 30, 0)) + if (lastRun > DeadShardThreshold) deadShards.Add(item.Key); } else @@ -199,9 +202,8 @@ namespace BirthdayBot if (detailedInfo) { result.Remove(result.Length - 1, 1); - result.Append($"[{guildInfo[item].Item2:+000;-000}"); - result.Append($" {Math.Floor(guildInfo[item].Item3.TotalMinutes):00}m"); - result.Append($"{guildInfo[item].Item3.Seconds:00}s] "); + result.Append($"[{guildInfo[item].Item2:+0;-0}"); + result.Append($" {Math.Floor(guildInfo[item].Item3.TotalSeconds):000}s] "); } } if (result.Length > 0) result.Remove(result.Length - 1, 1); @@ -213,25 +215,30 @@ namespace BirthdayBot if (nullShards.Count > 0) Log("Inactive shards: " + statusDisplay(nullShards, false)); // Remove dead shards - foreach (var dead in deadShards) - { + foreach (var dead in deadShards) { + // TODO investigate - has this been hanging here? _shards[dead].Dispose(); _shards[dead] = null; _destroyedShards++; } - if (_destroyedShards > MaxDestroyedShards) Program.ProgramStop(); - - // Start up any missing shards - int startAllowance = 4; - foreach (var id in nullShards) + if (Config.QuitOnFails && _destroyedShards > MaxDestroyedShards) { - // To avoid possible issues with resources strained over so many shards starting at once, - // initialization is spread out by only starting a few at a time. - if (startAllowance-- > 0) + Program.ProgramStop(); + } + else + { + // Start up any missing shards + int startAllowance = 4; + foreach (var id in nullShards) { - _shards[id] = await InitializeShard(id).ConfigureAwait(false); + // To avoid possible issues with resources strained over so many shards starting at once, + // initialization is spread out by only starting a few at a time. + if (startAllowance-- > 0) + { + _shards[id] = await InitializeShard(id).ConfigureAwait(false); + } + else break; } - else break; } // All done for now