mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-10-16 07:59:57 +00:00
Allow configuration of certain time-based values
This commit is contained in:
parent
c92350861a
commit
b0b39bbd0b
8 changed files with 59 additions and 42 deletions
|
@ -13,14 +13,14 @@ class AutoUserDownload : BackgroundService {
|
||||||
|
|
||||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
// Take action if a guild's cache is incomplete...
|
// Take action if a guild's cache is incomplete...
|
||||||
var incompleteCaches = ShardInstance.DiscordClient.Guilds
|
var incompleteCaches = Shard.DiscordClient.Guilds
|
||||||
.Where(g => !g.HasAllMembers)
|
.Where(g => !g.HasAllMembers)
|
||||||
.Select(g => g.Id)
|
.Select(g => g.Id)
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
// ...and if the guild contains any user data
|
// ...and if the guild contains any user data
|
||||||
IEnumerable<ulong> mustFetch;
|
IEnumerable<ulong> mustFetch;
|
||||||
try {
|
try {
|
||||||
await DbConcurrentOperationsLock.WaitAsync(token);
|
await ConcurrentSemaphore.WaitAsync(token);
|
||||||
using var db = new BotDatabaseContext();
|
using var db = new BotDatabaseContext();
|
||||||
lock (_failedDownloads)
|
lock (_failedDownloads)
|
||||||
mustFetch = db.UserEntries.AsNoTracking()
|
mustFetch = db.UserEntries.AsNoTracking()
|
||||||
|
@ -31,7 +31,7 @@ class AutoUserDownload : BackgroundService {
|
||||||
.ToList();
|
.ToList();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
DbConcurrentOperationsLock.Release();
|
ConcurrentSemaphore.Release();
|
||||||
} catch (ObjectDisposedException) { }
|
} catch (ObjectDisposedException) { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -39,9 +39,9 @@ class AutoUserDownload : BackgroundService {
|
||||||
var processStartTime = DateTimeOffset.UtcNow;
|
var processStartTime = DateTimeOffset.UtcNow;
|
||||||
foreach (var item in mustFetch) {
|
foreach (var item in mustFetch) {
|
||||||
// May cause a disconnect in certain situations. Make no further attempts until the next pass if it happens.
|
// 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...?
|
if (guild == null) continue; // A guild disappeared...?
|
||||||
|
|
||||||
await Task.Delay(200, CancellationToken.None); // Delay a bit (reduces the possibility of hanging, somehow).
|
await Task.Delay(200, CancellationToken.None); // Delay a bit (reduces the possibility of hanging, somehow).
|
||||||
|
|
|
@ -1,11 +1,15 @@
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
abstract class BackgroundService {
|
abstract class BackgroundService {
|
||||||
protected static SemaphoreSlim DbConcurrentOperationsLock { get; } = new(ShardManager.MaxConcurrentOperations);
|
protected static SemaphoreSlim ConcurrentSemaphore { get; private set; } = null!;
|
||||||
protected ShardInstance ShardInstance { get; }
|
|
||||||
|
|
||||||
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);
|
public abstract Task OnTick(int tickCount, CancellationToken token);
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,11 +15,11 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
try {
|
try {
|
||||||
await DbConcurrentOperationsLock.WaitAsync(token);
|
await ConcurrentSemaphore.WaitAsync(token);
|
||||||
await ProcessBirthdaysAsync(token);
|
await ProcessBirthdaysAsync(token);
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
DbConcurrentOperationsLock.Release();
|
ConcurrentSemaphore.Release();
|
||||||
} catch (ObjectDisposedException) { }
|
} catch (ObjectDisposedException) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,19 +27,19 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
private async Task ProcessBirthdaysAsync(CancellationToken token) {
|
private async Task ProcessBirthdaysAsync(CancellationToken token) {
|
||||||
// For database efficiency, fetch all database information at once before proceeding
|
// For database efficiency, fetch all database information at once before proceeding
|
||||||
using var db = new BotDatabaseContext();
|
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 presentGuildSettings = db.GuildConfigurations.Where(s => shardGuilds.Contains(s.GuildId));
|
||||||
var guildChecks = presentGuildSettings.ToList().Select(s => Tuple.Create(s.GuildId, s));
|
var guildChecks = presentGuildSettings.ToList().Select(s => Tuple.Create(s.GuildId, s));
|
||||||
|
|
||||||
var exceptions = new List<Exception>();
|
var exceptions = new List<Exception>();
|
||||||
foreach (var (guildId, settings) in guildChecks) {
|
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...?
|
if (guild == null) continue; // A guild disappeared...?
|
||||||
|
|
||||||
// Check task cancellation here. Processing during a single guild is never interrupted.
|
// Check task cancellation here. Processing during a single guild is never interrupted.
|
||||||
if (token.IsCancellationRequested) throw new TaskCanceledException();
|
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.");
|
Log("Client is not connected. Stopping early.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,24 +7,26 @@ namespace BirthdayBot.BackgroundServices;
|
||||||
/// Automatically removes database information for guilds that have not been accessed in a long time.
|
/// Automatically removes database information for guilds that have not been accessed in a long time.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class DataRetention : BackgroundService {
|
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.
|
// Amount of days without updates before data is considered stale and up for deletion.
|
||||||
const int StaleGuildThreshold = 180;
|
const int StaleGuildThreshold = 180;
|
||||||
const int StaleUserThreashold = 360;
|
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) {
|
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.
|
// 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 {
|
try {
|
||||||
await DbConcurrentOperationsLock.WaitAsync(token);
|
await ConcurrentSemaphore.WaitAsync(token);
|
||||||
await RemoveStaleEntriesAsync();
|
await RemoveStaleEntriesAsync();
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
DbConcurrentOperationsLock.Release();
|
ConcurrentSemaphore.Release();
|
||||||
} catch (ObjectDisposedException) { }
|
} catch (ObjectDisposedException) { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -34,14 +36,14 @@ class DataRetention : BackgroundService {
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
// Update guilds
|
// 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
|
var updatedGuilds = await db.GuildConfigurations
|
||||||
.Where(g => localGuilds.Contains(g.GuildId))
|
.Where(g => localGuilds.Contains(g.GuildId))
|
||||||
.ExecuteUpdateAsync(upd => upd.SetProperty(p => p.LastSeen, now));
|
.ExecuteUpdateAsync(upd => upd.SetProperty(p => p.LastSeen, now));
|
||||||
|
|
||||||
// Update guild users
|
// Update guild users
|
||||||
var updatedUsers = 0;
|
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();
|
var localUsers = guild.Users.Select(u => u.Id).ToList();
|
||||||
updatedUsers += await db.UserEntries
|
updatedUsers += await db.UserEntries
|
||||||
.Where(gu => gu.GuildId == guild.Id)
|
.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.
|
/// Reports user count statistics to external services on a shard by shard basis.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class ExternalStatisticsReporting : BackgroundService {
|
class ExternalStatisticsReporting : BackgroundService {
|
||||||
const int ProcessInterval = 1200 / ShardBackgroundWorker.Interval; // Process every ~20 minutes
|
readonly int ProcessInterval;
|
||||||
const int ProcessOffset = 300 / ShardBackgroundWorker.Interval; // Begin processing ~5 minutes after shard start
|
readonly int ProcessOffset;
|
||||||
|
|
||||||
private static readonly HttpClient _httpClient = new();
|
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) {
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
if (tickCount < ProcessOffset) return;
|
if (tickCount < ProcessOffset) return;
|
||||||
if (tickCount % ProcessInterval != 0) return;
|
if (tickCount % ProcessInterval != 0) return;
|
||||||
|
|
||||||
var botId = ShardInstance.DiscordClient.CurrentUser.Id;
|
var botId = Shard.DiscordClient.CurrentUser.Id;
|
||||||
if (botId == 0) return;
|
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);
|
if (dbotsToken != null) await SendDiscordBots(dbotsToken, count, botId, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -33,7 +36,7 @@ class ExternalStatisticsReporting : BackgroundService {
|
||||||
var post = new HttpRequestMessage(HttpMethod.Post, uri);
|
var post = new HttpRequestMessage(HttpMethod.Post, uri);
|
||||||
post.Headers.Add("Authorization", apiToken);
|
post.Headers.Add("Authorization", apiToken);
|
||||||
post.Content = new StringContent(string.Format(Body,
|
post.Content = new StringContent(string.Format(Body,
|
||||||
userCount, ShardInstance.Config.ShardTotal, ShardInstance.ShardId),
|
userCount, Shard.Config.ShardTotal, Shard.ShardId),
|
||||||
Encoding.UTF8, "application/json");
|
Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
await _httpClient.SendAsync(post, token);
|
await _httpClient.SendAsync(post, token);
|
||||||
|
|
|
@ -6,7 +6,7 @@ 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>
|
||||||
public const int Interval = 40;
|
private int Interval { get; }
|
||||||
|
|
||||||
private readonly Task _workerTask;
|
private readonly Task _workerTask;
|
||||||
private readonly CancellationTokenSource _workerCanceller;
|
private readonly CancellationTokenSource _workerCanceller;
|
||||||
|
@ -20,6 +20,7 @@ class ShardBackgroundWorker : IDisposable {
|
||||||
|
|
||||||
public ShardBackgroundWorker(ShardInstance instance) {
|
public ShardBackgroundWorker(ShardInstance instance) {
|
||||||
Instance = instance;
|
Instance = instance;
|
||||||
|
Interval = instance.Config.BackgroundInterval;
|
||||||
_workerCanceller = new CancellationTokenSource();
|
_workerCanceller = new CancellationTokenSource();
|
||||||
|
|
||||||
_workers = new List<BackgroundService>()
|
_workers = new List<BackgroundService>()
|
||||||
|
|
|
@ -25,6 +25,20 @@ class Configuration {
|
||||||
public string SqlPassword { get; }
|
public string SqlPassword { get; }
|
||||||
internal string SqlApplicationName { 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; }
|
||||||
|
|
||||||
public Configuration() {
|
public Configuration() {
|
||||||
var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs());
|
var args = CommandLineParameters.Parse(Environment.GetCommandLineArgs());
|
||||||
var path = args?.ConfigFile ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
|
var path = args?.ConfigFile ?? Path.GetDirectoryName(Assembly.GetEntryAssembly()!.Location)
|
||||||
|
@ -71,6 +85,10 @@ class Configuration {
|
||||||
SqlUsername = ReadConfKey<string>(jc, nameof(SqlUsername), true);
|
SqlUsername = ReadConfKey<string>(jc, nameof(SqlUsername), true);
|
||||||
SqlPassword = ReadConfKey<string>(jc, nameof(SqlPassword), true);
|
SqlPassword = ReadConfKey<string>(jc, nameof(SqlPassword), true);
|
||||||
SqlApplicationName = $"Shard{ShardStart:00}-{ShardStart + ShardAmount - 1:00}";
|
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
|
private static T? ReadConfKey<T>(JObject jc, string key, [DoesNotReturnIf(true)] bool failOnEmpty) {
|
||||||
|
|
|
@ -10,17 +10,6 @@ namespace BirthdayBot;
|
||||||
/// status reports regarding the overall health of the application.
|
/// status reports regarding the overall health of the application.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
class ShardManager : IDisposable {
|
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>
|
/// <summary>
|
||||||
/// Amount of time without a completed background service run before a shard instance
|
/// Amount of time without a completed background service run before a shard instance
|
||||||
/// is considered "dead" and tasked to be removed.
|
/// is considered "dead" and tasked to be removed.
|
||||||
|
@ -158,14 +147,14 @@ class ShardManager : IDisposable {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start null shards, a few at at time
|
// Start null shards, a few at at time
|
||||||
var startAllowance = MaxConcurrentOperations;
|
var startAllowance = Config.MaxConcurrentOperations;
|
||||||
foreach (var id in nullShards) {
|
foreach (var id in nullShards) {
|
||||||
if (startAllowance-- > 0) {
|
if (startAllowance-- > 0) {
|
||||||
_shards[id] = await InitializeShard(id);
|
_shards[id] = await InitializeShard(id);
|
||||||
} else break;
|
} else break;
|
||||||
}
|
}
|
||||||
|
|
||||||
await Task.Delay(StatusInterval * 1000, _mainCancel.Token);
|
await Task.Delay(Config.StatusInterval * 1000, _mainCancel.Token);
|
||||||
}
|
}
|
||||||
} catch (TaskCanceledException) { }
|
} catch (TaskCanceledException) { }
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue