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
This commit is contained in:
Noi 2021-06-15 19:29:35 -07:00
parent 88c18c4364
commit eb33e55aad
4 changed files with 79 additions and 37 deletions

View file

@ -3,7 +3,7 @@
<PropertyGroup> <PropertyGroup>
<OutputType>Exe</OutputType> <OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework> <TargetFramework>netcoreapp3.1</TargetFramework>
<Version>3.0.3</Version> <Version>3.1.0</Version>
<PackageId>BirthdayBot</PackageId> <PackageId>BirthdayBot</PackageId>
<Authors>NoiTheCat</Authors> <Authors>NoiTheCat</Authors>
<Product>BirthdayBot</Product> <Product>BirthdayBot</Product>
@ -20,9 +20,9 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Discord.Net" Version="2.4.0" /> <PackageReference Include="Discord.Net" Version="2.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="12.0.3" /> <PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="NodaTime" Version="3.0.3" /> <PackageReference Include="NodaTime" Version="3.0.5" />
<PackageReference Include="Npgsql" Version="4.1.5" /> <PackageReference Include="Npgsql" Version="5.0.7" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View file

@ -4,6 +4,7 @@ using Npgsql;
using System; using System;
using System.IO; using System.IO;
using System.Reflection; using System.Reflection;
using System.Text.RegularExpressions;
namespace BirthdayBot namespace BirthdayBot
{ {
@ -15,7 +16,14 @@ namespace BirthdayBot
public string BotToken { get; } public string BotToken { get; }
public string LogWebhook { get; } public string LogWebhook { get; }
public string DBotsToken { 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() public Configuration()
{ {
@ -31,15 +39,15 @@ namespace BirthdayBot
var jc = JObject.Parse(File.ReadAllText(confPath)); var jc = JObject.Parse(File.ReadAllText(confPath));
BotToken = jc["BotToken"]?.Value<string>(); BotToken = jc[nameof(BotToken)]?.Value<string>();
if (string.IsNullOrWhiteSpace(BotToken)) if (string.IsNullOrWhiteSpace(BotToken))
throw new Exception("'BotToken' must be specified."); throw new Exception($"'{nameof(BotToken)}' must be specified.");
LogWebhook = jc["LogWebhook"]?.Value<string>(); LogWebhook = jc[nameof(LogWebhook)]?.Value<string>();
if (string.IsNullOrWhiteSpace(LogWebhook)) 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) if (dbj != null)
{ {
DBotsToken = dbj.Value<string>(); DBotsToken = dbj.Value<string>();
@ -64,16 +72,41 @@ namespace BirthdayBot
if (sqldb != null) csb.Database = sqldb; // Optional database setting if (sqldb != null) csb.Database = sqldb; // Optional database setting
Database.DBConnectionString = csb.ToString(); Database.DBConnectionString = csb.ToString();
int? sc = jc["ShardCount"]?.Value<int>(); int? sc = jc[nameof(ShardTotal)]?.Value<int>();
if (!sc.HasValue) ShardCount = 1; if (!sc.HasValue) ShardTotal = 1;
else else
{ {
ShardCount = sc.Value; ShardTotal = sc.Value;
if (ShardCount <= 0) 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<string>();
if (!string.IsNullOrWhiteSpace(srVal))
{
Regex srPicker = new Regex(@"(?<low>\d{1,2})[-,]{1}(?<high>\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<bool>() ?? false;
} }
} }
} }

View file

@ -36,6 +36,7 @@ namespace BirthdayBot
private static void OnCancelKeyPressed(object sender, ConsoleCancelEventArgs e) private static void OnCancelKeyPressed(object sender, ConsoleCancelEventArgs e)
{ {
e.Cancel = true; e.Cancel = true;
Log("Shutdown", "Captured cancel key; sending shutdown.");
ProgramStop(); ProgramStop();
} }
@ -44,6 +45,7 @@ namespace BirthdayBot
{ {
if (_stopping) return; if (_stopping) return;
_stopping = true; _stopping = true;
Log("Shutdown", "Commencing shutdown...");
var dispose = Task.Run(_bot.Dispose); var dispose = Task.Run(_bot.Dispose);
if (!dispose.Wait(90000)) if (!dispose.Wait(90000))

View file

@ -29,6 +29,12 @@ namespace BirthdayBot
/// </summary> /// </summary>
private const int MaxDestroyedShards = 5; private const int MaxDestroyedShards = 5;
/// <summary>
/// Amount of time without a completed background service run before a shard instance
/// is considered "dead" and tasked to be removed.
/// </summary>
private static readonly TimeSpan DeadShardThreshold = new TimeSpan(0, 20, 0);
/// <summary> /// <summary>
/// A dictionary with shard IDs as its keys and shard instances as its values. /// 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, /// 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); foreach (var item in _cmdsMods.Commands) _dispatchCommands.Add(item.Item1, item.Item2);
_shards = new Dictionary<int, ShardInstance>(); _shards = new Dictionary<int, ShardInstance>();
// TODO implement more flexible sharding configuration here // Create only the specified shards as needed by this instance
for (int i = 0; i < Config.ShardCount; i++) for (int i = Config.ShardStart; i < (Config.ShardStart + Config.ShardAmount); i++)
{ {
_shards.Add(i, null); _shards.Add(i, null);
} }
@ -84,7 +90,6 @@ namespace BirthdayBot
public void Dispose() public void Dispose()
{ {
Log("Captured cancel key. Shutting down shard status watcher...");
_watchdogCancel.Cancel(); _watchdogCancel.Cancel();
_watchdogTask.Wait(5000); _watchdogTask.Wait(5000);
if (!_watchdogTask.IsCompleted) if (!_watchdogTask.IsCompleted)
@ -117,7 +122,7 @@ namespace BirthdayBot
var clientConf = new DiscordSocketConfig() var clientConf = new DiscordSocketConfig()
{ {
ShardId = shardId, ShardId = shardId,
TotalShards = Config.ShardCount, TotalShards = Config.ShardTotal,
LogLevel = LogSeverity.Info, LogLevel = LogSeverity.Info,
DefaultRetryMode = RetryMode.RetryRatelimit, DefaultRetryMode = RetryMode.RetryRatelimit,
MessageCacheSize = 0, // not needed at all MessageCacheSize = 0, // not needed at all
@ -142,7 +147,6 @@ namespace BirthdayBot
// Iterate through shard list, extract data // Iterate through shard list, extract data
var guildInfo = new Dictionary<int, (int, int, TimeSpan)>(); var guildInfo = new Dictionary<int, (int, int, TimeSpan)>();
var now = DateTimeOffset.UtcNow; var now = DateTimeOffset.UtcNow;
ulong? botId = null;
var nullShards = new List<int>(); var nullShards = new List<int>();
foreach (var item in _shards) foreach (var item in _shards)
{ {
@ -152,7 +156,6 @@ namespace BirthdayBot
continue; continue;
} }
var shard = item.Value; var shard = item.Value;
botId ??= shard.DiscordClient.CurrentUser?.Id;
var guildCount = shard.DiscordClient.Guilds.Count; var guildCount = shard.DiscordClient.Guilds.Count;
var connScore = shard.ConnectionScore; var connScore = shard.ConnectionScore;
@ -181,7 +184,7 @@ namespace BirthdayBot
badShards.Add(item.Key); badShards.Add(item.Key);
// Consider a shard dead after a long span without background activity // 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); deadShards.Add(item.Key);
} }
else else
@ -199,9 +202,8 @@ namespace BirthdayBot
if (detailedInfo) if (detailedInfo)
{ {
result.Remove(result.Length - 1, 1); result.Remove(result.Length - 1, 1);
result.Append($"[{guildInfo[item].Item2:+000;-000}"); result.Append($"[{guildInfo[item].Item2:+0;-0}");
result.Append($" {Math.Floor(guildInfo[item].Item3.TotalMinutes):00}m"); result.Append($" {Math.Floor(guildInfo[item].Item3.TotalSeconds):000}s] ");
result.Append($"{guildInfo[item].Item3.Seconds:00}s] ");
} }
} }
if (result.Length > 0) result.Remove(result.Length - 1, 1); 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)); if (nullShards.Count > 0) Log("Inactive shards: " + statusDisplay(nullShards, false));
// Remove dead shards // 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].Dispose();
_shards[dead] = null; _shards[dead] = null;
_destroyedShards++; _destroyedShards++;
} }
if (_destroyedShards > MaxDestroyedShards) Program.ProgramStop(); if (Config.QuitOnFails && _destroyedShards > MaxDestroyedShards)
// Start up any missing shards
int startAllowance = 4;
foreach (var id in nullShards)
{ {
// To avoid possible issues with resources strained over so many shards starting at once, Program.ProgramStop();
// initialization is spread out by only starting a few at a time. }
if (startAllowance-- > 0) 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 // All done for now