Merge pull request #38 from NoiTheCat/cleanup

Code cleanup

Removes various unneeded things from code and rewrites the shard status display.
This commit is contained in:
Noi 2022-11-22 23:42:27 -08:00 committed by GitHub
commit febfd27ece
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
22 changed files with 65 additions and 157 deletions

View file

@ -132,7 +132,7 @@ public class BirthdayModule : BotModuleBase {
var query = GetSortedUserList(Context.Guild); var query = GetSortedUserList(Context.Guild);
// TODO pagination instead of this workaround // TODO pagination instead of this workaround
bool hasOutputOneLine = false; var hasOutputOneLine = false;
// First output is shown as an interaction response, followed then as regular channel messages // First output is shown as an interaction response, followed then as regular channel messages
async Task doOutput(string msg) { async Task doOutput(string msg) {
if (!hasOutputOneLine) { if (!hasOutputOneLine) {
@ -146,8 +146,7 @@ public class BirthdayModule : BotModuleBase {
var output = new StringBuilder(); var output = new StringBuilder();
var resultCount = 0; var resultCount = 0;
output.AppendLine("Recent and upcoming birthdays:"); output.AppendLine("Recent and upcoming birthdays:");
for (int count = 0; count <= 21; count++) // cover 21 days total (7 prior, current day, 14 upcoming) for (var count = 0; count <= 21; count++) { // cover 21 days total (7 prior, current day, 14 upcoming)
{
var results = from item in query var results = from item in query
where item.DateIndex == search where item.DateIndex == search
select item; select item;

View file

@ -3,7 +3,6 @@ using Discord.Interactions;
using static BirthdayBot.Common; using static BirthdayBot.Common;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
[RequireBotModerator] [RequireBotModerator]
[Group("override", HelpCmdOverride)] [Group("override", HelpCmdOverride)]
public class BirthdayOverrideModule : BotModuleBase { public class BirthdayOverrideModule : BotModuleBase {

View file

@ -39,7 +39,7 @@ public abstract class BotModuleBase : InteractionModuleBase<SocketInteractionCon
/// throwing a FormatException if the input is not recognized. /// throwing a FormatException if the input is not recognized.
/// </summary> /// </summary>
protected static string ParseTimeZone(string tzinput) { protected static string ParseTimeZone(string tzinput) {
if (!TzNameMap.TryGetValue(tzinput, out string? tz)) if (!TzNameMap.TryGetValue(tzinput, out var tz))
throw new FormatException(":x: Unknown time zone name.\n" + throw new FormatException(":x: Unknown time zone name.\n" +
"To find your time zone, please refer to: https://kevinnovak.github.io/Time-Zone-Picker/"); "To find your time zone, please refer to: https://kevinnovak.github.io/Time-Zone-Picker/");
return tz!; return tz!;

View file

@ -97,8 +97,8 @@ public class ConfigModule : BotModuleBase {
internal static async Task CmdSetMessageResponse(SocketModal modal, SocketGuildChannel channel, internal static async Task CmdSetMessageResponse(SocketModal modal, SocketGuildChannel channel,
Dictionary<string, SocketMessageComponentData> data) { Dictionary<string, SocketMessageComponentData> data) {
string? newSingle = data[ModalComCidSingle].Value; var newSingle = data[ModalComCidSingle].Value;
string? newMulti = data[ModalComCidMulti].Value; var newMulti = data[ModalComCidMulti].Value;
if (string.IsNullOrWhiteSpace(newSingle)) newSingle = null; if (string.IsNullOrWhiteSpace(newSingle)) newSingle = null;
if (string.IsNullOrWhiteSpace(newMulti)) newMulti = null; if (string.IsNullOrWhiteSpace(newMulti)) newMulti = null;
@ -156,7 +156,7 @@ public class ConfigModule : BotModuleBase {
var existing = db.BlocklistEntries var existing = db.BlocklistEntries
.Where(bl => bl.GuildId == user.Guild.Id && bl.UserId == user.Id).FirstOrDefault(); .Where(bl => bl.GuildId == user.Guild.Id && bl.UserId == user.Id).FirstOrDefault();
bool already = (existing != null) == setting; var already = (existing != null) == setting;
if (already) { if (already) {
await RespondAsync($":white_check_mark: User is already {(setting ? "" : "not ")}blocked.").ConfigureAwait(false); await RespondAsync($":white_check_mark: User is already {(setting ? "" : "not ")}blocked.").ConfigureAwait(false);
return; return;
@ -171,13 +171,13 @@ public class ConfigModule : BotModuleBase {
[SlashCommand("set-moderated", HelpPfxModOnly + "Set moderated mode on the server.")] [SlashCommand("set-moderated", HelpPfxModOnly + "Set moderated mode on the server.")]
public async Task CmdSetModerated([Summary(name: "enable", description: "The moderated mode setting.")] bool setting) { public async Task CmdSetModerated([Summary(name: "enable", description: "The moderated mode setting.")] bool setting) {
bool current = false; var current = false;
await DoDatabaseUpdate(Context, s => { await DoDatabaseUpdate(Context, s => {
current = s.Moderated; current = s.Moderated;
s.Moderated = setting; s.Moderated = setting;
}); });
bool already = setting == current; var already = setting == current;
if (already) { if (already) {
await RespondAsync($":white_check_mark: Moderated mode is already **{(setting ? "en" : "dis")}abled**."); await RespondAsync($":white_check_mark: Moderated mode is already **{(setting ? "en" : "dis")}abled**.");
} else { } else {
@ -204,7 +204,7 @@ public class ConfigModule : BotModuleBase {
result.AppendLine($"Server time zone: `{ (guildconf.GuildTimeZone ?? "Not set - using UTC") }`"); result.AppendLine($"Server time zone: `{ (guildconf.GuildTimeZone ?? "Not set - using UTC") }`");
result.AppendLine(); result.AppendLine();
bool hasMembers = Common.HasMostMembersDownloaded(guild); var hasMembers = Common.HasMostMembersDownloaded(guild);
result.Append(DoTestFor("Bot has obtained the user list", () => hasMembers)); result.Append(DoTestFor("Bot has obtained the user list", () => hasMembers));
result.AppendLine($" - Has `{guild.DownloadedMemberCount}` of `{guild.MemberCount}` members."); result.AppendLine($" - Has `{guild.DownloadedMemberCount}` of `{guild.MemberCount}` members.");
int bdayCount = default; int bdayCount = default;
@ -238,7 +238,7 @@ public class ConfigModule : BotModuleBase {
announcech = guild.GetTextChannel((ulong)(guildconf.AnnouncementChannel ?? 0)); announcech = guild.GetTextChannel((ulong)(guildconf.AnnouncementChannel ?? 0));
return announcech != null; return announcech != null;
})); }));
string disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>"; var disp = announcech == null ? "announcement channel" : $"<#{announcech.Id}>";
result.AppendLine(DoTestFor($"(Optional) Bot can send messages into { disp }", delegate { result.AppendLine(DoTestFor($"(Optional) Bot can send messages into { disp }", delegate {
if (announcech == null) return false; if (announcech == null) return false;
return guild.CurrentUser.GetPermissions(announcech).SendMessages; return guild.CurrentUser.GetPermissions(announcech).SendMessages;
@ -251,7 +251,7 @@ public class ConfigModule : BotModuleBase {
const int announceMsgPreviewLimit = 350; const int announceMsgPreviewLimit = 350;
static string prepareAnnouncePreview(string announce) { static string prepareAnnouncePreview(string announce) {
string trunc = announce.Length > announceMsgPreviewLimit ? announce[..announceMsgPreviewLimit] + "`(...)`" : announce; var trunc = announce.Length > announceMsgPreviewLimit ? announce[..announceMsgPreviewLimit] + "`(...)`" : announce;
var result = new StringBuilder(); var result = new StringBuilder();
foreach (var line in trunc.Split('\n')) foreach (var line in trunc.Split('\n'))
result.AppendLine($"> {line}"); result.AppendLine($"> {line}");

View file

@ -1,7 +1,6 @@
using Discord.Interactions; using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
public class HelpModule : BotModuleBase { public class HelpModule : BotModuleBase {
private const string TopMessage = private const string TopMessage =
"Thank you for using Birthday Bot!\n" + "Thank you for using Birthday Bot!\n" +
@ -33,12 +32,10 @@ public class HelpModule : BotModuleBase {
public async Task CmdHelp() { public async Task CmdHelp() {
const string DMWarn = "Please note that this bot works in servers only. " + const string DMWarn = "Please note that this bot works in servers only. " +
"The bot will not respond to any other commands within a DM."; "The bot will not respond to any other commands within a DM.";
string ver =
#if DEBUG #if DEBUG
"DEBUG flag set"; var ver = "DEBUG flag set";
#else #else
"v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3); var ver = "v" + System.Reflection.Assembly.GetExecutingAssembly().GetName().Version!.ToString(3);
#endif #endif
var result = new EmbedBuilder() var result = new EmbedBuilder()
.WithAuthor("Help & About") .WithAuthor("Help & About")
@ -48,6 +45,6 @@ public class HelpModule : BotModuleBase {
.AddField("Commands", RegularCommandsField) .AddField("Commands", RegularCommandsField)
.AddField("Moderator commands", ModCommandsField) .AddField("Moderator commands", ModCommandsField)
.Build(); .Build();
await RespondAsync(text: (Context.Channel is IDMChannel ? DMWarn : null), embed: result).ConfigureAwait(false); await RespondAsync(text: Context.Channel is IDMChannel ? DMWarn : null, embed: result).ConfigureAwait(false);
} }
} }

View file

@ -1,5 +1,4 @@
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
/// <summary> /// <summary>
/// An instance-less class meant to handle incoming submitted modals. /// An instance-less class meant to handle incoming submitted modals.
/// </summary> /// </summary>
@ -28,7 +27,6 @@ static class ModalResponder {
await handler(arg, channel, data).ConfigureAwait(false); await handler(arg, channel, data).ConfigureAwait(false);
} catch (Exception e) { } catch (Exception e) {
inst.Log(nameof(ModalResponder), $"Unhandled exception. {e}"); inst.Log(nameof(ModalResponder), $"Unhandled exception. {e}");
// TODO when implementing proper application error logging, see here
await arg.RespondAsync(ShardInstance.InternalError); await arg.RespondAsync(ShardInstance.InternalError);
} }
} }

View file

@ -2,7 +2,6 @@
using Discord.Interactions; using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
/// <summary> /// <summary>
/// Only users not on the blocklist or affected by moderator mode may use the command.<br/> /// Only users not on the blocklist or affected by moderator mode may use the command.<br/>
/// This is used in the <see cref="BotModuleBase"/> base class. Manually using it anywhere else is unnecessary. /// This is used in the <see cref="BotModuleBase"/> base class. Manually using it anywhere else is unnecessary.

View file

@ -1,7 +1,6 @@
using Discord.Interactions; using Discord.Interactions;
namespace BirthdayBot.ApplicationCommands; namespace BirthdayBot.ApplicationCommands;
/// <summary> /// <summary>
/// Implements the included precondition from Discord.Net, requiring a guild context while using our custom error message.<br/><br/> /// Implements the included precondition from Discord.Net, requiring a guild context while using our custom error message.<br/><br/>
/// Combining this with <see cref="RequireBotModeratorAttribute"/> is redundant. If possible, only use the latter instead. /// Combining this with <see cref="RequireBotModeratorAttribute"/> is redundant. If possible, only use the latter instead.

View file

@ -1,5 +1,4 @@
namespace BirthdayBot.BackgroundServices; namespace BirthdayBot.BackgroundServices;
abstract class BackgroundService { abstract class BackgroundService {
protected static SemaphoreSlim DbConcurrentOperationsLock { get; } = new(ShardManager.MaxConcurrentOperations); protected static SemaphoreSlim DbConcurrentOperationsLock { get; } = new(ShardManager.MaxConcurrentOperations);
protected ShardInstance ShardInstance { get; } protected ShardInstance ShardInstance { get; }

View file

@ -111,7 +111,7 @@ class BirthdayRoleUpdate : BackgroundService {
private static async Task<IEnumerable<SocketGuildUser>> UpdateGuildBirthdayRoles(SocketGuild g, SocketRole r, HashSet<ulong> toApply) { private static async Task<IEnumerable<SocketGuildUser>> UpdateGuildBirthdayRoles(SocketGuild g, SocketRole r, HashSet<ulong> toApply) {
var additions = new List<SocketGuildUser>(); var additions = new List<SocketGuildUser>();
try { try {
var removals = new List<SocketGuildUser>(); // TODO check if roles can be removed in-place instead of building a list first var removals = new List<SocketGuildUser>();
var no_ops = new HashSet<ulong>(); var no_ops = new HashSet<ulong>();
// Scan role for members no longer needing it // Scan role for members no longer needing it

View file

@ -1,7 +1,6 @@
using System.Text; using System.Text;
namespace BirthdayBot.BackgroundServices; namespace BirthdayBot.BackgroundServices;
/// <summary> /// <summary>
/// Reports user count statistics to external services on a shard by shard basis. /// Reports user count statistics to external services on a shard by shard basis.
/// </summary> /// </summary>

View file

@ -1,7 +1,6 @@
using System.Text; using System.Text;
namespace BirthdayBot; namespace BirthdayBot;
static class Common { static class Common {
/// <summary> /// <summary>
/// Formats a user's name to a consistent, readable format which makes use of their nickname. /// Formats a user's name to a consistent, readable format which makes use of their nickname.
@ -42,7 +41,7 @@ static class Common {
if (guild.MemberCount > 30) { if (guild.MemberCount > 30) {
// For guilds of size over 30, require 85% or more of the members to be known // For guilds of size over 30, require 85% or more of the members to be known
// (26/30, 42/50, 255/300, etc) // (26/30, 42/50, 255/300, etc)
int threshold = (int)(guild.MemberCount * 0.85); var threshold = (int)(guild.MemberCount * 0.85);
return guild.DownloadedMemberCount >= threshold; return guild.DownloadedMemberCount >= threshold;
} else { } else {
// For smaller guilds, fail if two or more members are missing // For smaller guilds, fail if two or more members are missing

View file

@ -6,7 +6,6 @@ using System.Reflection;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
namespace BirthdayBot; namespace BirthdayBot;
/// <summary> /// <summary>
/// Loads and holds configuration values. /// Loads and holds configuration values.
/// </summary> /// </summary>
@ -15,7 +14,6 @@ class Configuration {
public string BotToken { get; } public string BotToken { get; }
public string? DBotsToken { get; } public string? DBotsToken { get; }
public bool QuitOnFails { get; }
public int ShardStart { get; } public int ShardStart { get; }
public int ShardAmount { get; } public int ShardAmount { get; }
@ -47,7 +45,6 @@ class Configuration {
BotToken = ReadConfKey<string>(jc, nameof(BotToken), true); BotToken = ReadConfKey<string>(jc, nameof(BotToken), true);
DBotsToken = ReadConfKey<string>(jc, nameof(DBotsToken), false); DBotsToken = ReadConfKey<string>(jc, nameof(DBotsToken), false);
QuitOnFails = ReadConfKey<bool?>(jc, nameof(QuitOnFails), false) ?? false;
ShardTotal = args.ShardTotal ?? ReadConfKey<int?>(jc, nameof(ShardTotal), false) ?? 1; ShardTotal = args.ShardTotal ?? ReadConfKey<int?>(jc, nameof(ShardTotal), false) ?? 1;
if (ShardTotal < 1) throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer."); if (ShardTotal < 1) throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer.");

View file

@ -2,7 +2,6 @@
using Npgsql; using Npgsql;
namespace BirthdayBot.Data; namespace BirthdayBot.Data;
public class BotDatabaseContext : DbContext { public class BotDatabaseContext : DbContext {
private static readonly string _connectionString; private static readonly string _connectionString;

View file

@ -1,5 +1,4 @@
namespace BirthdayBot.Data; namespace BirthdayBot.Data;
internal static class Extensions { internal static class Extensions {
/// <summary> /// <summary>
/// Gets the corresponding <see cref="GuildConfig"/> for this guild, or a new one if one does not exist. /// Gets the corresponding <see cref="GuildConfig"/> for this guild, or a new one if one does not exist.

View file

@ -0,0 +1,4 @@
[*.cs]
generated_code = true
dotnet_analyzer_diagnostic.category-CodeQuality.severity = none
dotnet_diagnostic.CS1591.severity = none

View file

@ -2,7 +2,6 @@
using System.ComponentModel.DataAnnotations.Schema; using System.ComponentModel.DataAnnotations.Schema;
namespace BirthdayBot.Data; namespace BirthdayBot.Data;
[Table("user_birthdays")] [Table("user_birthdays")]
public class UserEntry { public class UserEntry {
[Key] [Key]

View file

@ -1,9 +0,0 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Style", "IDE0161:Convert to file-scoped namespace",
Scope = "namespace", Target = "~N:BirthdayBot.Data.Migrations")]

View file

@ -43,13 +43,14 @@ class Program {
public static void ProgramStop() { public static void ProgramStop() {
if (_stopping) return; if (_stopping) return;
_stopping = true; _stopping = true;
Log("Shutdown", "Commencing shutdown..."); Log(nameof(Program), "Shutting down...");
var dispose = Task.Run(_bot!.Dispose); var dispose = Task.Run(_bot!.Dispose);
if (!dispose.Wait(90000)) { if (!dispose.Wait(30000)) {
Log("Shutdown", "Normal shutdown has not concluded after 90 seconds. Will force quit."); Log(nameof(Program), "Disconnection is taking too long. Will force exit.");
Environment.ExitCode &= (int)ExitCodes.ForcedExit; Environment.ExitCode &= (int)ExitCodes.ForcedExit;
} }
Log(nameof(Program), $"Uptime: {BotUptime}");
Environment.Exit(Environment.ExitCode); Environment.Exit(Environment.ExitCode);
} }

View file

@ -1,14 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
https://go.microsoft.com/fwlink/?LinkID=208121.
-->
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<PublishProtocol>FileSystem</PublishProtocol>
<Configuration>Release</Configuration>
<Platform>Any CPU</Platform>
<TargetFramework>net6.0</TargetFramework>
<PublishDir>bin\Release\net6.0\publish\</PublishDir>
<SelfContained>false</SelfContained>
</PropertyGroup>
</Project>

View file

@ -46,7 +46,6 @@ public sealed class ShardInstance : IDisposable {
// Background task constructor begins background processing immediately. // Background task constructor begins background processing immediately.
_background = new ShardBackgroundWorker(this); _background = new ShardBackgroundWorker(this);
Log(nameof(ShardInstance), "Instance created.");
} }
/// <summary> /// <summary>
@ -66,7 +65,6 @@ public sealed class ShardInstance : IDisposable {
DiscordClient.LogoutAsync().Wait(5000); DiscordClient.LogoutAsync().Wait(5000);
DiscordClient.Dispose(); DiscordClient.Dispose();
_interactionService.Dispose(); _interactionService.Dispose();
Log(nameof(ShardInstance), "Instance disposed.");
} }
internal void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message); internal void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message);
@ -127,7 +125,6 @@ public sealed class ShardInstance : IDisposable {
await _interactionService.ExecuteCommandAsync(context, _services).ConfigureAwait(false); await _interactionService.ExecuteCommandAsync(context, _services).ConfigureAwait(false);
} catch (Exception e) { } catch (Exception e) {
Log(nameof(DiscordClient_InteractionCreated), $"Unhandled exception. {e}"); Log(nameof(DiscordClient_InteractionCreated), $"Unhandled exception. {e}");
// TODO when implementing proper application error logging, see here
if (arg.Type == InteractionType.ApplicationCommand) { if (arg.Type == InteractionType.ApplicationCommand) {
if (arg.HasResponded) await arg.ModifyOriginalResponseAsync(prop => prop.Content = InternalError); if (arg.HasResponded) await arg.ModifyOriginalResponseAsync(prop => prop.Content = InternalError);
else await arg.RespondAsync(InternalError); else await arg.RespondAsync(InternalError);
@ -161,7 +158,6 @@ public sealed class ShardInstance : IDisposable {
await context.Interaction.RespondAsync(errReply, ephemeral: true).ConfigureAwait(false); await context.Interaction.RespondAsync(errReply, ephemeral: true).ConfigureAwait(false);
} else { } else {
// Generic error response // Generic error response
// TODO when implementing proper application error logging, see here
var ia = context.Interaction; var ia = context.Interaction;
if (ia.HasResponded) await ia.ModifyOriginalResponseAsync(p => p.Content = InternalError).ConfigureAwait(false); if (ia.HasResponded) await ia.ModifyOriginalResponseAsync(p => p.Content = InternalError).ConfigureAwait(false);
else await ia.RespondAsync(InternalError).ConfigureAwait(false); else await ia.RespondAsync(InternalError).ConfigureAwait(false);

View file

@ -13,12 +13,7 @@ class ShardManager : IDisposable {
/// <summary> /// <summary>
/// Number of seconds between each time the status task runs, in seconds. /// Number of seconds between each time the status task runs, in seconds.
/// </summary> /// </summary>
private const int StatusInterval = 60; private const int StatusInterval = 90;
/// <summary>
/// Number of shards allowed to be destroyed before the program may close itself, if configured.
/// </summary>
private const int MaxDestroyedShards = 10; // TODO make configurable
/// <summary> /// <summary>
/// Number of concurrent shard startups to happen on each check. /// Number of concurrent shard startups to happen on each check.
@ -28,8 +23,7 @@ class ShardManager : IDisposable {
/// <summary> /// <summary>
/// Amount of time without a completed background service run before a shard instance /// Amount of time without a completed background service run before a shard instance
/// is considered "dead" and tasked to be removed. A fraction of this value is also used /// is considered "dead" and tasked to be removed.
/// to determine when a shard is "slow".
/// </summary> /// </summary>
private static readonly TimeSpan DeadShardThreshold = new(0, 20, 0); private static readonly TimeSpan DeadShardThreshold = new(0, 20, 0);
@ -43,7 +37,6 @@ class ShardManager : IDisposable {
private readonly Task _statusTask; private readonly Task _statusTask;
private readonly CancellationTokenSource _mainCancel; private readonly CancellationTokenSource _mainCancel;
private int _destroyedShards = 0;
internal Configuration Config { get; } internal Configuration Config { get; }
@ -90,8 +83,6 @@ class ShardManager : IDisposable {
/// Creates and sets up a new shard instance. /// Creates and sets up a new shard instance.
/// </summary> /// </summary>
private async Task<ShardInstance> InitializeShard(int shardId) { private async Task<ShardInstance> InitializeShard(int shardId) {
ShardInstance newInstance;
var clientConf = new DiscordSocketConfig() { var clientConf = new DiscordSocketConfig() {
ShardId = shardId, ShardId = shardId,
TotalShards = Config.ShardTotal, TotalShards = Config.ShardTotal,
@ -106,8 +97,8 @@ class ShardManager : IDisposable {
.AddSingleton(s => new DiscordSocketClient(clientConf)) .AddSingleton(s => new DiscordSocketClient(clientConf))
.AddSingleton(s => new InteractionService(s.GetRequiredService<DiscordSocketClient>())) .AddSingleton(s => new InteractionService(s.GetRequiredService<DiscordSocketClient>()))
.BuildServiceProvider(); .BuildServiceProvider();
newInstance = services.GetRequiredService<ShardInstance>(); var newInstance = services.GetRequiredService<ShardInstance>();
await newInstance.StartAsync().ConfigureAwait(false); await newInstance.StartAsync();
return newInstance; return newInstance;
} }
@ -120,105 +111,62 @@ class ShardManager : IDisposable {
return null; return null;
} }
#region Status checking and display
private struct GuildStatusData {
public int GuildCount;
public TimeSpan LastTaskRunTime;
public string? ExecutingTask;
}
private static string StatusDisplay(IEnumerable<int> guildList, Dictionary<int, GuildStatusData> guildInfo, bool showDetail) {
if (!guildList.Any()) return "--";
var result = new StringBuilder();
foreach (var item in guildList) {
result.Append(item.ToString("00") + " ");
if (showDetail) {
result.Remove(result.Length - 1, 1);
result.Append($"[{Math.Floor(guildInfo[item].LastTaskRunTime.TotalSeconds):000}s");
if (guildInfo[item].ExecutingTask != null)
result.Append($" {guildInfo[item].ExecutingTask}");
result.Append("] ");
}
}
if (result.Length > 0) result.Remove(result.Length - 1, 1);
return result.ToString();
}
private async Task StatusLoop() { private async Task StatusLoop() {
try { try {
while (!_mainCancel.IsCancellationRequested) { while (!_mainCancel.IsCancellationRequested) {
Log($"Bot uptime: {Program.BotUptime}"); Log($"Uptime: {Program.BotUptime}");
// Iterate through shard list, extract data // Iterate through shards, create report on each
var guildInfo = new Dictionary<int, GuildStatusData>(); var shardStatuses = new StringBuilder();
var now = DateTimeOffset.UtcNow;
var nullShards = new List<int>(); var nullShards = new List<int>();
foreach (var item in _shards) { var deadShards = new List<int>();
if (item.Value == null) { for (var i = 0; i < _shards.Count; i++) {
nullShards.Add(item.Key); shardStatuses.Append($"Shard {i:00}: ");
if (_shards[i] == null) {
shardStatuses.AppendLine("Inactive.");
nullShards.Add(i);
continue; continue;
} }
var shard = item.Value;
guildInfo[item.Key] = new GuildStatusData {
GuildCount = shard.DiscordClient.Guilds.Count,
LastTaskRunTime = now - shard.LastBackgroundRun,
ExecutingTask = shard.CurrentExecutingService
};
}
// Process info
var guildCounts = guildInfo.Select(i => i.Value.GuildCount);
var guildTotal = guildCounts.Sum();
var guildAverage = guildCounts.Any() ? guildCounts.Average() : 0;
Log($"Currently in {guildTotal} guilds. Average shard load: {guildAverage:0.0}.");
// Health report
var goodShards = new List<int>();
var badShards = new List<int>(); // shards with low connection score OR long time since last work
var deadShards = new List<int>(); // shards to destroy and reinitialize
foreach (var item in guildInfo) {
var lastRun = item.Value.LastTaskRunTime;
var shard = _shards[i]!;
var client = shard.DiscordClient;
shardStatuses.Append($"{Enum.GetName(typeof(ConnectionState), client.ConnectionState)} ({client.Latency:000}ms).");
shardStatuses.Append($" Guilds: {client.Guilds.Count}.");
shardStatuses.Append($" Background: {shard.CurrentExecutingService ?? "Idle"}");
var lastRun = DateTimeOffset.UtcNow - shard.LastBackgroundRun;
if (lastRun > DeadShardThreshold / 3) { if (lastRun > DeadShardThreshold / 3) {
badShards.Add(item.Key); // Formerly known as a 'slow' shard
shardStatuses.Append($", heartbeat {Math.Floor(lastRun.TotalMinutes):00}m ago.");
// Consider a shard dead after a long span without background activity
if (lastRun > DeadShardThreshold)
deadShards.Add(item.Key);
} else { } else {
goodShards.Add(item.Key); shardStatuses.Append('.');
}
shardStatuses.AppendLine();
if (lastRun > DeadShardThreshold) {
shardStatuses.AppendLine($"Shard {i:00} marked for disposal.");
deadShards.Add(i);
} }
} }
Log("Online: " + StatusDisplay(goodShards, guildInfo, false)); Log(shardStatuses.ToString().TrimEnd());
if (badShards.Count > 0) Log("Slow: " + StatusDisplay(badShards, guildInfo, true));
if (deadShards.Count > 0) Log("Dead: " + StatusDisplay(deadShards, guildInfo, false));
if (nullShards.Count > 0) Log("Offline: " + StatusDisplay(nullShards, guildInfo, false));
// Remove dead shards // Remove dead shards
foreach (var dead in deadShards) { foreach (var dead in deadShards) {
_shards[dead]!.Dispose(); _shards[dead]!.Dispose();
_shards[dead] = null; _shards[dead] = null;
_destroyedShards++;
}
if (Config.QuitOnFails && _destroyedShards > MaxDestroyedShards) {
Environment.ExitCode = (int)Program.ExitCodes.DeadShardThreshold;
Program.ProgramStop();
} else {
// Start up any missing shards
var startAllowance = MaxConcurrentOperations;
foreach (var id in nullShards) {
// 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;
}
} }
await Task.Delay(StatusInterval * 1000, _mainCancel.Token).ConfigureAwait(false); // Start null shards, a few at at time
var startAllowance = MaxConcurrentOperations;
foreach (var id in nullShards) {
if (startAllowance-- > 0) {
_shards[id] = await InitializeShard(id);
} else break;
}
await Task.Delay(StatusInterval * 1000, _mainCancel.Token);
} }
} catch (TaskCanceledException) { } } catch (TaskCanceledException) { }
} }
#endregion
} }