mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-10-16 07:59:57 +00:00
Merge pull request #49 from NoiTheCat/dev/more-config
Add configurable runtime settings that were previously hardcoded
This commit is contained in:
commit
fa8bb0abd2
9 changed files with 81 additions and 54 deletions
|
@ -13,14 +13,14 @@ class AutoUserDownload : BackgroundService {
|
|||
|
||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
// Take action if a guild's cache is incomplete...
|
||||
var incompleteCaches = ShardInstance.DiscordClient.Guilds
|
||||
var incompleteCaches = Shard.DiscordClient.Guilds
|
||||
.Where(g => !g.HasAllMembers)
|
||||
.Select(g => g.Id)
|
||||
.ToHashSet();
|
||||
// ...and if the guild contains any user data
|
||||
IEnumerable<ulong> mustFetch;
|
||||
try {
|
||||
await DbConcurrentOperationsLock.WaitAsync(token);
|
||||
await ConcurrentSemaphore.WaitAsync(token);
|
||||
using var db = new BotDatabaseContext();
|
||||
lock (_failedDownloads)
|
||||
mustFetch = db.UserEntries.AsNoTracking()
|
||||
|
@ -31,7 +31,7 @@ class AutoUserDownload : BackgroundService {
|
|||
.ToList();
|
||||
} finally {
|
||||
try {
|
||||
DbConcurrentOperationsLock.Release();
|
||||
ConcurrentSemaphore.Release();
|
||||
} catch (ObjectDisposedException) { }
|
||||
}
|
||||
|
||||
|
@ -39,9 +39,9 @@ class AutoUserDownload : BackgroundService {
|
|||
var processStartTime = DateTimeOffset.UtcNow;
|
||||
foreach (var item in mustFetch) {
|
||||
// May cause a disconnect in certain situations. Make no further attempts until the next pass if it happens.
|
||||
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) break;
|
||||
if (Shard.DiscordClient.ConnectionState != ConnectionState.Connected) break;
|
||||
|
||||
var guild = ShardInstance.DiscordClient.GetGuild(item);
|
||||
var guild = Shard.DiscordClient.GetGuild(item);
|
||||
if (guild == null) continue; // A guild disappeared...?
|
||||
|
||||
await Task.Delay(200, CancellationToken.None); // Delay a bit (reduces the possibility of hanging, somehow).
|
||||
|
|
|
@ -1,11 +1,15 @@
|
|||
namespace BirthdayBot.BackgroundServices;
|
||||
abstract class BackgroundService {
|
||||
protected static SemaphoreSlim DbConcurrentOperationsLock { get; } = new(ShardManager.MaxConcurrentOperations);
|
||||
protected ShardInstance ShardInstance { get; }
|
||||
protected static SemaphoreSlim ConcurrentSemaphore { get; private set; } = null!;
|
||||
|
||||
public BackgroundService(ShardInstance instance) => ShardInstance = instance;
|
||||
protected ShardInstance Shard { get; }
|
||||
|
||||
protected void Log(string message) => ShardInstance.Log(GetType().Name, message);
|
||||
public BackgroundService(ShardInstance instance) {
|
||||
Shard = instance;
|
||||
ConcurrentSemaphore ??= new SemaphoreSlim(instance.Config.MaxConcurrentOperations);
|
||||
}
|
||||
|
||||
protected void Log(string message) => Shard.Log(GetType().Name, message);
|
||||
|
||||
public abstract Task OnTick(int tickCount, CancellationToken token);
|
||||
}
|
||||
|
|
|
@ -15,11 +15,11 @@ class BirthdayRoleUpdate : BackgroundService {
|
|||
/// </summary>
|
||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
try {
|
||||
await DbConcurrentOperationsLock.WaitAsync(token);
|
||||
await ConcurrentSemaphore.WaitAsync(token);
|
||||
await ProcessBirthdaysAsync(token);
|
||||
} finally {
|
||||
try {
|
||||
DbConcurrentOperationsLock.Release();
|
||||
ConcurrentSemaphore.Release();
|
||||
} catch (ObjectDisposedException) { }
|
||||
}
|
||||
}
|
||||
|
@ -27,19 +27,19 @@ class BirthdayRoleUpdate : BackgroundService {
|
|||
private async Task ProcessBirthdaysAsync(CancellationToken token) {
|
||||
// For database efficiency, fetch all database information at once before proceeding
|
||||
using var db = new BotDatabaseContext();
|
||||
var shardGuilds = ShardInstance.DiscordClient.Guilds.Select(g => g.Id).ToHashSet();
|
||||
var shardGuilds = Shard.DiscordClient.Guilds.Select(g => g.Id).ToHashSet();
|
||||
var presentGuildSettings = db.GuildConfigurations.Where(s => shardGuilds.Contains(s.GuildId));
|
||||
var guildChecks = presentGuildSettings.ToList().Select(s => Tuple.Create(s.GuildId, s));
|
||||
|
||||
var exceptions = new List<Exception>();
|
||||
foreach (var (guildId, settings) in guildChecks) {
|
||||
var guild = ShardInstance.DiscordClient.GetGuild(guildId);
|
||||
var guild = Shard.DiscordClient.GetGuild(guildId);
|
||||
if (guild == null) continue; // A guild disappeared...?
|
||||
|
||||
// Check task cancellation here. Processing during a single guild is never interrupted.
|
||||
if (token.IsCancellationRequested) throw new TaskCanceledException();
|
||||
|
||||
if (ShardInstance.DiscordClient.ConnectionState != ConnectionState.Connected) {
|
||||
if (Shard.DiscordClient.ConnectionState != ConnectionState.Connected) {
|
||||
Log("Client is not connected. Stopping early.");
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -7,24 +7,26 @@ namespace BirthdayBot.BackgroundServices;
|
|||
/// Automatically removes database information for guilds that have not been accessed in a long time.
|
||||
/// </summary>
|
||||
class DataRetention : BackgroundService {
|
||||
const int ProcessInterval = 5400 / ShardBackgroundWorker.Interval; // Process about once per hour and a half
|
||||
private readonly int ProcessInterval;
|
||||
|
||||
// Amount of days without updates before data is considered stale and up for deletion.
|
||||
const int StaleGuildThreshold = 180;
|
||||
const int StaleUserThreashold = 360;
|
||||
|
||||
public DataRetention(ShardInstance instance) : base(instance) { }
|
||||
public DataRetention(ShardInstance instance) : base(instance) {
|
||||
ProcessInterval = 5400 / Shard.Config.BackgroundInterval; // Process about once per hour and a half
|
||||
}
|
||||
|
||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
// On each tick, run only a set group of guilds, each group still processed every ProcessInterval ticks.
|
||||
if ((tickCount + ShardInstance.ShardId) % ProcessInterval != 0) return;
|
||||
if ((tickCount + Shard.ShardId) % ProcessInterval != 0) return;
|
||||
|
||||
try {
|
||||
await DbConcurrentOperationsLock.WaitAsync(token);
|
||||
await ConcurrentSemaphore.WaitAsync(token);
|
||||
await RemoveStaleEntriesAsync();
|
||||
} finally {
|
||||
try {
|
||||
DbConcurrentOperationsLock.Release();
|
||||
ConcurrentSemaphore.Release();
|
||||
} catch (ObjectDisposedException) { }
|
||||
}
|
||||
}
|
||||
|
@ -34,14 +36,14 @@ class DataRetention : BackgroundService {
|
|||
var now = DateTimeOffset.UtcNow;
|
||||
|
||||
// Update guilds
|
||||
var localGuilds = ShardInstance.DiscordClient.Guilds.Select(g => g.Id).ToList();
|
||||
var localGuilds = Shard.DiscordClient.Guilds.Select(g => g.Id).ToList();
|
||||
var updatedGuilds = await db.GuildConfigurations
|
||||
.Where(g => localGuilds.Contains(g.GuildId))
|
||||
.ExecuteUpdateAsync(upd => upd.SetProperty(p => p.LastSeen, now));
|
||||
|
||||
// Update guild users
|
||||
var updatedUsers = 0;
|
||||
foreach (var guild in ShardInstance.DiscordClient.Guilds) {
|
||||
foreach (var guild in Shard.DiscordClient.Guilds) {
|
||||
var localUsers = guild.Users.Select(u => u.Id).ToList();
|
||||
updatedUsers += await db.UserEntries
|
||||
.Where(gu => gu.GuildId == guild.Id)
|
||||
|
|
|
@ -5,22 +5,25 @@ namespace BirthdayBot.BackgroundServices;
|
|||
/// Reports user count statistics to external services on a shard by shard basis.
|
||||
/// </summary>
|
||||
class ExternalStatisticsReporting : BackgroundService {
|
||||
const int ProcessInterval = 1200 / ShardBackgroundWorker.Interval; // Process every ~20 minutes
|
||||
const int ProcessOffset = 300 / ShardBackgroundWorker.Interval; // Begin processing ~5 minutes after shard start
|
||||
readonly int ProcessInterval;
|
||||
readonly int ProcessOffset;
|
||||
|
||||
private static readonly HttpClient _httpClient = new();
|
||||
|
||||
public ExternalStatisticsReporting(ShardInstance instance) : base(instance) { }
|
||||
public ExternalStatisticsReporting(ShardInstance instance) : base(instance) {
|
||||
ProcessInterval = 1200 / Shard.Config.BackgroundInterval; // Process every ~20 minutes
|
||||
ProcessOffset = 300 / Shard.Config.BackgroundInterval; // No processing until ~5 minutes after shard start
|
||||
}
|
||||
|
||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||
if (tickCount < ProcessOffset) return;
|
||||
if (tickCount % ProcessInterval != 0) return;
|
||||
|
||||
var botId = ShardInstance.DiscordClient.CurrentUser.Id;
|
||||
var botId = Shard.DiscordClient.CurrentUser.Id;
|
||||
if (botId == 0) return;
|
||||
var count = ShardInstance.DiscordClient.Guilds.Count;
|
||||
var count = Shard.DiscordClient.Guilds.Count;
|
||||
|
||||
var dbotsToken = ShardInstance.Config.DBotsToken;
|
||||
var dbotsToken = Shard.Config.DBotsToken;
|
||||
if (dbotsToken != null) await SendDiscordBots(dbotsToken, count, botId, token);
|
||||
}
|
||||
|
||||
|
@ -33,7 +36,7 @@ class ExternalStatisticsReporting : BackgroundService {
|
|||
var post = new HttpRequestMessage(HttpMethod.Post, uri);
|
||||
post.Headers.Add("Authorization", apiToken);
|
||||
post.Content = new StringContent(string.Format(Body,
|
||||
userCount, ShardInstance.Config.ShardTotal, ShardInstance.ShardId),
|
||||
userCount, Shard.Config.ShardTotal, Shard.ShardId),
|
||||
Encoding.UTF8, "application/json");
|
||||
|
||||
await _httpClient.SendAsync(post, token);
|
||||
|
|
|
@ -6,7 +6,7 @@ class ShardBackgroundWorker : IDisposable {
|
|||
/// <summary>
|
||||
/// The interval, in seconds, in which background tasks are attempted to be run within a shard.
|
||||
/// </summary>
|
||||
public const int Interval = 40;
|
||||
private int Interval { get; }
|
||||
|
||||
private readonly Task _workerTask;
|
||||
private readonly CancellationTokenSource _workerCanceller;
|
||||
|
@ -20,6 +20,7 @@ class ShardBackgroundWorker : IDisposable {
|
|||
|
||||
public ShardBackgroundWorker(ShardInstance instance) {
|
||||
Instance = instance;
|
||||
Interval = instance.Config.BackgroundInterval;
|
||||
_workerCanceller = new CancellationTokenSource();
|
||||
|
||||
_workers = new List<BackgroundService>()
|
||||
|
|
|
@ -25,6 +25,25 @@ class Configuration {
|
|||
public string SqlPassword { get; }
|
||||
internal string SqlApplicationName { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Number of seconds between each time the status task runs, in seconds.
|
||||
/// </summary>
|
||||
public int StatusInterval { get; }
|
||||
/// <summary>
|
||||
/// Number of concurrent shard startups to happen on each check.
|
||||
/// This value also determines the maximum amount of concurrent background database operations.
|
||||
/// </summary>
|
||||
public int MaxConcurrentOperations { get; }
|
||||
/// <summary>
|
||||
/// Amount of time to wait between background task runs within each shard.
|
||||
/// </summary>
|
||||
public int BackgroundInterval { get; }
|
||||
/// <summary>
|
||||
/// Gets whether to show common connect/disconnect events and other related messages.
|
||||
/// This is disabled in the public instance, but it's worth keeping enabled in self-hosted bots.
|
||||
/// </summary>
|
||||
public bool LogConnectionStatus { get; }
|
||||
|
||||
public Configuration() {
|
||||
var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs());
|
||||
var path = args?.ConfigFile ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
|
||||
|
@ -71,6 +90,11 @@ class Configuration {
|
|||
SqlUsername = ReadConfKey<string>(jc, nameof(SqlUsername), true);
|
||||
SqlPassword = ReadConfKey<string>(jc, nameof(SqlPassword), true);
|
||||
SqlApplicationName = $"Shard{ShardStart:00}-{ShardStart + ShardAmount - 1:00}";
|
||||
|
||||
StatusInterval = ReadConfKey<int?>(jc, nameof(StatusInterval), false) ?? 90;
|
||||
MaxConcurrentOperations = ReadConfKey<int?>(jc, nameof(MaxConcurrentOperations), false) ?? 4;
|
||||
BackgroundInterval = ReadConfKey<int?>(jc, nameof(BackgroundInterval), false) ?? 60;
|
||||
LogConnectionStatus = ReadConfKey<bool?>(jc, nameof(LogConnectionStatus), false) ?? true;
|
||||
}
|
||||
|
||||
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
|
||||
|
|
|
@ -72,23 +72,27 @@ public sealed class ShardInstance : IDisposable {
|
|||
private Task Client_Log(LogMessage arg) {
|
||||
// Suppress certain messages
|
||||
if (arg.Message != null) {
|
||||
switch (arg.Message) {
|
||||
case "Connecting":
|
||||
case "Connected":
|
||||
case "Ready":
|
||||
case "Disconnecting":
|
||||
case "Disconnected":
|
||||
case "Resumed previous session":
|
||||
case "Failed to resume previous session":
|
||||
case "Serializer Error": // The exception associated with this log appears a lot as of v3.2-ish
|
||||
return Task.CompletedTask;
|
||||
if (!_manager.Config.LogConnectionStatus) {
|
||||
switch (arg.Message) {
|
||||
case "Connecting":
|
||||
case "Connected":
|
||||
case "Ready":
|
||||
case "Disconnecting":
|
||||
case "Disconnected":
|
||||
case "Resumed previous session":
|
||||
case "Failed to resume previous session":
|
||||
case "Serializer Error": // The exception associated with this log appears a lot as of v3.2-ish
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
Log("Discord.Net", $"{arg.Severity}: {arg.Message}");
|
||||
}
|
||||
|
||||
if (arg.Exception != null) {
|
||||
if (arg.Exception is GatewayReconnectException
|
||||
|| arg.Exception.Message == "WebSocket connection was closed") return Task.CompletedTask;
|
||||
if (!_manager.Config.LogConnectionStatus) {
|
||||
if (arg.Exception is GatewayReconnectException || arg.Exception.Message == "WebSocket connection was closed")
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
Log("Discord.Net exception", arg.Exception.ToString());
|
||||
}
|
||||
|
|
|
@ -10,17 +10,6 @@ namespace BirthdayBot;
|
|||
/// status reports regarding the overall health of the application.
|
||||
/// </summary>
|
||||
class ShardManager : IDisposable {
|
||||
/// <summary>
|
||||
/// Number of seconds between each time the status task runs, in seconds.
|
||||
/// </summary>
|
||||
private const int StatusInterval = 90;
|
||||
|
||||
/// <summary>
|
||||
/// Number of concurrent shard startups to happen on each check.
|
||||
/// This value also determines the maximum amount of concurrent background database operations.
|
||||
/// </summary>
|
||||
public const int MaxConcurrentOperations = 4;
|
||||
|
||||
/// <summary>
|
||||
/// Amount of time without a completed background service run before a shard instance
|
||||
/// is considered "dead" and tasked to be removed.
|
||||
|
@ -158,14 +147,14 @@ class ShardManager : IDisposable {
|
|||
}
|
||||
|
||||
// Start null shards, a few at at time
|
||||
var startAllowance = MaxConcurrentOperations;
|
||||
var startAllowance = Config.MaxConcurrentOperations;
|
||||
foreach (var id in nullShards) {
|
||||
if (startAllowance-- > 0) {
|
||||
_shards[id] = await InitializeShard(id);
|
||||
} else break;
|
||||
}
|
||||
|
||||
await Task.Delay(StatusInterval * 1000, _mainCancel.Token);
|
||||
await Task.Delay(Config.StatusInterval * 1000, _mainCancel.Token);
|
||||
}
|
||||
} catch (TaskCanceledException) { }
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue