diff --git a/BackgroundServices/SelectiveAutoUserDownload.cs b/BackgroundServices/SelectiveAutoUserDownload.cs index 06de92a..0b2fe53 100644 --- a/BackgroundServices/SelectiveAutoUserDownload.cs +++ b/BackgroundServices/SelectiveAutoUserDownload.cs @@ -1,86 +1,61 @@ using BirthdayBot.Data; +using Discord; using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; -namespace BirthdayBot.BackgroundServices -{ - /// - /// A type of workaround to the issue of user information not being cached for guilds that - /// have user information existing in the bot's database. This service runs frequently and - /// determines guilds in which user data must be downloaded, and proceeds to request it. - /// - class SelectiveAutoUserDownload : BackgroundService - { - private static readonly SemaphoreSlim _updateLock = new(2); +namespace BirthdayBot.BackgroundServices; - private readonly HashSet _fetchRequests = new(); +/// +/// Rather than use up unnecessary resources by auto-downloading the user list in -every- +/// server we're in, this service checks if fetching the user list is warranted for each +/// guild before proceeding to request it. +/// +class SelectiveAutoUserDownload : BackgroundService { + private readonly HashSet _fetchRequests = new(); - public SelectiveAutoUserDownload(ShardInstance instance) : base(instance) { } + public SelectiveAutoUserDownload(ShardInstance instance) : base(instance) { } - public override async Task OnTick(CancellationToken token) - { - IEnumerable requests; - lock (_fetchRequests) - { - requests = _fetchRequests.ToArray(); - _fetchRequests.Clear(); - } - - foreach (var guild in ShardInstance.DiscordClient.Guilds) - { - if (ShardInstance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) - { - Log("Client no longer connected. Stopping early."); - return; - } - - // Determine if there is action to be taken... - if (guild.HasAllMembers) continue; - if (requests.Contains(guild.Id) || await GuildUserAnyAsync(guild.Id, token).ConfigureAwait(false)) - { - await guild.DownloadUsersAsync().ConfigureAwait(false); - await Task.Delay(500, CancellationToken.None).ConfigureAwait(false); - } - } + public override async Task OnTick(CancellationToken token) { + IEnumerable requests; + lock (_fetchRequests) { + requests = _fetchRequests.ToArray(); + _fetchRequests.Clear(); } - /// - /// Determines if the user database contains any entries corresponding to this guild. - /// - /// True if any entries exist. - private static async Task GuildUserAnyAsync(ulong guildId, CancellationToken token) - { - try - { - await _updateLock.WaitAsync(token).ConfigureAwait(false); + foreach (var guild in ShardInstance.DiscordClient.Guilds) { + if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) { + Log("Client no longer connected. Stopping early."); + return; } - catch (Exception ex) when (ex is OperationCanceledException or ObjectDisposedException) - { - // Calling thread does not expect the exception that SemaphoreSlim throws... - throw new TaskCanceledException(); - } - try - { - using var db = await Database.OpenConnectionAsync().ConfigureAwait(false); - using var c = db.CreateCommand(); - c.CommandText = $"select count(*) from {GuildUserConfiguration.BackingTable} where guild_id = @Gid"; - c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guildId; - await c.PrepareAsync(CancellationToken.None).ConfigureAwait(false); - var r = (long)await c.ExecuteScalarAsync(token).ConfigureAwait(false); - return r != 0; - } - finally - { - _updateLock.Release(); - } - } - public void RequestDownload(ulong guildId) - { - lock (_fetchRequests) _fetchRequests.Add(guildId); + // Determine if there is action to be taken... + if (guild.HasAllMembers) continue; + if (requests.Contains(guild.Id) || await GuildUserAnyAsync(guild.Id).ConfigureAwait(false)) { + await guild.DownloadUsersAsync().ConfigureAwait(false); + // Must delay after a download request. Seems to hang indefinitely otherwise. + await Task.Delay(300, CancellationToken.None).ConfigureAwait(false); + } } } + + /// + /// Determines if the user database contains any entries corresponding to this guild. + /// + /// True if any entries exist. + private static async Task GuildUserAnyAsync(ulong guildId) { + using var db = await Database.OpenConnectionAsync().ConfigureAwait(false); + using var c = db.CreateCommand(); + c.CommandText = $"select true from {GuildUserConfiguration.BackingTable} where guild_id = @Gid limit 1"; + c.Parameters.Add("@Gid", NpgsqlTypes.NpgsqlDbType.Bigint).Value = (long)guildId; + await c.PrepareAsync(CancellationToken.None).ConfigureAwait(false); + using var r = await c.ExecuteReaderAsync(CancellationToken.None).ConfigureAwait(false); + return r.Read(); + } + + public void RequestDownload(ulong guildId) { + lock (_fetchRequests) _fetchRequests.Add(guildId); + } } diff --git a/UserInterface/CommandsCommon.cs b/UserInterface/CommandsCommon.cs index 9bca783..6b7800d 100644 --- a/UserInterface/CommandsCommon.cs +++ b/UserInterface/CommandsCommon.cs @@ -22,7 +22,7 @@ namespace BirthdayBot.UserInterface public const string ParameterError = ":x: Invalid usage. Refer to how to use the command and try again."; public const string NoParameterError = ":x: This command does not accept any parameters."; public const string InternalError = ":x: An internal bot error occurred. The bot maintainer has been notified of the issue."; - public const string UsersNotDownloadedError = ":x: Currently unavailable. Please try again in a few minutes."; + public const string UsersNotDownloadedError = ":eight_spoked_asterisk: Still catching up... Please try the command again in a few minutes."; public delegate Task CommandHandler(ShardInstance instance, GuildConfiguration gconf, string[] param, SocketTextChannel reqChannel, SocketGuildUser reqUser);