Add user list fetch background task

This commit is contained in:
Noi 2022-08-29 14:51:27 -07:00
parent 92d23217b9
commit 7f43605a7f
2 changed files with 81 additions and 40 deletions

64
BackgroundUserListLoad.cs Normal file
View file

@ -0,0 +1,64 @@
using Microsoft.Extensions.DependencyInjection;
using WorldTime.Data;
namespace WorldTime;
/// <summary>
/// Proactively fills the user cache for guilds in which any time zone configuration exists.
/// </summary>
/// <remarks>Modeled after BirthdayBot's similar feature.</remarks>
class BackgroundUserListLoad : IDisposable {
private readonly IServiceProvider _services;
private readonly Task _workerTask;
private readonly CancellationTokenSource _workerCancel;
public BackgroundUserListLoad(IServiceProvider services) {
_services = services;
_workerCancel = new();
_workerTask = Task.Factory.StartNew(Worker, _workerCancel.Token,
TaskCreationOptions.LongRunning, TaskScheduler.Default);
}
public void Dispose() {
_workerCancel.Cancel();
_workerCancel.Dispose();
_workerTask.Dispose();
}
private async Task Worker() {
while (!_workerCancel.IsCancellationRequested) {
// Interval same as status
await Task.Delay(WorldTime.StatusInterval * 1000, _workerCancel.Token);
foreach (var shard in _services.GetRequiredService<DiscordShardedClient>().Shards) {
try {
await ProcessShard(shard);
} catch (Exception ex) {
Program.Log(nameof(BackgroundUserListLoad), ex.ToString());
}
}
}
}
private async Task ProcessShard(DiscordSocketClient shard) {
using var db = _services.GetRequiredService<BotDatabaseContext>();
// Check when a guild's cache is incomplete...
var incompleteCaches = shard.Guilds.Where(g => !g.HasAllMembers).Select(g => (long)g.Id).ToHashSet();
// ...and contains any user data.
var mustFetch = db.UserEntries.Where(e => incompleteCaches.Contains(e.GuildId)).Select(e => e.GuildId).Distinct();
var processed = 0;
foreach (var item in mustFetch) {
// May cause a disconnect in certain situations. Cancel all further attempts until the next pass if it happens.
if (shard.ConnectionState != ConnectionState.Connected) break;
var guild = shard.GetGuild((ulong)item);
if (guild == null) continue; // A guild disappeared...?
await guild.DownloadUsersAsync().ConfigureAwait(false); // We're already on a seperate thread, no need to use Task.Run
await Task.Delay(200, CancellationToken.None).ConfigureAwait(false); // Must delay, or else it seems to hang...
processed++;
}
if (processed > 100) Program.Log(nameof(BackgroundUserListLoad), $"Explicit user list request processed for {processed} guilds.");
}
}

View file

@ -16,20 +16,15 @@ internal class WorldTime : IDisposable {
/// Number of seconds between each time the status task runs, in seconds.
/// </summary>
#if DEBUG
private const int StatusInterval = 20;
internal const int StatusInterval = 20;
#else
private const int StatusInterval = 300;
internal const int StatusInterval = 300;
#endif
/// <summary>
/// Number of concurrent shard startups to happen on each check.
/// This value is also used in <see cref="DataRetention"/>.
/// </summary>
public const int MaxConcurrentOperations = 5;
private readonly Task _statusTask;
private readonly CancellationTokenSource _mainCancel;
private readonly CancellationTokenSource _statusCancel;
private readonly IServiceProvider _services;
private readonly BackgroundUserListLoad _bgFetch;
internal Configuration Config { get; }
internal DiscordShardedClient DiscordClient => _services.GetRequiredService<DiscordShardedClient>();
@ -45,7 +40,9 @@ internal class WorldTime : IDisposable {
LogLevel = LogSeverity.Info,
DefaultRetryMode = RetryMode.RetryRatelimit,
MessageCacheSize = 0, // disable message cache
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers,
SuppressUnknownDispatchWarnings = true,
LogGatewayIntentWarnings = false
};
_services = new ServiceCollection()
.AddSingleton(new DiscordShardedClient(clientConf))
@ -54,15 +51,16 @@ internal class WorldTime : IDisposable {
.BuildServiceProvider();
DiscordClient.Log += DiscordClient_Log;
DiscordClient.ShardReady += DiscordClient_ShardReady;
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
var iasrv = _services.GetRequiredService<InteractionService>();
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
iasrv.SlashCommandExecuted += InteractionService_SlashCommandExecuted;
// Start status reporting thread
_mainCancel = new CancellationTokenSource();
_statusTask = Task.Factory.StartNew(StatusLoop, _mainCancel.Token,
_statusCancel = new CancellationTokenSource();
_statusTask = Task.Factory.StartNew(StatusLoop, _statusCancel.Token,
TaskCreationOptions.LongRunning, TaskScheduler.Default);
_bgFetch = new(_services);
}
public async Task StartAsync() {
@ -72,26 +70,24 @@ internal class WorldTime : IDisposable {
}
public void Dispose() {
_mainCancel.Cancel();
_statusCancel.Cancel();
_statusTask.Wait(10000);
if (!_statusTask.IsCompleted)
Program.Log(nameof(WorldTime), "Warning: Main thread did not cleanly finish up in time. Continuing...");
_mainCancel.Cancel();
_statusTask.Wait(5000);
_mainCancel.Dispose();
_statusCancel.Dispose();
Program.Log(nameof(WorldTime), $"Uptime: {Program.BotUptime}");
}
private async Task StatusLoop() {
try {
await Task.Delay(30000, _mainCancel.Token).ConfigureAwait(false); // initial 30 second delay
while (!_mainCancel.IsCancellationRequested) {
await Task.Delay(30000, _statusCancel.Token).ConfigureAwait(false); // initial 30 second delay
while (!_statusCancel.IsCancellationRequested) {
Program.Log(nameof(WorldTime), $"Bot uptime: {Program.BotUptime}");
await PeriodicReport(DiscordClient.CurrentUser.Id, DiscordClient.Guilds.Count, _mainCancel.Token).ConfigureAwait(false);
await Task.Delay(StatusInterval * 1000, _mainCancel.Token).ConfigureAwait(false);
await PeriodicReport(DiscordClient.CurrentUser.Id, DiscordClient.Guilds.Count, _statusCancel.Token).ConfigureAwait(false);
await Task.Delay(StatusInterval * 1000, _statusCancel.Token).ConfigureAwait(false);
}
} catch (TaskCanceledException) { }
}
@ -173,25 +169,6 @@ internal class WorldTime : IDisposable {
#endif
}
/// <summary>
/// Non-specific handler for incoming events.
/// </summary>
private async Task DiscordClient_MessageReceived(SocketMessage message) {
if (message.Author.IsWebhook) return;
if (message.Type != MessageType.Default) return;
if (message.Channel is not SocketTextChannel channel) return;
// Proactively fill guild user cache if the bot has any data for the respective guild
// Can skip an extra query if the last_seen update is known to have been successful, otherwise query for any users
if (!channel.Guild.HasAllMembers) {
using var db = _services.GetRequiredService<BotDatabaseContext>();
if (db.HasAnyUsers(channel.Guild)) {
// Event handler hangs if awaited normally or used with Task.Run
await Task.Factory.StartNew(channel.Guild.DownloadUsersAsync);
}
}
}
const string InternalError = ":x: An unknown error occurred. If it persists, please notify the bot owner.";
// Slash command preparation and invocation