diff --git a/Common/Constants/Constants.cs b/Common/Constants/Constants.cs index 2d5d94ce..d6687f25 100644 --- a/Common/Constants/Constants.cs +++ b/Common/Constants/Constants.cs @@ -3,6 +3,7 @@ public static class Duration { public static readonly TimeSpan AuditRetentionTime = TimeSpan.FromDays(90); + public static readonly TimeSpan ShockerControlLogRetentionTime = TimeSpan.FromDays(365); public static readonly TimeSpan PasswordResetRequestLifetime = TimeSpan.FromHours(1); diff --git a/Common/Constants/HardLimits.cs b/Common/Constants/HardLimits.cs index 34c1063d..468218d3 100644 --- a/Common/Constants/HardLimits.cs +++ b/Common/Constants/HardLimits.cs @@ -47,4 +47,6 @@ public static class HardLimits public const int ShockerControlLogCustomNameMaxLength = 64; public const int CreateShareRequestMaxShockers = 128; + + public const int MaxShockerControlLogsPerUser = 2048; } diff --git a/Cron/Jobs/ClearOldShockerControlLogs.cs b/Cron/Jobs/ClearOldShockerControlLogs.cs new file mode 100644 index 00000000..446a55ea --- /dev/null +++ b/Cron/Jobs/ClearOldShockerControlLogs.cs @@ -0,0 +1,70 @@ +using Microsoft.EntityFrameworkCore; +using OpenShock.Common; +using OpenShock.Common.Constants; +using OpenShock.Common.OpenShockDb; +using OpenShock.Cron.Attributes; + +namespace OpenShock.Cron.Jobs; + +/// +/// Deletes shocker control logs older than the retention period and enforces a maximum log count +/// +[CronJob("0 0 * * *")] // Every day at midnight (https://crontab.guru/) +public sealed class ClearOldShockerControlLogs +{ + private readonly OpenShockContext _db; + private readonly ILogger _logger; + + /// + /// DI constructor + /// + /// + /// + public ClearOldShockerControlLogs(OpenShockContext db, ILogger logger) + { + _db = db; + _logger = logger; + } + + public async Task Execute() + { + // Calculate the retention threshold based on configured retention time. + var retentionThreshold = DateTime.UtcNow - Duration.ShockerControlLogRetentionTime; + + // Delete logs older than the retention threshold. + var deletedByAge = await _db.ShockerControlLogs + .Where(log => log.CreatedOn < retentionThreshold) + .ExecuteDeleteAsync(); + + _logger.LogInformation("Deleted {deletedCount} shocker control logs older than {retentionThreshold:O}.", deletedByAge, retentionThreshold); + + var userLogsCounts = await _db.ShockerControlLogs + .GroupBy(log => log.Shocker.DeviceNavigation.Owner) + .Select(group => new + { + UserId = group.Key, + CountToDelete = Math.Max(0, group.Count() - HardLimits.MaxShockerControlLogsPerUser), + DeleteBeforeDate = group + .OrderByDescending(log => log.CreatedOn) + .Skip(HardLimits.MaxShockerControlLogsPerUser) + .Select(log => log.CreatedOn) + .FirstOrDefault() + }) + .Where(result => result.CountToDelete > 0) + .ToArrayAsync(); + + if (userLogsCounts.Length != 0) + { + _logger.LogInformation("A total of {totalLogsToDelete} logs will be deleted to enforce per-user limits.", userLogsCounts.Sum(x => x.CountToDelete)); + + foreach (var userLogCount in userLogsCounts) + { + await _db.ShockerControlLogs + .Where(log => log.Shocker.DeviceNavigation.Owner == userLogCount.UserId && log.CreatedOn < userLogCount.DeleteBeforeDate) + .ExecuteDeleteAsync(); + } + } + + _logger.LogInformation("Done!"); + } +} \ No newline at end of file