Restore "most users downloaded" checks

Workaround to deal with continued instances of inconsistent Discord.Net user downloading behavior
This commit is contained in:
Noi 2021-12-05 17:20:05 -08:00
parent 5e4d030467
commit 51e241aca8
6 changed files with 79 additions and 82 deletions

View file

@ -5,8 +5,8 @@ namespace BirthdayBot.BackgroundServices;
/// <summary> /// <summary>
/// Proactively fills the user cache for guilds in which any birthday data already exists. /// Proactively fills the user cache for guilds in which any birthday data already exists.
/// </summary> /// </summary>
class SelectiveAutoUserDownload : BackgroundService { class AutoUserDownload : BackgroundService {
public SelectiveAutoUserDownload(ShardInstance instance) : base(instance) { } public AutoUserDownload(ShardInstance instance) : base(instance) { }
public override async Task OnTick(int tickCount, CancellationToken token) { public override async Task OnTick(int tickCount, CancellationToken token) {
foreach (var guild in ShardInstance.DiscordClient.Guilds) { foreach (var guild in ShardInstance.DiscordClient.Guilds) {

View file

@ -40,7 +40,7 @@ class BirthdayRoleUpdate : BackgroundService {
/// </summary> /// </summary>
private static async Task ProcessGuildAsync(SocketGuild guild) { private static async Task ProcessGuildAsync(SocketGuild guild) {
// Load guild information - stop if local cache is unavailable. // Load guild information - stop if local cache is unavailable.
if (!guild.HasAllMembers) return; if (!Common.HasMostMembersDownloaded(guild)) return;
var gc = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false); var gc = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
if (gc == null) return; if (gc == null) return;

View file

@ -1,15 +1,9 @@
using System; namespace BirthdayBot.BackgroundServices;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace BirthdayBot.BackgroundServices /// <summary>
{ /// Handles the execution of periodic background tasks specific to each shard.
/// <summary> /// </summary>
/// Handles the execution of periodic background tasks specific to each shard. class ShardBackgroundWorker : IDisposable {
/// </summary>
class ShardBackgroundWorker : IDisposable
{
/// <summary> /// <summary>
/// The interval, in seconds, in which background tasks are attempted to be run within a shard. /// The interval, in seconds, in which background tasks are attempted to be run within a shard.
/// </summary> /// </summary>
@ -22,22 +16,17 @@ namespace BirthdayBot.BackgroundServices
private ShardInstance Instance { get; } private ShardInstance Instance { get; }
public BirthdayRoleUpdate BirthdayUpdater { 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 ShardBackgroundWorker(ShardInstance instance) public ShardBackgroundWorker(ShardInstance instance) {
{
Instance = instance; Instance = instance;
_workerCanceller = new CancellationTokenSource(); _workerCanceller = new CancellationTokenSource();
BirthdayUpdater = new BirthdayRoleUpdate(instance);
UserDownloader = new SelectiveAutoUserDownload(instance);
_workers = new List<BackgroundService>() _workers = new List<BackgroundService>()
{ {
{UserDownloader}, {new AutoUserDownload(instance)},
{BirthdayUpdater}, {new BirthdayRoleUpdate(instance)},
{new DataRetention(instance)}, {new DataRetention(instance)},
{new ExternalStatisticsReporting(instance)} {new ExternalStatisticsReporting(instance)}
}; };
@ -45,8 +34,7 @@ namespace BirthdayBot.BackgroundServices
_workerTask = Task.Factory.StartNew(WorkerLoop, _workerCanceller.Token); _workerTask = Task.Factory.StartNew(WorkerLoop, _workerCanceller.Token);
} }
public void Dispose() public void Dispose() {
{
_workerCanceller.Cancel(); _workerCanceller.Cancel();
_workerTask.Wait(5000); _workerTask.Wait(5000);
if (!_workerTask.IsCompleted) if (!_workerTask.IsCompleted)
@ -59,30 +47,23 @@ namespace BirthdayBot.BackgroundServices
/// *The* background task for the shard. /// *The* background task for the shard.
/// Executes service tasks and handles errors. /// Executes service tasks and handles errors.
/// </summary> /// </summary>
private async Task WorkerLoop() private async Task WorkerLoop() {
{
LastBackgroundRun = DateTimeOffset.UtcNow; LastBackgroundRun = DateTimeOffset.UtcNow;
try try {
{ while (!_workerCanceller.IsCancellationRequested) {
while (!_workerCanceller.IsCancellationRequested)
{
await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false); await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false);
// Skip this round of task execution if the client is not connected // Skip this round of task execution if the client is not connected
if (Instance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) continue; if (Instance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) continue;
// Execute tasks sequentially // Execute tasks sequentially
foreach (var service in _workers) foreach (var service in _workers) {
{
CurrentExecutingService = service.GetType().Name; CurrentExecutingService = service.GetType().Name;
try try {
{
if (_workerCanceller.IsCancellationRequested) break; if (_workerCanceller.IsCancellationRequested) break;
_tickCount++; _tickCount++;
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false); await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
} } catch (Exception ex) when (ex is not TaskCanceledException) {
catch (Exception ex) when (ex is not TaskCanceledException)
{
// 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());
} }
@ -90,8 +71,6 @@ namespace BirthdayBot.BackgroundServices
CurrentExecutingService = null; CurrentExecutingService = null;
LastBackgroundRun = DateTimeOffset.UtcNow; LastBackgroundRun = DateTimeOffset.UtcNow;
} }
} } catch (TaskCanceledException) { }
catch (TaskCanceledException) { }
}
} }
} }

View file

@ -31,4 +31,22 @@ static class Common {
{ 1, "Jan" }, { 2, "Feb" }, { 3, "Mar" }, { 4, "Apr" }, { 5, "May" }, { 6, "Jun" }, { 1, "Jan" }, { 2, "Feb" }, { 3, "Mar" }, { 4, "Apr" }, { 5, "May" }, { 6, "Jun" },
{ 7, "Jul" }, { 8, "Aug" }, { 9, "Sep" }, { 10, "Oct" }, { 11, "Nov" }, { 12, "Dec" } { 7, "Jul" }, { 8, "Aug" }, { 9, "Sep" }, { 10, "Oct" }, { 11, "Nov" }, { 12, "Dec" }
}; };
/// <summary>
/// An alternative to <see cref="SocketGuild.HasAllMembers"/>.
/// Returns true if *most* members have been downloaded.
/// Used as a workaround check due to Discord.Net occasionally unable to actually download all members.
/// </summary>
public static bool HasMostMembersDownloaded(SocketGuild guild) {
if (guild.HasAllMembers) return true;
if (guild.MemberCount > 30) {
// For guilds of size over 30, require 85% or more of the members to be known
// (26/30, 42/50, 255/300, etc)
int threshold = (int)(guild.MemberCount * 0.85);
return guild.DownloadedMemberCount >= threshold;
} else {
// For smaller guilds, fail if two or more members are missing
return guild.MemberCount - guild.DownloadedMemberCount <= 2;
}
}
} }

View file

@ -82,11 +82,11 @@ internal abstract class CommandsCommon {
/// </returns> /// </returns>
/// <remarks> /// <remarks>
/// Any updates to the member cache aren't accessible until the event handler finishes execution, meaning proactive downloading /// Any updates to the member cache aren't accessible until the event handler finishes execution, meaning proactive downloading
/// is necessary, and is handled by <seealso cref="BackgroundServices.SelectiveAutoUserDownload"/>. In situations where /// is necessary, and is handled by <seealso cref="BackgroundServices.AutoUserDownload"/>. In situations where
/// this approach fails, this is to be called, and the user must be asked to attempt the command again if this returns false. /// this approach fails, this is to be called, and the user must be asked to attempt the command again if this returns false.
/// </remarks> /// </remarks>
protected static async Task<bool> HasMemberCacheAsync(SocketGuild guild) { protected static async Task<bool> HasMemberCacheAsync(SocketGuild guild) {
if (guild.HasAllMembers) return true; if (Common.HasMostMembersDownloaded(guild)) return true;
// Event handling thread hangs if awaited normally or used with Task.Run // Event handling thread hangs if awaited normally or used with Task.Run
await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false); await Task.Factory.StartNew(guild.DownloadUsersAsync).ConfigureAwait(false);
return false; return false;

View file

@ -387,7 +387,7 @@ internal class ManagerCommands : CommandsCommon {
var conf = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false); var conf = await GuildConfiguration.LoadAsync(guild.Id, true).ConfigureAwait(false);
result.AppendLine($"Server ID: {guild.Id} | Bot shard ID: {instance.ShardId:00}"); result.AppendLine($"Server ID: {guild.Id} | Bot shard ID: {instance.ShardId:00}");
bool hasMembers = guild.HasAllMembers; bool 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 = -1; int bdayCount = -1;