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.
|
||||
/// </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
|
||||
|
|
Loading…
Reference in a new issue