mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-24 01:14:12 +00:00
Modified all background services
-Removed a number of diagnostic messages -Removed ConnectionStatus, connection scores, etc. -Modified work intervals for certain background tasks -Updated code style
This commit is contained in:
parent
8cff530a7c
commit
6f34fbe657
10 changed files with 452 additions and 612 deletions
|
@ -1,16 +1,14 @@
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices
|
namespace BirthdayBot.BackgroundServices;
|
||||||
{
|
|
||||||
abstract class BackgroundService
|
abstract class BackgroundService {
|
||||||
{
|
|
||||||
protected ShardInstance ShardInstance { get; }
|
protected ShardInstance ShardInstance { get; }
|
||||||
|
|
||||||
public BackgroundService(ShardInstance instance) => ShardInstance = instance;
|
public BackgroundService(ShardInstance instance) => ShardInstance = instance;
|
||||||
|
|
||||||
protected void Log(string message) => ShardInstance.Log(GetType().Name, message);
|
protected void Log(string message) => ShardInstance.Log(GetType().Name, message);
|
||||||
|
|
||||||
public abstract Task OnTick(CancellationToken token);
|
public abstract Task OnTick(int tickCount, CancellationToken token);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,38 +8,32 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices
|
namespace BirthdayBot.BackgroundServices;
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Core automatic functionality of the bot. Manages role memberships based on birthday information,
|
/// Core automatic functionality of the bot. Manages role memberships based on birthday information,
|
||||||
/// and optionally sends the announcement message to appropriate guilds.
|
/// and optionally sends the announcement message to appropriate guilds.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class BirthdayRoleUpdate : BackgroundService
|
class BirthdayRoleUpdate : BackgroundService {
|
||||||
{
|
|
||||||
public BirthdayRoleUpdate(ShardInstance instance) : base(instance) { }
|
public BirthdayRoleUpdate(ShardInstance instance) : base(instance) { }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Processes birthday updates for all available guilds synchronously.
|
/// Processes birthday updates for all available guilds synchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override async Task OnTick(CancellationToken token)
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
{
|
|
||||||
var exs = new List<Exception>();
|
var exs = new List<Exception>();
|
||||||
foreach (var guild in ShardInstance.DiscordClient.Guilds)
|
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
||||||
{
|
if (ShardInstance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) {
|
||||||
if (ShardInstance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected)
|
|
||||||
{
|
|
||||||
Log("Client is not connected. Stopping early.");
|
Log("Client is not connected. Stopping early.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Single guilds are fully processed and are not interrupted by task cancellation.
|
// Check task cancellation here. Processing during a single guild is never interrupted.
|
||||||
if (token.IsCancellationRequested) throw new TaskCanceledException();
|
if (token.IsCancellationRequested) throw new TaskCanceledException();
|
||||||
try
|
|
||||||
{
|
try {
|
||||||
await ProcessGuildAsync(guild).ConfigureAwait(false);
|
await ProcessGuildAsync(guild).ConfigureAwait(false);
|
||||||
}
|
} catch (Exception ex) {
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Catch all exceptions per-guild but continue processing, throw at end.
|
// Catch all exceptions per-guild but continue processing, throw at end.
|
||||||
exs.Add(ex);
|
exs.Add(ex);
|
||||||
}
|
}
|
||||||
|
@ -57,11 +51,11 @@ namespace BirthdayBot.BackgroundServices
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Main method where actual guild processing occurs.
|
/// Main method where actual guild processing occurs.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static async Task<PGDiagnostic> ProcessGuildAsync(SocketGuild guild)
|
private static async Task<PGDiagnostic> ProcessGuildAsync(SocketGuild guild) {
|
||||||
{
|
|
||||||
var diag = new PGDiagnostic();
|
var diag = new PGDiagnostic();
|
||||||
|
|
||||||
// Load guild information - stop if there is none (bot never previously used in guild)
|
// Load guild information - stop if local cache is unavailable.
|
||||||
|
if (!Common.HasMostMembersDownloaded(guild)) return diag;
|
||||||
var gc = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
var gc = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
|
||||||
if (gc == null) return diag;
|
if (gc == null) return diag;
|
||||||
|
|
||||||
|
@ -80,14 +74,11 @@ namespace BirthdayBot.BackgroundServices
|
||||||
|
|
||||||
IEnumerable<SocketGuildUser> announcementList;
|
IEnumerable<SocketGuildUser> announcementList;
|
||||||
// Update roles as appropriate
|
// Update roles as appropriate
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
var updateResult = await UpdateGuildBirthdayRoles(guild, role, birthdays).ConfigureAwait(false);
|
var updateResult = await UpdateGuildBirthdayRoles(guild, role, birthdays).ConfigureAwait(false);
|
||||||
announcementList = updateResult.Item1;
|
announcementList = updateResult.Item1;
|
||||||
diag.RoleApplyResult = updateResult.Item2; // statistics
|
diag.RoleApplyResult = updateResult.Item2; // statistics
|
||||||
}
|
} catch (Discord.Net.HttpException ex) {
|
||||||
catch (Discord.Net.HttpException ex)
|
|
||||||
{
|
|
||||||
diag.RoleApply = ex.Message;
|
diag.RoleApply = ex.Message;
|
||||||
return diag;
|
return diag;
|
||||||
}
|
}
|
||||||
|
@ -98,14 +89,11 @@ namespace BirthdayBot.BackgroundServices
|
||||||
var announceping = gc.AnnouncePing;
|
var announceping = gc.AnnouncePing;
|
||||||
SocketTextChannel channel = null;
|
SocketTextChannel channel = null;
|
||||||
if (gc.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gc.AnnounceChannelId.Value);
|
if (gc.AnnounceChannelId.HasValue) channel = guild.GetTextChannel(gc.AnnounceChannelId.Value);
|
||||||
if (announcementList.Any())
|
if (announcementList.Any()) {
|
||||||
{
|
|
||||||
var announceResult =
|
var announceResult =
|
||||||
await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList).ConfigureAwait(false);
|
await AnnounceBirthdaysAsync(announce, announceping, channel, announcementList).ConfigureAwait(false);
|
||||||
diag.Announcement = announceResult;
|
diag.Announcement = announceResult;
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
diag.Announcement = "No new role additions. Announcement not needed.";
|
diag.Announcement = "No new role additions. Announcement not needed.";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -115,19 +103,16 @@ namespace BirthdayBot.BackgroundServices
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Checks if the bot may be allowed to alter roles.
|
/// Checks if the bot may be allowed to alter roles.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static string CheckCorrectRoleSettings(SocketGuild guild, SocketRole role)
|
private static string CheckCorrectRoleSettings(SocketGuild guild, SocketRole role) {
|
||||||
{
|
if (role == null) return "Birthday role is not configured, or has gone missing.";
|
||||||
if (role == null) return "Designated role is not set, or target role cannot be found.";
|
|
||||||
|
|
||||||
if (!guild.CurrentUser.GuildPermissions.ManageRoles)
|
if (!guild.CurrentUser.GuildPermissions.ManageRoles) {
|
||||||
{
|
|
||||||
return "Bot does not have the 'Manage Roles' permission.";
|
return "Bot does not have the 'Manage Roles' permission.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check potential role order conflict
|
// Check potential role order conflict
|
||||||
if (role.Position >= guild.CurrentUser.Hierarchy)
|
if (role.Position >= guild.CurrentUser.Hierarchy) {
|
||||||
{
|
return "Can't access the birthday role. Is it above the bot's permissions?";
|
||||||
return "Bot is unable to access the designated role due to permission hierarchy.";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
|
@ -137,20 +122,17 @@ namespace BirthdayBot.BackgroundServices
|
||||||
/// Gets all known users from the given guild and returns a list including only those who are
|
/// Gets all known users from the given guild and returns a list including only those who are
|
||||||
/// currently experiencing a birthday in the respective time zone.
|
/// currently experiencing a birthday in the respective time zone.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserConfiguration> guildUsers, string defaultTzStr)
|
private static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<GuildUserConfiguration> guildUsers, string defaultTzStr) {
|
||||||
{
|
|
||||||
var birthdayUsers = new HashSet<ulong>();
|
var birthdayUsers = new HashSet<ulong>();
|
||||||
|
|
||||||
DateTimeZone defaultTz = null;
|
DateTimeZone defaultTz = null;
|
||||||
if (defaultTzStr != null) defaultTz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(defaultTzStr);
|
if (defaultTzStr != null) defaultTz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(defaultTzStr);
|
||||||
defaultTz ??= DateTimeZoneProviders.Tzdb.GetZoneOrNull("UTC");
|
defaultTz ??= DateTimeZoneProviders.Tzdb.GetZoneOrNull("UTC");
|
||||||
|
|
||||||
foreach (var item in guildUsers)
|
foreach (var item in guildUsers) {
|
||||||
{
|
|
||||||
// Determine final time zone to use for calculation
|
// Determine final time zone to use for calculation
|
||||||
DateTimeZone tz = null;
|
DateTimeZone tz = null;
|
||||||
if (item.TimeZone != null)
|
if (item.TimeZone != null) {
|
||||||
{
|
|
||||||
// Try user-provided time zone
|
// Try user-provided time zone
|
||||||
tz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(item.TimeZone);
|
tz = DateTimeZoneProviders.Tzdb.GetZoneOrNull(item.TimeZone);
|
||||||
}
|
}
|
||||||
|
@ -161,13 +143,11 @@ namespace BirthdayBot.BackgroundServices
|
||||||
|
|
||||||
var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
||||||
// Special case: If birthday is February 29 and it's not a leap year, recognize it on March 1st
|
// Special case: If birthday is February 29 and it's not a leap year, recognize it on March 1st
|
||||||
if (targetMonth == 2 && targetDay == 29 && !DateTime.IsLeapYear(checkNow.Year))
|
if (targetMonth == 2 && targetDay == 29 && !DateTime.IsLeapYear(checkNow.Year)) {
|
||||||
{
|
|
||||||
targetMonth = 3;
|
targetMonth = 3;
|
||||||
targetDay = 1;
|
targetDay = 1;
|
||||||
}
|
}
|
||||||
if (targetMonth == checkNow.Month && targetDay == checkNow.Day)
|
if (targetMonth == checkNow.Month && targetDay == checkNow.Day) {
|
||||||
{
|
|
||||||
birthdayUsers.Add(item.UserId);
|
birthdayUsers.Add(item.UserId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -183,27 +163,23 @@ namespace BirthdayBot.BackgroundServices
|
||||||
/// Second item: Counts of users who have had roles added/removed, used for operation reporting.
|
/// Second item: Counts of users who have had roles added/removed, used for operation reporting.
|
||||||
/// </returns>
|
/// </returns>
|
||||||
private static async Task<(IEnumerable<SocketGuildUser>, (int, int))> UpdateGuildBirthdayRoles(
|
private static async Task<(IEnumerable<SocketGuildUser>, (int, int))> UpdateGuildBirthdayRoles(
|
||||||
SocketGuild g, SocketRole r, HashSet<ulong> names)
|
SocketGuild g, SocketRole r, HashSet<ulong> names) {
|
||||||
{
|
|
||||||
// Check members currently with the role. Figure out which users to remove it from.
|
// Check members currently with the role. Figure out which users to remove it from.
|
||||||
var roleRemoves = new List<SocketGuildUser>();
|
var roleRemoves = new List<SocketGuildUser>();
|
||||||
var roleKeeps = new HashSet<ulong>();
|
var roleKeeps = new HashSet<ulong>();
|
||||||
foreach (var member in r.Members)
|
foreach (var member in r.Members) {
|
||||||
{
|
|
||||||
if (!names.Contains(member.Id)) roleRemoves.Add(member);
|
if (!names.Contains(member.Id)) roleRemoves.Add(member);
|
||||||
else roleKeeps.Add(member.Id);
|
else roleKeeps.Add(member.Id);
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Can we remove during the iteration instead of after? investigate later...
|
// TODO Can we remove during the iteration instead of after? investigate later...
|
||||||
foreach (var user in roleRemoves)
|
foreach (var user in roleRemoves) {
|
||||||
{
|
|
||||||
await user.RemoveRoleAsync(r).ConfigureAwait(false);
|
await user.RemoveRoleAsync(r).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply role to members not already having it. Prepare announcement list.
|
// Apply role to members not already having it. Prepare announcement list.
|
||||||
var newBirthdays = new List<SocketGuildUser>();
|
var newBirthdays = new List<SocketGuildUser>();
|
||||||
foreach (var target in names)
|
foreach (var target in names) {
|
||||||
{
|
|
||||||
var member = g.GetUser(target);
|
var member = g.GetUser(target);
|
||||||
if (member == null) continue;
|
if (member == null) continue;
|
||||||
if (roleKeeps.Contains(member.Id)) continue; // already has role - do nothing
|
if (roleKeeps.Contains(member.Id)) continue; // already has role - do nothing
|
||||||
|
@ -223,9 +199,8 @@ namespace BirthdayBot.BackgroundServices
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <returns>The message to place into operation status log.</returns>
|
/// <returns>The message to place into operation status log.</returns>
|
||||||
private static async Task<string> AnnounceBirthdaysAsync(
|
private static async Task<string> AnnounceBirthdaysAsync(
|
||||||
(string, string) announce, bool announcePing, SocketTextChannel c, IEnumerable<SocketGuildUser> names)
|
(string, string) announce, bool announcePing, SocketTextChannel c, IEnumerable<SocketGuildUser> names) {
|
||||||
{
|
if (c == null) return "Announcement channel is not configured, or has gone missing.";
|
||||||
if (c == null) return "Announcement channel is not set, or previous announcement channel has been deleted.";
|
|
||||||
|
|
||||||
string announceMsg;
|
string announceMsg;
|
||||||
if (names.Count() == 1) announceMsg = announce.Item1 ?? announce.Item2 ?? DefaultAnnounce;
|
if (names.Count() == 1) announceMsg = announce.Item1 ?? announce.Item2 ?? DefaultAnnounce;
|
||||||
|
@ -240,27 +215,22 @@ namespace BirthdayBot.BackgroundServices
|
||||||
namestrings.Sort(StringComparer.OrdinalIgnoreCase);
|
namestrings.Sort(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var namedisplay = new StringBuilder();
|
var namedisplay = new StringBuilder();
|
||||||
foreach (var item in namestrings)
|
foreach (var item in namestrings) {
|
||||||
{
|
|
||||||
namedisplay.Append(", ");
|
namedisplay.Append(", ");
|
||||||
namedisplay.Append(item);
|
namedisplay.Append(item);
|
||||||
}
|
}
|
||||||
namedisplay.Remove(0, 2); // Remove initial comma and space
|
namedisplay.Remove(0, 2); // Remove initial comma and space
|
||||||
|
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString())).ConfigureAwait(false);
|
await c.SendMessageAsync(announceMsg.Replace("%n", namedisplay.ToString())).ConfigureAwait(false);
|
||||||
return null;
|
return null;
|
||||||
}
|
} catch (Discord.Net.HttpException ex) {
|
||||||
catch (Discord.Net.HttpException ex)
|
|
||||||
{
|
|
||||||
// Directly use the resulting exception message in the operation status log
|
// Directly use the resulting exception message in the operation status log
|
||||||
return ex.Message;
|
return ex.Message;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class PGDiagnostic
|
private class PGDiagnostic {
|
||||||
{
|
|
||||||
const string DefaultValue = "--";
|
const string DefaultValue = "--";
|
||||||
|
|
||||||
public string RoleCheck = DefaultValue;
|
public string RoleCheck = DefaultValue;
|
||||||
|
@ -269,8 +239,7 @@ namespace BirthdayBot.BackgroundServices
|
||||||
public (int, int)? RoleApplyResult;
|
public (int, int)? RoleApplyResult;
|
||||||
public string Announcement = DefaultValue;
|
public string Announcement = DefaultValue;
|
||||||
|
|
||||||
public string Export()
|
public string Export() {
|
||||||
{
|
|
||||||
var result = new StringBuilder();
|
var result = new StringBuilder();
|
||||||
result.AppendLine("Test result:");
|
result.AppendLine("Test result:");
|
||||||
result.AppendLine("Check role permissions: " + (RoleCheck ?? ":white_check_mark:"));
|
result.AppendLine("Check role permissions: " + (RoleCheck ?? ":white_check_mark:"));
|
||||||
|
@ -285,4 +254,3 @@ namespace BirthdayBot.BackgroundServices
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -1,44 +0,0 @@
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// Keeps track of the connection status, assigning a score based on either the connection's
|
|
||||||
/// longevity or the amount of time it has remained persistently disconnected.
|
|
||||||
/// </summary>
|
|
||||||
class ConnectionStatus : BackgroundService
|
|
||||||
{
|
|
||||||
// About 3 minutes
|
|
||||||
public const int StableScore = 180 / ShardBackgroundWorker.Interval;
|
|
||||||
|
|
||||||
public bool Stable { get { return Score >= StableScore; } }
|
|
||||||
public int Score { get; private set; }
|
|
||||||
|
|
||||||
public ConnectionStatus(ShardInstance instance) : base(instance) { }
|
|
||||||
|
|
||||||
public override Task OnTick(CancellationToken token)
|
|
||||||
{
|
|
||||||
switch (ShardInstance.DiscordClient.ConnectionState)
|
|
||||||
{
|
|
||||||
case Discord.ConnectionState.Connected:
|
|
||||||
if (Score < 0) Score = 0;
|
|
||||||
Score++;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
if (Score > 0) Score = 0;
|
|
||||||
Score--;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// In response to a disconnection event, will immediately reset a positive score to zero.
|
|
||||||
/// </summary>
|
|
||||||
public void Disconnected()
|
|
||||||
{
|
|
||||||
if (Score > 0) Score = 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -7,47 +7,35 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices
|
namespace BirthdayBot.BackgroundServices;
|
||||||
{
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Data retention adherence service:
|
|
||||||
/// Automatically removes database information for guilds that have not been accessed in a long time.
|
/// Automatically removes database information for guilds that have not been accessed in a long time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class DataRetention : BackgroundService
|
class DataRetention : BackgroundService {
|
||||||
{
|
|
||||||
private static readonly SemaphoreSlim _updateLock = new(ShardManager.MaxConcurrentOperations);
|
private static readonly SemaphoreSlim _updateLock = new(ShardManager.MaxConcurrentOperations);
|
||||||
const int ProcessInterval = 3600 / ShardBackgroundWorker.Interval; // Process about once per hour
|
const int ProcessInterval = 3600 / ShardBackgroundWorker.Interval; // Process about once per hour
|
||||||
private int _tickCount = -1;
|
const int Stagger = 3; // How many ticks in between each group of guilds to stagger processing.
|
||||||
|
|
||||||
public DataRetention(ShardInstance instance) : base(instance) { }
|
public DataRetention(ShardInstance instance) : base(instance) { }
|
||||||
|
|
||||||
public override async Task OnTick(CancellationToken token)
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
{
|
// On each tick, run only a set group of guilds, each group still processed every ProcessInterval ticks.
|
||||||
if ((++_tickCount + ShardInstance.ShardId * 3) % ProcessInterval != 0)
|
if ((tickCount + ShardInstance.ShardId * Stagger) % ProcessInterval != 0) return;
|
||||||
{
|
|
||||||
// Do not process on every tick.
|
|
||||||
// Stagger processing based on shard ID, to not choke the background processing task.
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
// A semaphore is used to restrict this work being done concurrently on other shards
|
// A semaphore is used to restrict this work being done concurrently on other shards
|
||||||
// to avoid putting pressure on the SQL connection pool. Clearing old database information
|
// to avoid putting pressure on the SQL connection pool. Clearing old database information
|
||||||
// ultimately is a low priority among other tasks.
|
// ultimately is a low priority among other tasks.
|
||||||
await _updateLock.WaitAsync(token).ConfigureAwait(false);
|
await _updateLock.WaitAsync(token).ConfigureAwait(false);
|
||||||
}
|
} catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException) {
|
||||||
catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException)
|
// Caller does not expect the exception that SemaphoreSlim throws...
|
||||||
{
|
|
||||||
// Calling thread does not expect the exception that SemaphoreSlim throws...
|
|
||||||
throw new TaskCanceledException();
|
throw new TaskCanceledException();
|
||||||
}
|
}
|
||||||
try
|
try {
|
||||||
{
|
|
||||||
// Build a list of all values to update
|
// Build a list of all values to update
|
||||||
var updateList = new Dictionary<ulong, List<ulong>>();
|
var updateList = new Dictionary<ulong, List<ulong>>();
|
||||||
foreach (var g in ShardInstance.DiscordClient.Guilds)
|
foreach (var g in ShardInstance.DiscordClient.Guilds) {
|
||||||
{
|
|
||||||
// Get list of IDs for all users who exist in the database and currently exist in the guild
|
// Get list of IDs for all users who exist in the database and currently exist in the guild
|
||||||
var userList = GuildUserConfiguration.LoadAllAsync(g.Id);
|
var userList = GuildUserConfiguration.LoadAllAsync(g.Id);
|
||||||
var guildUserIds = from gu in g.Users select gu.Id;
|
var guildUserIds = from gu in g.Users select gu.Id;
|
||||||
|
@ -76,8 +64,7 @@ namespace BirthdayBot.BackgroundServices
|
||||||
// Do actual updates
|
// Do actual updates
|
||||||
int updatedGuilds = 0;
|
int updatedGuilds = 0;
|
||||||
int updatedUsers = 0;
|
int updatedUsers = 0;
|
||||||
foreach (var item in updateList)
|
foreach (var item in updateList) {
|
||||||
{
|
|
||||||
var guild = item.Key;
|
var guild = item.Key;
|
||||||
var userlist = item.Value;
|
var userlist = item.Value;
|
||||||
|
|
||||||
|
@ -85,8 +72,7 @@ namespace BirthdayBot.BackgroundServices
|
||||||
updatedGuilds += await cUpdateGuild.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
updatedGuilds += await cUpdateGuild.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
|
|
||||||
pUpdateGU_g.Value = (long)guild;
|
pUpdateGU_g.Value = (long)guild;
|
||||||
foreach (var userid in userlist)
|
foreach (var userid in userlist) {
|
||||||
{
|
|
||||||
pUpdateGU_u.Value = (long)userid;
|
pUpdateGU_u.Value = (long)userid;
|
||||||
updatedUsers += await cUpdateGuildUser.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
updatedUsers += await cUpdateGuildUser.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
|
@ -97,39 +83,31 @@ namespace BirthdayBot.BackgroundServices
|
||||||
// Delete all old values - expects referencing tables to have 'on delete cascade'
|
// Delete all old values - expects referencing tables to have 'on delete cascade'
|
||||||
using var t = db.BeginTransaction();
|
using var t = db.BeginTransaction();
|
||||||
int staleGuilds, staleUsers;
|
int staleGuilds, staleUsers;
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand()) {
|
||||||
{
|
|
||||||
// Delete data for guilds not seen in 4 weeks
|
// Delete data for guilds not seen in 4 weeks
|
||||||
c.CommandText = $"delete from {GuildConfiguration.BackingTable} where (now() - interval '28 days') > last_seen";
|
c.CommandText = $"delete from {GuildConfiguration.BackingTable} where (now() - interval '28 days') > last_seen";
|
||||||
staleGuilds = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
staleGuilds = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
using (var c = db.CreateCommand())
|
using (var c = db.CreateCommand()) {
|
||||||
{
|
|
||||||
// Delete data for users not seen in 8 weeks
|
// Delete data for users not seen in 8 weeks
|
||||||
c.CommandText = $"delete from {GuildUserConfiguration.BackingTable} where (now() - interval '56 days') > last_seen";
|
c.CommandText = $"delete from {GuildUserConfiguration.BackingTable} where (now() - interval '56 days') > last_seen";
|
||||||
staleUsers = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
staleUsers = await c.ExecuteNonQueryAsync(CancellationToken.None).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
if (staleGuilds != 0 || staleUsers != 0)
|
if (staleGuilds != 0 || staleUsers != 0) {
|
||||||
{
|
|
||||||
resultText.Append(" Discarded ");
|
resultText.Append(" Discarded ");
|
||||||
if (staleGuilds != 0)
|
if (staleGuilds != 0) {
|
||||||
{
|
|
||||||
resultText.Append($"{staleGuilds} guilds");
|
resultText.Append($"{staleGuilds} guilds");
|
||||||
if (staleUsers != 0) resultText.Append(", ");
|
if (staleUsers != 0) resultText.Append(", ");
|
||||||
}
|
}
|
||||||
if (staleUsers != 0)
|
if (staleUsers != 0) {
|
||||||
{
|
|
||||||
resultText.Append($"{staleUsers} standalone users");
|
resultText.Append($"{staleUsers} standalone users");
|
||||||
}
|
}
|
||||||
resultText.Append('.');
|
resultText.Append('.');
|
||||||
}
|
}
|
||||||
t.Commit();
|
t.Commit();
|
||||||
Log(resultText.ToString());
|
Log(resultText.ToString());
|
||||||
}
|
} finally {
|
||||||
finally
|
|
||||||
{
|
|
||||||
_updateLock.Release();
|
_updateLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -4,55 +4,47 @@ using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
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>
|
||||||
class ExternalStatisticsReporting : BackgroundService
|
class ExternalStatisticsReporting : BackgroundService {
|
||||||
{
|
const int ProcessInterval = 1200 / ShardBackgroundWorker.Interval; // Process every ~20 minutes
|
||||||
const int ProcessInterval = 600 / ShardBackgroundWorker.Interval; // Process every ~5 minutes
|
const int ProcessOffset = 300 / ShardBackgroundWorker.Interval; // Begin processing 5 minutes after shard start
|
||||||
private int _tickCount = 0;
|
|
||||||
|
|
||||||
private static readonly HttpClient _httpClient = new();
|
private static readonly HttpClient _httpClient = new();
|
||||||
|
|
||||||
public ExternalStatisticsReporting(ShardInstance instance) : base(instance) { }
|
public ExternalStatisticsReporting(ShardInstance instance) : base(instance) { }
|
||||||
|
|
||||||
public override async Task OnTick(CancellationToken token)
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
{
|
if (tickCount < ProcessOffset) return;
|
||||||
if (++_tickCount % ProcessInterval != 0) return;
|
if (tickCount % ProcessInterval != 0) return;
|
||||||
|
|
||||||
var botId = ShardInstance.DiscordClient.CurrentUser.Id;
|
var botId = ShardInstance.DiscordClient.CurrentUser.Id;
|
||||||
if (botId == 0) return;
|
if (botId == 0) return;
|
||||||
|
var count = ShardInstance.DiscordClient.Guilds.Count;
|
||||||
|
|
||||||
await SendDiscordBots(ShardInstance.DiscordClient.Guilds.Count, botId, token);
|
var dbotsToken = ShardInstance.Config.DBotsToken;
|
||||||
|
if (dbotsToken != null) await SendDiscordBots(dbotsToken, count, botId, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task SendDiscordBots(int userCount, ulong botId, CancellationToken token)
|
private async Task SendDiscordBots(string apiToken, int userCount, ulong botId, CancellationToken token) {
|
||||||
{
|
try {
|
||||||
var dbotsToken = ShardInstance.Config.DBotsToken;
|
|
||||||
if (dbotsToken != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
const string dBotsApiUrl = "https://discord.bots.gg/api/v1/bots/{0}/stats";
|
const string dBotsApiUrl = "https://discord.bots.gg/api/v1/bots/{0}/stats";
|
||||||
const string Body = "{{ \"guildCount\": {0}, \"shardCount\": {1}, \"shardId\": {2} }}";
|
const string Body = "{{ \"guildCount\": {0}, \"shardCount\": {1}, \"shardId\": {2} }}";
|
||||||
var uri = new Uri(string.Format(dBotsApiUrl, botId));
|
var uri = new Uri(string.Format(dBotsApiUrl, botId));
|
||||||
|
|
||||||
var post = new HttpRequestMessage(HttpMethod.Post, uri);
|
var post = new HttpRequestMessage(HttpMethod.Post, uri);
|
||||||
post.Headers.Add("Authorization", dbotsToken);
|
post.Headers.Add("Authorization", apiToken);
|
||||||
post.Content = new StringContent(string.Format(Body,
|
post.Content = new StringContent(string.Format(Body,
|
||||||
userCount, ShardInstance.Config.ShardTotal, ShardInstance.ShardId),
|
userCount, ShardInstance.Config.ShardTotal, ShardInstance.ShardId),
|
||||||
Encoding.UTF8, "application/json");
|
Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
await _httpClient.SendAsync(post, token);
|
await _httpClient.SendAsync(post, token);
|
||||||
Log("Discord Bots: Update successful.");
|
Log("Discord Bots: Update successful.");
|
||||||
}
|
} catch (Exception ex) {
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Log("Discord Bots: Exception encountered during update: " + ex.Message);
|
Log("Discord Bots: Exception encountered during update: " + ex.Message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -18,7 +18,7 @@ class SelectiveAutoUserDownload : BackgroundService {
|
||||||
|
|
||||||
public SelectiveAutoUserDownload(ShardInstance instance) : base(instance) { }
|
public SelectiveAutoUserDownload(ShardInstance instance) : base(instance) { }
|
||||||
|
|
||||||
public override async Task OnTick(CancellationToken token) {
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
IEnumerable<ulong> requests;
|
IEnumerable<ulong> requests;
|
||||||
lock (_fetchRequests) {
|
lock (_fetchRequests) {
|
||||||
requests = _fetchRequests.ToArray();
|
requests = _fetchRequests.ToArray();
|
||||||
|
@ -26,10 +26,8 @@ class SelectiveAutoUserDownload : BackgroundService {
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
||||||
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) {
|
// Has the potential to disconnect while in the middle of processing.
|
||||||
Log("Client no longer connected. Stopping early.");
|
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine if there is action to be taken...
|
// Determine if there is action to be taken...
|
||||||
if (guild.HasAllMembers) continue;
|
if (guild.HasAllMembers) continue;
|
||||||
|
|
|
@ -18,22 +18,20 @@ namespace BirthdayBot.BackgroundServices
|
||||||
private readonly Task _workerTask;
|
private readonly Task _workerTask;
|
||||||
private readonly CancellationTokenSource _workerCanceller;
|
private readonly CancellationTokenSource _workerCanceller;
|
||||||
private readonly List<BackgroundService> _workers;
|
private readonly List<BackgroundService> _workers;
|
||||||
|
private int _tickCount = -1;
|
||||||
|
|
||||||
private ShardInstance Instance { get; }
|
private ShardInstance Instance { get; }
|
||||||
|
|
||||||
public ConnectionStatus ConnStatus { get; }
|
|
||||||
public BirthdayRoleUpdate BirthdayUpdater { get; }
|
public BirthdayRoleUpdate BirthdayUpdater { get; }
|
||||||
public SelectiveAutoUserDownload UserDownloader { get; }
|
public SelectiveAutoUserDownload UserDownloader { get; }
|
||||||
public DateTimeOffset LastBackgroundRun { get; private set; }
|
public DateTimeOffset LastBackgroundRun { get; private set; }
|
||||||
public string CurrentExecutingService { get; private set; }
|
public string? CurrentExecutingService { get; private set; }
|
||||||
public int ConnectionScore => ConnStatus.Score;
|
|
||||||
|
|
||||||
public ShardBackgroundWorker(ShardInstance instance)
|
public ShardBackgroundWorker(ShardInstance instance)
|
||||||
{
|
{
|
||||||
Instance = instance;
|
Instance = instance;
|
||||||
_workerCanceller = new CancellationTokenSource();
|
_workerCanceller = new CancellationTokenSource();
|
||||||
|
|
||||||
ConnStatus = new ConnectionStatus(instance);
|
|
||||||
BirthdayUpdater = new BirthdayRoleUpdate(instance);
|
BirthdayUpdater = new BirthdayRoleUpdate(instance);
|
||||||
UserDownloader = new SelectiveAutoUserDownload(instance);
|
UserDownloader = new SelectiveAutoUserDownload(instance);
|
||||||
_workers = new List<BackgroundService>()
|
_workers = new List<BackgroundService>()
|
||||||
|
@ -70,9 +68,8 @@ namespace BirthdayBot.BackgroundServices
|
||||||
{
|
{
|
||||||
await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false);
|
await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
// ConnectionStatus will always run. Its result determines if remaining tasks also this time.
|
// Skip this round of task execution if the client is not connected
|
||||||
await ConnStatus.OnTick(_workerCanceller.Token).ConfigureAwait(false);
|
if (Instance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) continue;
|
||||||
if (!ConnStatus.Stable) continue;
|
|
||||||
|
|
||||||
// Execute tasks sequentially
|
// Execute tasks sequentially
|
||||||
foreach (var service in _workers)
|
foreach (var service in _workers)
|
||||||
|
@ -81,30 +78,20 @@ namespace BirthdayBot.BackgroundServices
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (_workerCanceller.IsCancellationRequested) break;
|
if (_workerCanceller.IsCancellationRequested) break;
|
||||||
await service.OnTick(_workerCanceller.Token).ConfigureAwait(false);
|
_tickCount++;
|
||||||
|
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex) when (ex is not TaskCanceledException)
|
||||||
{
|
|
||||||
|
|
||||||
if (ex is TaskCanceledException)
|
|
||||||
{
|
|
||||||
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} was interrupted by a cancellation request.");
|
|
||||||
throw;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
{
|
||||||
// TODO webhook log
|
// TODO webhook log
|
||||||
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} encountered an exception:\n" + ex.ToString());
|
Instance.Log(nameof(WorkerLoop), $"{CurrentExecutingService} encountered an exception:\n" + ex.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
CurrentExecutingService = null;
|
CurrentExecutingService = null;
|
||||||
LastBackgroundRun = DateTimeOffset.UtcNow;
|
LastBackgroundRun = DateTimeOffset.UtcNow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (TaskCanceledException) { }
|
catch (TaskCanceledException) { }
|
||||||
|
|
||||||
Instance.Log(nameof(WorkerLoop), "Background worker has concluded normally.");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,16 +6,15 @@ using System.IO;
|
||||||
using System.Reflection;
|
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>
|
||||||
class Configuration
|
class Configuration {
|
||||||
{
|
|
||||||
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 const string ShardLenConfKey = "ShardRange";
|
public const string ShardLenConfKey = "ShardRange";
|
||||||
public int ShardStart { get; }
|
public int ShardStart { get; }
|
||||||
|
@ -25,14 +24,12 @@ namespace BirthdayBot
|
||||||
|
|
||||||
public bool QuitOnFails { get; }
|
public bool QuitOnFails { get; }
|
||||||
|
|
||||||
public Configuration()
|
public Configuration() {
|
||||||
{
|
|
||||||
// Looks for settings.json in the executable directory.
|
// Looks for settings.json in the executable directory.
|
||||||
var confPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
var confPath = Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location);
|
||||||
confPath += Path.DirectorySeparatorChar + "settings.json";
|
confPath += Path.DirectorySeparatorChar + "settings.json";
|
||||||
|
|
||||||
if (!File.Exists(confPath))
|
if (!File.Exists(confPath)) {
|
||||||
{
|
|
||||||
throw new Exception("Settings file not found."
|
throw new Exception("Settings file not found."
|
||||||
+ " Create a file in the executable directory named 'settings.json'.");
|
+ " Create a file in the executable directory named 'settings.json'.");
|
||||||
}
|
}
|
||||||
|
@ -48,12 +45,9 @@ namespace BirthdayBot
|
||||||
throw new Exception($"'{nameof(LogWebhook)}' must be specified.");
|
throw new Exception($"'{nameof(LogWebhook)}' must be specified.");
|
||||||
|
|
||||||
var dbj = jc[nameof(DBotsToken)];
|
var dbj = jc[nameof(DBotsToken)];
|
||||||
if (dbj != null)
|
if (dbj != null) {
|
||||||
{
|
|
||||||
DBotsToken = dbj.Value<string>();
|
DBotsToken = dbj.Value<string>();
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
DBotsToken = null;
|
DBotsToken = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -62,8 +56,7 @@ namespace BirthdayBot
|
||||||
var sqlpass = jc["SqlPassword"]?.Value<string>();
|
var sqlpass = jc["SqlPassword"]?.Value<string>();
|
||||||
if (string.IsNullOrWhiteSpace(sqluser) || string.IsNullOrWhiteSpace(sqlpass))
|
if (string.IsNullOrWhiteSpace(sqluser) || string.IsNullOrWhiteSpace(sqlpass))
|
||||||
throw new Exception("'SqlUsername', 'SqlPassword' must be specified.");
|
throw new Exception("'SqlUsername', 'SqlPassword' must be specified.");
|
||||||
var csb = new NpgsqlConnectionStringBuilder()
|
var csb = new NpgsqlConnectionStringBuilder() {
|
||||||
{
|
|
||||||
Host = sqlhost,
|
Host = sqlhost,
|
||||||
Username = sqluser,
|
Username = sqluser,
|
||||||
Password = sqlpass
|
Password = sqlpass
|
||||||
|
@ -74,33 +67,25 @@ namespace BirthdayBot
|
||||||
|
|
||||||
int? sc = jc[nameof(ShardTotal)]?.Value<int>();
|
int? sc = jc[nameof(ShardTotal)]?.Value<int>();
|
||||||
if (!sc.HasValue) ShardTotal = 1;
|
if (!sc.HasValue) ShardTotal = 1;
|
||||||
else
|
else {
|
||||||
{
|
|
||||||
ShardTotal = sc.Value;
|
ShardTotal = sc.Value;
|
||||||
if (ShardTotal <= 0)
|
if (ShardTotal <= 0) {
|
||||||
{
|
|
||||||
throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer.");
|
throw new Exception($"'{nameof(ShardTotal)}' must be a positive integer.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
string srVal = jc[ShardLenConfKey]?.Value<string>();
|
string srVal = jc[ShardLenConfKey]?.Value<string>();
|
||||||
if (!string.IsNullOrWhiteSpace(srVal))
|
if (!string.IsNullOrWhiteSpace(srVal)) {
|
||||||
{
|
|
||||||
Regex srPicker = new(@"(?<low>\d{1,2})[-,]{1}(?<high>\d{1,2})");
|
Regex srPicker = new(@"(?<low>\d{1,2})[-,]{1}(?<high>\d{1,2})");
|
||||||
var m = srPicker.Match(srVal);
|
var m = srPicker.Match(srVal);
|
||||||
if (m.Success)
|
if (m.Success) {
|
||||||
{
|
|
||||||
ShardStart = int.Parse(m.Groups["low"].Value);
|
ShardStart = int.Parse(m.Groups["low"].Value);
|
||||||
int high = int.Parse(m.Groups["high"].Value);
|
int high = int.Parse(m.Groups["high"].Value);
|
||||||
ShardAmount = high - (ShardStart - 1);
|
ShardAmount = high - (ShardStart - 1);
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
throw new Exception($"Shard range not properly formatted in '{ShardLenConfKey}'.");
|
throw new Exception($"Shard range not properly formatted in '{ShardLenConfKey}'.");
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
else
|
|
||||||
{
|
|
||||||
// Default: this instance handles all shards from ShardTotal
|
// Default: this instance handles all shards from ShardTotal
|
||||||
ShardStart = 0;
|
ShardStart = 0;
|
||||||
ShardAmount = ShardTotal;
|
ShardAmount = ShardTotal;
|
||||||
|
@ -109,4 +94,3 @@ namespace BirthdayBot
|
||||||
QuitOnFails = jc[nameof(QuitOnFails)]?.Value<bool>() ?? false;
|
QuitOnFails = jc[nameof(QuitOnFails)]?.Value<bool>() ?? false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
|
@ -30,11 +30,6 @@ namespace BirthdayBot
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public string CurrentExecutingService => _background.CurrentExecutingService;
|
public string CurrentExecutingService => _background.CurrentExecutingService;
|
||||||
public Configuration Config => _manager.Config;
|
public Configuration Config => _manager.Config;
|
||||||
/// <summary>
|
|
||||||
/// Returns this shard's connection score.
|
|
||||||
/// See <see cref="BackgroundServices.ConnectionStatus"/> for details on what this means.
|
|
||||||
/// </summary>
|
|
||||||
public int ConnectionScore => _background.ConnectionScore;
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Prepares and configures the shard instances, but does not yet start its connection.
|
/// Prepares and configures the shard instances, but does not yet start its connection.
|
||||||
|
@ -51,7 +46,6 @@ namespace BirthdayBot
|
||||||
|
|
||||||
// Background task constructor begins background processing immediately.
|
// Background task constructor begins background processing immediately.
|
||||||
_background = new ShardBackgroundWorker(this);
|
_background = new ShardBackgroundWorker(this);
|
||||||
DiscordClient.Disconnected += Client_Disconnected;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
@ -69,13 +63,10 @@ namespace BirthdayBot
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
Log("Instance", "Cleaning up...");
|
|
||||||
|
|
||||||
// Unsubscribe from own events
|
// Unsubscribe from own events
|
||||||
DiscordClient.Log -= Client_Log;
|
DiscordClient.Log -= Client_Log;
|
||||||
DiscordClient.Ready -= Client_Ready;
|
DiscordClient.Ready -= Client_Ready;
|
||||||
DiscordClient.MessageReceived -= Client_MessageReceived;
|
DiscordClient.MessageReceived -= Client_MessageReceived;
|
||||||
DiscordClient.Disconnected -= Client_Disconnected;
|
|
||||||
|
|
||||||
_background.Dispose();
|
_background.Dispose();
|
||||||
try
|
try
|
||||||
|
@ -98,9 +89,8 @@ namespace BirthdayBot
|
||||||
}
|
}
|
||||||
|
|
||||||
var clientDispose = Task.Run(DiscordClient.Dispose);
|
var clientDispose = Task.Run(DiscordClient.Dispose);
|
||||||
if (!clientDispose.Wait(10000))
|
if (!clientDispose.Wait(10000)) Log("Instance", "Warning: Client is hanging on dispose. Will continue.");
|
||||||
Log("Instance", "Warning: Client hanging on dispose.");
|
else Log("Instance", "Shard instance disposed.");
|
||||||
Log("Instance", "Shard instance disposed.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message);
|
public void Log(string source, string message) => Program.Log($"Shard {ShardId:00}] [{source}", message);
|
||||||
|
@ -130,6 +120,7 @@ namespace BirthdayBot
|
||||||
case "Disconnecting":
|
case "Disconnecting":
|
||||||
case "Disconnected":
|
case "Disconnected":
|
||||||
case "WebSocket connection was closed":
|
case "WebSocket connection was closed":
|
||||||
|
case "Server requested a reconnect":
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
if (arg.Message == "Heartbeat Errored")
|
if (arg.Message == "Heartbeat Errored")
|
||||||
|
@ -152,15 +143,6 @@ namespace BirthdayBot
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private async Task Client_Ready() => await DiscordClient.SetGameAsync(CommandPrefix + "help");
|
private async Task Client_Ready() => await DiscordClient.SetGameAsync(CommandPrefix + "help");
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Notify ConnectionStatus of a disconnect.
|
|
||||||
/// </summary>
|
|
||||||
private Task Client_Disconnected(Exception arg)
|
|
||||||
{
|
|
||||||
_background.ConnStatus.Disconnected();
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Determines if the incoming message is an incoming command, and dispatches to the appropriate handler if necessary.
|
/// Determines if the incoming message is an incoming command, and dispatches to the appropriate handler if necessary.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
@ -150,7 +150,7 @@ namespace BirthdayBot
|
||||||
Log($"Bot uptime: {Common.BotUptime}");
|
Log($"Bot uptime: {Common.BotUptime}");
|
||||||
|
|
||||||
// Iterate through shard list, extract data
|
// Iterate through shard list, extract data
|
||||||
var guildInfo = new Dictionary<int, (int, int, TimeSpan, string)>();
|
var guildInfo = new Dictionary<int, (int, TimeSpan, string)>();
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
var nullShards = new List<int>();
|
var nullShards = new List<int>();
|
||||||
foreach (var item in _shards)
|
foreach (var item in _shards)
|
||||||
|
@ -163,11 +163,10 @@ namespace BirthdayBot
|
||||||
var shard = item.Value;
|
var shard = item.Value;
|
||||||
|
|
||||||
var guildCount = shard.DiscordClient.Guilds.Count;
|
var guildCount = shard.DiscordClient.Guilds.Count;
|
||||||
var connScore = shard.ConnectionScore;
|
|
||||||
var lastRun = now - shard.LastBackgroundRun;
|
var lastRun = now - shard.LastBackgroundRun;
|
||||||
var lastExec = shard.CurrentExecutingService ?? "null";
|
var lastExec = shard.CurrentExecutingService ?? "null";
|
||||||
|
|
||||||
guildInfo[item.Key] = (guildCount, connScore, lastRun, lastExec);
|
guildInfo[item.Key] = (guildCount, lastRun, lastExec);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process info
|
// Process info
|
||||||
|
@ -182,10 +181,9 @@ namespace BirthdayBot
|
||||||
var deadShards = new List<int>(); // shards to destroy and reinitialize
|
var deadShards = new List<int>(); // shards to destroy and reinitialize
|
||||||
foreach (var item in guildInfo)
|
foreach (var item in guildInfo)
|
||||||
{
|
{
|
||||||
var connScore = item.Value.Item2;
|
var lastRun = item.Value.Item2;
|
||||||
var lastRun = item.Value.Item3;
|
|
||||||
|
|
||||||
if (lastRun > new TimeSpan(0, 10, 0) || connScore < ConnectionStatus.StableScore)
|
if (lastRun > DeadShardThreshold / 3)
|
||||||
{
|
{
|
||||||
badShards.Add(item.Key);
|
badShards.Add(item.Key);
|
||||||
|
|
||||||
|
@ -208,9 +206,8 @@ namespace BirthdayBot
|
||||||
if (detailedInfo)
|
if (detailedInfo)
|
||||||
{
|
{
|
||||||
result.Remove(result.Length - 1, 1);
|
result.Remove(result.Length - 1, 1);
|
||||||
result.Append($"[{guildInfo[item].Item2:+0;-0}");
|
result.Append($"[{Math.Floor(guildInfo[item].Item2.TotalSeconds):000}s");
|
||||||
result.Append($" {Math.Floor(guildInfo[item].Item3.TotalSeconds):000}s");
|
result.Append($" {guildInfo[item].Item3}] ");
|
||||||
result.Append($" {guildInfo[item].Item4}] ");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (result.Length > 0) result.Remove(result.Length - 1, 1);
|
if (result.Length > 0) result.Remove(result.Length - 1, 1);
|
||||||
|
|
Loading…
Reference in a new issue