mirror of
https://github.com/Cleanuparr/Cleanuparr.git
synced 2026-10-28 10:48:49 -04:00
added cleanup of old and dry run cycles
This commit is contained in:
1 parent
24b7f34e0c
commit
af1823779a
6 files changed
+121
-3
No files matched your search
@@ -9,6 +9,7 @@ using Cleanuparr.Api.Features.Arr.Contracts.Responses;
|
||||
using Cleanuparr.Infrastructure.Features.Arr.Interfaces;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.Arr;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -278,6 +279,36 @@ public class ArrConfigControllerTests : IDisposable
|
||||
(await _dataContext.ArrInstances.CountAsync(i => i.Id == instance.Id)).ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSonarrInstance_Existing_RemovesSeekerCycles()
|
||||
{
|
||||
// Arrange
|
||||
var sonarr = await _dataContext.ArrConfigs.FirstAsync(c => c.Type == InstanceType.Sonarr);
|
||||
var instance = new ArrInstance
|
||||
{
|
||||
Name = "doomed",
|
||||
Url = new Uri("http://doomed:8989"),
|
||||
ApiKey = "k",
|
||||
ArrConfigId = sonarr.Id,
|
||||
Enabled = true,
|
||||
};
|
||||
_dataContext.ArrInstances.Add(instance);
|
||||
await _dataContext.SaveChangesAsync();
|
||||
|
||||
_eventsContext.SeekerCycles.AddRange(
|
||||
new SeekerCycle { ArrInstanceId = instance.Id },
|
||||
new SeekerCycle { ArrInstanceId = Guid.NewGuid() });
|
||||
await _eventsContext.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
var result = await _controller.DeleteSonarrInstance(instance.Id);
|
||||
|
||||
// Assert
|
||||
result.ShouldBeOfType<NoContentResult>();
|
||||
(await _eventsContext.SeekerCycles.CountAsync(c => c.ArrInstanceId == instance.Id)).ShouldBe(0);
|
||||
(await _eventsContext.SeekerCycles.CountAsync()).ShouldBe(1);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteSonarrInstance_FailsSearchEventsThatAreStillInFlight()
|
||||
{
|
||||
|
||||
@@ -350,6 +350,10 @@ public sealed class ArrConfigController : ControllerBase
|
||||
.Where(e => e.ArrInstanceId == arrInstanceId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
await _eventsContext.SeekerCycles
|
||||
.Where(e => e.ArrInstanceId == arrInstanceId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
await _eventsContext.SearchQueue
|
||||
.Where(e => e.ArrInstanceId == arrInstanceId)
|
||||
.ExecuteDeleteAsync(cancellationToken);
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
using Cleanuparr.Domain.Enums;
|
||||
using Cleanuparr.Infrastructure.Events;
|
||||
using Cleanuparr.Infrastructure.Features.DryRun;
|
||||
using Cleanuparr.Infrastructure.Tests.Features.Jobs.TestHelpers;
|
||||
using Cleanuparr.Infrastructure.Tests.TestHelpers;
|
||||
using Cleanuparr.Persistence;
|
||||
using Cleanuparr.Persistence.Models.Configuration.General;
|
||||
using Cleanuparr.Persistence.Models.Events;
|
||||
using Cleanuparr.Persistence.Models.State;
|
||||
using Microsoft.Data.Sqlite;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -189,6 +191,64 @@ public class EventCleanupServiceTests : IDisposable
|
||||
delay.ShouldBe(TimeSpan.FromSeconds(expectedSeconds));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PruneSeekerCyclesAsync_DeletesCompletedCyclesOlderThanRetention()
|
||||
{
|
||||
// Arrange
|
||||
using EventsContext eventsContext = TestDataContextFactory.CreateEvents();
|
||||
FakeTimeProvider timeProvider = new();
|
||||
EventCleanupService service = new EventCleanupService(_logger, _serviceProvider.GetRequiredService<IServiceScopeFactory>(), timeProvider, new DryRunActivity());
|
||||
|
||||
Guid oldCompletedId = Guid.NewGuid();
|
||||
eventsContext.SeekerCycles.Add(new SeekerCycle
|
||||
{
|
||||
Id = oldCompletedId,
|
||||
StartedAt = timeProvider.GetUtcNow().AddDays(-40),
|
||||
CompletedAt = timeProvider.GetUtcNow().AddDays(-31),
|
||||
CompletionReason = SeekerCycleCompletionReason.Completed,
|
||||
});
|
||||
await eventsContext.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
await service.PruneSeekerCyclesAsync(eventsContext, retentionDays: 30);
|
||||
|
||||
// Assert
|
||||
(await eventsContext.SeekerCycles.CountAsync(c => c.Id == oldCompletedId)).ShouldBe(0);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task PruneSeekerCyclesAsync_KeepsOpenAndRecentCycles()
|
||||
{
|
||||
// Arrange
|
||||
using EventsContext eventsContext = TestDataContextFactory.CreateEvents();
|
||||
FakeTimeProvider timeProvider = new();
|
||||
EventCleanupService service = new EventCleanupService(_logger, _serviceProvider.GetRequiredService<IServiceScopeFactory>(), timeProvider, new DryRunActivity());
|
||||
|
||||
Guid openCycleId = Guid.NewGuid();
|
||||
Guid recentCompletedId = Guid.NewGuid();
|
||||
eventsContext.SeekerCycles.AddRange(
|
||||
new SeekerCycle
|
||||
{
|
||||
Id = openCycleId,
|
||||
StartedAt = timeProvider.GetUtcNow().AddDays(-60),
|
||||
CompletedAt = null,
|
||||
},
|
||||
new SeekerCycle
|
||||
{
|
||||
Id = recentCompletedId,
|
||||
StartedAt = timeProvider.GetUtcNow().AddDays(-5),
|
||||
CompletedAt = timeProvider.GetUtcNow().AddDays(-1),
|
||||
CompletionReason = SeekerCycleCompletionReason.Completed,
|
||||
});
|
||||
await eventsContext.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
await service.PruneSeekerCyclesAsync(eventsContext, retentionDays: 30);
|
||||
|
||||
// Assert
|
||||
(await eventsContext.SeekerCycles.CountAsync()).ShouldBe(2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task WaitForNextRunAsync_DelayElapses_Completes()
|
||||
{
|
||||
|
||||
@@ -83,6 +83,10 @@ public sealed class DryRunPurgerTests : IDisposable
|
||||
new SeekerHistory { ItemTitle = "dry run history", IsDryRun = true, CycleId = Guid.NewGuid() },
|
||||
new SeekerHistory { ItemTitle = "real history", IsDryRun = false, CycleId = Guid.NewGuid() });
|
||||
|
||||
_eventsContext.SeekerCycles.AddRange(
|
||||
new SeekerCycle { IsDryRun = true },
|
||||
new SeekerCycle { IsDryRun = false });
|
||||
|
||||
await _eventsContext.SaveChangesAsync();
|
||||
|
||||
// Act
|
||||
@@ -93,6 +97,8 @@ public sealed class DryRunPurgerTests : IDisposable
|
||||
(await _eventsContext.Strikes.CountAsync(x => x.IsDryRun)).ShouldBe(0);
|
||||
(await _eventsContext.SeekerHistory.CountAsync(x => x.IsDryRun)).ShouldBe(0);
|
||||
(await _eventsContext.SeekerHistory.CountAsync()).ShouldBe(1);
|
||||
(await _eventsContext.SeekerCycles.CountAsync(x => x.IsDryRun)).ShouldBe(0);
|
||||
(await _eventsContext.SeekerCycles.CountAsync()).ShouldBe(1);
|
||||
|
||||
DownloadItem touched = await _eventsContext.DownloadItems.AsNoTracking().FirstAsync(x => x.DownloadId == "touched-by-dry-run");
|
||||
touched.IsMarkedForRemoval.ShouldBeTrue();
|
||||
|
||||
@@ -177,6 +177,7 @@ public class EventCleanupService : BackgroundService
|
||||
|
||||
// Prune events older than the configured retention window
|
||||
await PruneEventsAsync(eventsContext, config.HistoryRetentionDays);
|
||||
await PruneSeekerCyclesAsync(eventsContext, config.HistoryRetentionDays);
|
||||
|
||||
await CleanupStrikesAsync(eventsContext, config.StrikeInactivityWindowHours);
|
||||
|
||||
@@ -217,6 +218,19 @@ public class EventCleanupService : BackgroundService
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task PruneSeekerCyclesAsync(EventsContext eventsContext, ushort retentionDays)
|
||||
{
|
||||
DateTimeOffset cutoff = _timeProvider.GetUtcNow().AddDays(-retentionDays);
|
||||
int deleted = await eventsContext.SeekerCycles
|
||||
.Where(c => c.CompletedAt != null && c.CompletedAt < cutoff)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
if (deleted > 0)
|
||||
{
|
||||
_logger.LogInformation("Pruned {Count} search cycles older than {Days} days", deleted, retentionDays);
|
||||
}
|
||||
}
|
||||
|
||||
internal async Task PruneJobRunsAsync(EventsContext eventsContext, DateTimeOffset cutoff)
|
||||
{
|
||||
int deleted = await eventsContext.JobRuns
|
||||
|
||||
@@ -47,12 +47,15 @@ public sealed class DryRunPurger : IDryRunPurger
|
||||
int deletedHistory = await _eventsContext.SeekerHistory
|
||||
.Where(h => h.IsDryRun)
|
||||
.ExecuteDeleteAsync();
|
||||
int deletedCycles = await _eventsContext.SeekerCycles
|
||||
.Where(c => c.IsDryRun)
|
||||
.ExecuteDeleteAsync();
|
||||
|
||||
if (deletedStrikes + deletedEvents + deletedManualEvents + deletedItems + deletedHistory > 0)
|
||||
if (deletedStrikes + deletedEvents + deletedManualEvents + deletedItems + deletedHistory + deletedCycles > 0)
|
||||
{
|
||||
_logger.LogWarning(
|
||||
"Purged dry-run data: {Strikes} strikes, {Events} events, {ManualEvents} manual events, {Items} orphaned download items, {History} search history entries removed",
|
||||
deletedStrikes, deletedEvents, deletedManualEvents, deletedItems, deletedHistory);
|
||||
"Purged dry-run data: {Strikes} strikes, {Events} events, {ManualEvents} manual events, {Items} orphaned download items, {History} search history entries, {Cycles} search cycles removed",
|
||||
deletedStrikes, deletedEvents, deletedManualEvents, deletedItems, deletedHistory, deletedCycles);
|
||||
}
|
||||
|
||||
await transaction.CommitAsync();
|
||||
|
||||
Reference in new issue
Block a user