mirror of
https://github.com/NoiTheCat/BirthdayBot.git
synced 2024-11-24 01:14:12 +00:00
Reduce concurrent database operations in background services
This commit is contained in:
parent
1bc241577a
commit
31d5513c9e
4 changed files with 32 additions and 11 deletions
|
@ -1,6 +1,7 @@
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
abstract class BackgroundService {
|
abstract class BackgroundService {
|
||||||
|
protected static SemaphoreSlim DbConcurrentOperationsLock { get; } = new(ShardManager.MaxConcurrentOperations);
|
||||||
protected ShardInstance ShardInstance { get; }
|
protected ShardInstance ShardInstance { get; }
|
||||||
|
|
||||||
public BackgroundService(ShardInstance instance) => ShardInstance = instance;
|
public BackgroundService(ShardInstance instance) => ShardInstance = instance;
|
||||||
|
|
|
@ -3,7 +3,6 @@ using NodaTime;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Core automatic functionality of the bot. Manages role memberships based on birthday information,
|
/// Core automatic functionality of the bot. Manages role memberships based on birthday information,
|
||||||
/// and optionally sends the announcement message to appropriate guilds.
|
/// and optionally sends the announcement message to appropriate guilds.
|
||||||
|
@ -15,11 +14,22 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
/// Processes birthday updates for all available guilds synchronously.
|
/// Processes birthday updates for all available guilds synchronously.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public override async Task OnTick(int tickCount, CancellationToken token) {
|
public override async Task OnTick(int tickCount, CancellationToken token) {
|
||||||
|
try {
|
||||||
|
await DbConcurrentOperationsLock.WaitAsync(token);
|
||||||
|
await ProcessBirthdaysAsync(token);
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
DbConcurrentOperationsLock.Release();
|
||||||
|
} catch (ObjectDisposedException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 => (long)g.Id).ToHashSet();
|
var shardGuilds = ShardInstance.DiscordClient.Guilds.Select(g => (long)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 => new Tuple<ulong, GuildConfig>((ulong)s.GuildId, s));
|
var guildChecks = presentGuildSettings.ToList().Select(s => Tuple.Create((ulong)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) {
|
||||||
|
@ -44,7 +54,7 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
// Invalid role was configured. Clear the setting and quit.
|
// Invalid role was configured. Clear the setting and quit.
|
||||||
settings.RoleId = null;
|
settings.RoleId = null;
|
||||||
db.Update(settings);
|
db.Update(settings);
|
||||||
await db.SaveChangesAsync();
|
await db.SaveChangesAsync(CancellationToken.None);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -102,13 +112,13 @@ class BirthdayRoleUpdate : BackgroundService {
|
||||||
/// Gets all known users from the given guild and returns a list including only those who are
|
/// Gets all known users from the given guild and returns a list including only those who are
|
||||||
/// currently experiencing a birthday in the respective time zone.
|
/// currently experiencing a birthday in the respective time zone.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<UserEntry> guildUsers, string? ServerDefaultTzId) {
|
public static HashSet<ulong> GetGuildCurrentBirthdays(IEnumerable<UserEntry> guildUsers, string? serverDefaultTzId) {
|
||||||
var birthdayUsers = new HashSet<ulong>();
|
var birthdayUsers = new HashSet<ulong>();
|
||||||
|
|
||||||
foreach (var record in guildUsers) {
|
foreach (var record in guildUsers) {
|
||||||
// Determine final time zone to use for calculation
|
// Determine final time zone to use for calculation
|
||||||
DateTimeZone tz = DateTimeZoneProviders.Tzdb
|
DateTimeZone tz = DateTimeZoneProviders.Tzdb
|
||||||
.GetZoneOrNull(record.TimeZone ?? ServerDefaultTzId ?? "UTC")!;
|
.GetZoneOrNull(record.TimeZone ?? serverDefaultTzId ?? "UTC")!;
|
||||||
|
|
||||||
var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
var checkNow = SystemClock.Instance.GetCurrentInstant().InZone(tz);
|
||||||
// Special case: If user's birthday is 29-Feb and it's currently not a leap year, check against 1-Mar
|
// Special case: If user's birthday is 29-Feb and it's currently not a leap year, check against 1-Mar
|
||||||
|
|
|
@ -2,13 +2,12 @@
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 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 {
|
||||||
private static readonly SemaphoreSlim _updateLock = new(ShardManager.MaxConcurrentOperations);
|
|
||||||
const int ProcessInterval = 5400 / ShardBackgroundWorker.Interval; // Process about once per hour and a half
|
const int ProcessInterval = 5400 / ShardBackgroundWorker.Interval; // Process about once per hour and a half
|
||||||
|
|
||||||
// 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;
|
||||||
|
@ -19,6 +18,17 @@ class DataRetention : BackgroundService {
|
||||||
// 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 + ShardInstance.ShardId) % ProcessInterval != 0) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await DbConcurrentOperationsLock.WaitAsync(token);
|
||||||
|
await RemoveStaleEntriesAsync();
|
||||||
|
} finally {
|
||||||
|
try {
|
||||||
|
DbConcurrentOperationsLock.Release();
|
||||||
|
} catch (ObjectDisposedException) { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task RemoveStaleEntriesAsync() {
|
||||||
using var db = new BotDatabaseContext();
|
using var db = new BotDatabaseContext();
|
||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
int updatedGuilds = 0, updatedUsers = 0;
|
int updatedGuilds = 0, updatedUsers = 0;
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
namespace BirthdayBot.BackgroundServices;
|
namespace BirthdayBot.BackgroundServices;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Handles the execution of periodic background tasks specific to each shard.
|
/// Handles the execution of periodic background tasks specific to each shard.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
@ -54,7 +53,7 @@ class ShardBackgroundWorker : IDisposable {
|
||||||
await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false);
|
await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false);
|
||||||
|
|
||||||
// Skip this round of task execution if the client is not connected
|
// Skip this round of task execution if the client is not connected
|
||||||
if (Instance.DiscordClient.ConnectionState != Discord.ConnectionState.Connected) continue;
|
if (Instance.DiscordClient.ConnectionState != ConnectionState.Connected) continue;
|
||||||
|
|
||||||
// Execute tasks sequentially
|
// Execute tasks sequentially
|
||||||
foreach (var service in _workers) {
|
foreach (var service in _workers) {
|
||||||
|
@ -62,8 +61,9 @@ class ShardBackgroundWorker : IDisposable {
|
||||||
try {
|
try {
|
||||||
if (_workerCanceller.IsCancellationRequested) break;
|
if (_workerCanceller.IsCancellationRequested) break;
|
||||||
_tickCount++;
|
_tickCount++;
|
||||||
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
|
await service.OnTick(_tickCount, _workerCanceller.Token);
|
||||||
} catch (Exception ex) when (ex is not TaskCanceledException) {
|
} catch (Exception ex) when (ex is not
|
||||||
|
(TaskCanceledException or OperationCanceledException or ObjectDisposedException)) {
|
||||||
Instance.Log(CurrentExecutingService, ex.ToString());
|
Instance.Log(CurrentExecutingService, ex.ToString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue