mirror of
https://github.com/NoiTheCat/WorldTime.git
synced 2024-11-21 14:34:36 +00:00
Add user list fetch background task
This commit is contained in:
parent
92d23217b9
commit
7f43605a7f
2 changed files with 81 additions and 40 deletions
64
BackgroundUserListLoad.cs
Normal file
64
BackgroundUserListLoad.cs
Normal 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.");
|
||||||
|
}
|
||||||
|
}
|
57
WorldTime.cs
57
WorldTime.cs
|
@ -16,20 +16,15 @@ internal class WorldTime : IDisposable {
|
||||||
/// 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>
|
||||||
#if DEBUG
|
#if DEBUG
|
||||||
private const int StatusInterval = 20;
|
internal const int StatusInterval = 20;
|
||||||
#else
|
#else
|
||||||
private const int StatusInterval = 300;
|
internal const int StatusInterval = 300;
|
||||||
#endif
|
#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 Task _statusTask;
|
||||||
private readonly CancellationTokenSource _mainCancel;
|
private readonly CancellationTokenSource _statusCancel;
|
||||||
private readonly IServiceProvider _services;
|
private readonly IServiceProvider _services;
|
||||||
|
private readonly BackgroundUserListLoad _bgFetch;
|
||||||
|
|
||||||
internal Configuration Config { get; }
|
internal Configuration Config { get; }
|
||||||
internal DiscordShardedClient DiscordClient => _services.GetRequiredService<DiscordShardedClient>();
|
internal DiscordShardedClient DiscordClient => _services.GetRequiredService<DiscordShardedClient>();
|
||||||
|
@ -45,7 +40,9 @@ internal class WorldTime : IDisposable {
|
||||||
LogLevel = LogSeverity.Info,
|
LogLevel = LogSeverity.Info,
|
||||||
DefaultRetryMode = RetryMode.RetryRatelimit,
|
DefaultRetryMode = RetryMode.RetryRatelimit,
|
||||||
MessageCacheSize = 0, // disable message cache
|
MessageCacheSize = 0, // disable message cache
|
||||||
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers | GatewayIntents.GuildMessages
|
GatewayIntents = GatewayIntents.Guilds | GatewayIntents.GuildMembers,
|
||||||
|
SuppressUnknownDispatchWarnings = true,
|
||||||
|
LogGatewayIntentWarnings = false
|
||||||
};
|
};
|
||||||
_services = new ServiceCollection()
|
_services = new ServiceCollection()
|
||||||
.AddSingleton(new DiscordShardedClient(clientConf))
|
.AddSingleton(new DiscordShardedClient(clientConf))
|
||||||
|
@ -54,15 +51,16 @@ internal class WorldTime : IDisposable {
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
DiscordClient.Log += DiscordClient_Log;
|
DiscordClient.Log += DiscordClient_Log;
|
||||||
DiscordClient.ShardReady += DiscordClient_ShardReady;
|
DiscordClient.ShardReady += DiscordClient_ShardReady;
|
||||||
DiscordClient.MessageReceived += DiscordClient_MessageReceived;
|
|
||||||
var iasrv = _services.GetRequiredService<InteractionService>();
|
var iasrv = _services.GetRequiredService<InteractionService>();
|
||||||
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
|
DiscordClient.InteractionCreated += DiscordClient_InteractionCreated;
|
||||||
iasrv.SlashCommandExecuted += InteractionService_SlashCommandExecuted;
|
iasrv.SlashCommandExecuted += InteractionService_SlashCommandExecuted;
|
||||||
|
|
||||||
// Start status reporting thread
|
// Start status reporting thread
|
||||||
_mainCancel = new CancellationTokenSource();
|
_statusCancel = new CancellationTokenSource();
|
||||||
_statusTask = Task.Factory.StartNew(StatusLoop, _mainCancel.Token,
|
_statusTask = Task.Factory.StartNew(StatusLoop, _statusCancel.Token,
|
||||||
TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
TaskCreationOptions.LongRunning, TaskScheduler.Default);
|
||||||
|
|
||||||
|
_bgFetch = new(_services);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task StartAsync() {
|
public async Task StartAsync() {
|
||||||
|
@ -72,26 +70,24 @@ internal class WorldTime : IDisposable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose() {
|
public void Dispose() {
|
||||||
_mainCancel.Cancel();
|
_statusCancel.Cancel();
|
||||||
_statusTask.Wait(10000);
|
_statusTask.Wait(10000);
|
||||||
if (!_statusTask.IsCompleted)
|
if (!_statusTask.IsCompleted)
|
||||||
Program.Log(nameof(WorldTime), "Warning: Main thread did not cleanly finish up in time. Continuing...");
|
Program.Log(nameof(WorldTime), "Warning: Main thread did not cleanly finish up in time. Continuing...");
|
||||||
|
|
||||||
_mainCancel.Cancel();
|
_statusCancel.Dispose();
|
||||||
_statusTask.Wait(5000);
|
|
||||||
_mainCancel.Dispose();
|
|
||||||
|
|
||||||
Program.Log(nameof(WorldTime), $"Uptime: {Program.BotUptime}");
|
Program.Log(nameof(WorldTime), $"Uptime: {Program.BotUptime}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task StatusLoop() {
|
private async Task StatusLoop() {
|
||||||
try {
|
try {
|
||||||
await Task.Delay(30000, _mainCancel.Token).ConfigureAwait(false); // initial 30 second delay
|
await Task.Delay(30000, _statusCancel.Token).ConfigureAwait(false); // initial 30 second delay
|
||||||
while (!_mainCancel.IsCancellationRequested) {
|
while (!_statusCancel.IsCancellationRequested) {
|
||||||
Program.Log(nameof(WorldTime), $"Bot uptime: {Program.BotUptime}");
|
Program.Log(nameof(WorldTime), $"Bot uptime: {Program.BotUptime}");
|
||||||
|
|
||||||
await PeriodicReport(DiscordClient.CurrentUser.Id, DiscordClient.Guilds.Count, _mainCancel.Token).ConfigureAwait(false);
|
await PeriodicReport(DiscordClient.CurrentUser.Id, DiscordClient.Guilds.Count, _statusCancel.Token).ConfigureAwait(false);
|
||||||
await Task.Delay(StatusInterval * 1000, _mainCancel.Token).ConfigureAwait(false);
|
await Task.Delay(StatusInterval * 1000, _statusCancel.Token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
} catch (TaskCanceledException) { }
|
} catch (TaskCanceledException) { }
|
||||||
}
|
}
|
||||||
|
@ -173,25 +169,6 @@ internal class WorldTime : IDisposable {
|
||||||
#endif
|
#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.";
|
const string InternalError = ":x: An unknown error occurred. If it persists, please notify the bot owner.";
|
||||||
|
|
||||||
// Slash command preparation and invocation
|
// Slash command preparation and invocation
|
||||||
|
|
Loading…
Reference in a new issue