Reduce concurrent database operations in background services

This commit is contained in:
Noi 2022-08-09 17:04:24 -07:00
parent 1bc241577a
commit 31d5513c9e
4 changed files with 32 additions and 11 deletions

View file

@ -1,6 +1,7 @@
namespace BirthdayBot.BackgroundServices;
abstract class BackgroundService {
protected static SemaphoreSlim DbConcurrentOperationsLock { get; } = new(ShardManager.MaxConcurrentOperations);
protected ShardInstance ShardInstance { get; }
public BackgroundService(ShardInstance instance) => ShardInstance = instance;

View file

@ -3,7 +3,6 @@ using NodaTime;
using System.Text;
namespace BirthdayBot.BackgroundServices;
/// <summary>
/// Core automatic functionality of the bot. Manages role memberships based on birthday information,
/// and optionally sends the announcement message to appropriate guilds.
@ -15,11 +14,22 @@ class BirthdayRoleUpdate : BackgroundService {
/// Processes birthday updates for all available guilds synchronously.
/// </summary>
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
using var db = new BotDatabaseContext();
var shardGuilds = ShardInstance.DiscordClient.Guilds.Select(g => (long)g.Id).ToHashSet();
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>();
foreach (var (guildId, settings) in guildChecks) {
@ -44,7 +54,7 @@ class BirthdayRoleUpdate : BackgroundService {
// Invalid role was configured. Clear the setting and quit.
settings.RoleId = null;
db.Update(settings);
await db.SaveChangesAsync();
await db.SaveChangesAsync(CancellationToken.None);
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
/// currently experiencing a birthday in the respective time zone.
/// </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>();
foreach (var record in guildUsers) {
// Determine final time zone to use for calculation
DateTimeZone tz = DateTimeZoneProviders.Tzdb
.GetZoneOrNull(record.TimeZone ?? ServerDefaultTzId ?? "UTC")!;
.GetZoneOrNull(record.TimeZone ?? serverDefaultTzId ?? "UTC")!;
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

View file

@ -2,13 +2,12 @@
using System.Text;
namespace BirthdayBot.BackgroundServices;
/// <summary>
/// Automatically removes database information for guilds that have not been accessed in a long time.
/// </summary>
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
// Amount of days without updates before data is considered stale and up for deletion.
const int StaleGuildThreshold = 180;
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.
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();
var now = DateTimeOffset.UtcNow;
int updatedGuilds = 0, updatedUsers = 0;

View file

@ -1,5 +1,4 @@
namespace BirthdayBot.BackgroundServices;
/// <summary>
/// Handles the execution of periodic background tasks specific to each shard.
/// </summary>
@ -54,7 +53,7 @@ class ShardBackgroundWorker : IDisposable {
await Task.Delay(Interval * 1000, _workerCanceller.Token).ConfigureAwait(false);
// 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
foreach (var service in _workers) {
@ -62,8 +61,9 @@ class ShardBackgroundWorker : IDisposable {
try {
if (_workerCanceller.IsCancellationRequested) break;
_tickCount++;
await service.OnTick(_tickCount, _workerCanceller.Token).ConfigureAwait(false);
} catch (Exception ex) when (ex is not TaskCanceledException) {
await service.OnTick(_tickCount, _workerCanceller.Token);
} catch (Exception ex) when (ex is not
(TaskCanceledException or OperationCanceledException or ObjectDisposedException)) {
Instance.Log(CurrentExecutingService, ex.ToString());
}
}