diff --git a/src/Umbraco.Core/Services/ICacheInstructionService.cs b/src/Umbraco.Core/Services/ICacheInstructionService.cs index 25b52c09e3..94ccea42b4 100644 --- a/src/Umbraco.Core/Services/ICacheInstructionService.cs +++ b/src/Umbraco.Core/Services/ICacheInstructionService.cs @@ -34,14 +34,37 @@ public interface ICacheInstructionService void DeliverInstructionsInBatches(IEnumerable instructions, string localIdentity); /// - /// Processes and then prunes pending database cache instructions. + /// Processes pending database cache instructions. + /// + /// Cache refreshers. + /// Cancellation token. + /// Local identity of the executing AppDomain. + /// Id of the latest processed instruction. + /// The processing result. + ProcessInstructionsResult ProcessInstructions( + CacheRefresherCollection cacheRefreshers, + CancellationToken cancellationToken, + string localIdentity, + int lastId) => + ProcessInstructions( + cacheRefreshers, + ServerRole.Unknown, + cancellationToken, + localIdentity, + lastPruned: DateTime.UtcNow, + lastId); + + /// + /// Processes pending database cache instructions. /// /// Cache refreshers. /// Server role. /// Cancellation token. /// Local identity of the executing AppDomain. /// Date of last prune operation. - /// Id of the latest processed instruction + /// Id of the latest processed instruction. + /// The processing result. + [Obsolete("Use the non-obsolete overload. Scheduled for removal in V17.")] ProcessInstructionsResult ProcessInstructions( CacheRefresherCollection cacheRefreshers, ServerRole serverRole, diff --git a/src/Umbraco.Core/Services/ProcessInstructionsResult.cs b/src/Umbraco.Core/Services/ProcessInstructionsResult.cs index 39751dad61..e109d1ed10 100644 --- a/src/Umbraco.Core/Services/ProcessInstructionsResult.cs +++ b/src/Umbraco.Core/Services/ProcessInstructionsResult.cs @@ -14,11 +14,13 @@ public class ProcessInstructionsResult public int LastId { get; private set; } + [Obsolete("Instruction pruning has been moved to a separate background job. Scheduled for removal in V18.")] public bool InstructionsWerePruned { get; private set; } public static ProcessInstructionsResult AsCompleted(int numberOfInstructionsProcessed, int lastId) => new() { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId }; + [Obsolete("Instruction pruning has been moved to a separate background job. Scheduled for removal in V18.")] public static ProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) => new() { diff --git a/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJob.cs b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJob.cs new file mode 100644 index 0000000000..fca52f1581 --- /dev/null +++ b/src/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJob.cs @@ -0,0 +1,61 @@ +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.Scoping; + +namespace Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +/// +/// A background job that prunes cache instructions from the database. +/// +public class CacheInstructionsPruningJob : IRecurringBackgroundJob +{ + private readonly IOptions _globalSettings; + private readonly ICacheInstructionRepository _cacheInstructionRepository; + private readonly ICoreScopeProvider _scopeProvider; + private readonly TimeProvider _timeProvider; + + /// + /// Initializes a new instance of the class. + /// + /// Provides scopes for database operations. + /// The global settings configuration. + /// The repository for cache instructions. + /// The time provider. + public CacheInstructionsPruningJob( + IOptions globalSettings, + ICacheInstructionRepository cacheInstructionRepository, + ICoreScopeProvider scopeProvider, + TimeProvider timeProvider) + { + _globalSettings = globalSettings; + _cacheInstructionRepository = cacheInstructionRepository; + _scopeProvider = scopeProvider; + _timeProvider = timeProvider; + Period = globalSettings.Value.DatabaseServerMessenger.TimeBetweenPruneOperations; + } + + /// + public event EventHandler PeriodChanged + { + add { } + remove { } + } + + /// + public TimeSpan Period { get; } + + /// + public Task RunJobAsync() + { + DateTimeOffset pruneDate = _timeProvider.GetUtcNow() - _globalSettings.Value.DatabaseServerMessenger.TimeToRetainInstructions; + using (ICoreScope scope = _scopeProvider.CreateCoreScope()) + { + _cacheInstructionRepository.DeleteInstructionsOlderThan(pruneDate.DateTime); + scope.Complete(); + } + + return Task.CompletedTask; + } +} diff --git a/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs index f8e9f1ee88..dc697c9a69 100644 --- a/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs +++ b/src/Umbraco.Infrastructure/Services/CacheInstructionService.cs @@ -122,43 +122,30 @@ namespace Umbraco.Cms /// public ProcessInstructionsResult ProcessInstructions( CacheRefresherCollection cacheRefreshers, - ServerRole serverRole, CancellationToken cancellationToken, string localIdentity, - DateTime lastPruned, int lastId) { using (!_profilingLogger.IsEnabled(Core.Logging.LogLevel.Debug) ? null : _profilingLogger.DebugDuration("Syncing from database...")) using (ICoreScope scope = ScopeProvider.CreateCoreScope()) { var numberOfInstructionsProcessed = ProcessDatabaseInstructions(cacheRefreshers, cancellationToken, localIdentity, ref lastId); - - // Check for pruning throttling. - if (cancellationToken.IsCancellationRequested || DateTime.UtcNow - lastPruned <= - _globalSettings.DatabaseServerMessenger.TimeBetweenPruneOperations) - { - scope.Complete(); - return ProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId); - } - - var instructionsWerePruned = false; - switch (serverRole) - { - case ServerRole.Single: - case ServerRole.SchedulingPublisher: - PruneOldInstructions(); - instructionsWerePruned = true; - break; - } - scope.Complete(); - - return instructionsWerePruned - ? ProcessInstructionsResult.AsCompletedAndPruned(numberOfInstructionsProcessed, lastId) - : ProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId); + return ProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId); } } + /// + [Obsolete("Use the non-obsolete overload. Scheduled for removal in V17.")] + public ProcessInstructionsResult ProcessInstructions( + CacheRefresherCollection cacheRefreshers, + ServerRole serverRole, + CancellationToken cancellationToken, + string localIdentity, + DateTime lastPruned, + int lastId) => + ProcessInstructions(cacheRefreshers, cancellationToken, localIdentity, lastId); + private CacheInstruction CreateCacheInstruction(IEnumerable instructions, string localIdentity) => new( 0, @@ -486,21 +473,6 @@ namespace Umbraco.Cms return jsonRefresher; } - - /// - /// Remove old instructions from the database - /// - /// - /// Always leave the last (most recent) record in the db table, this is so that not all instructions are removed which - /// would cause - /// the site to cold boot if there's been no instruction activity for more than TimeToRetainInstructions. - /// See: http://issues.umbraco.org/issue/U4-7643#comment=67-25085 - /// - private void PruneOldInstructions() - { - DateTime pruneDate = DateTime.UtcNow - _globalSettings.DatabaseServerMessenger.TimeToRetainInstructions; - _cacheInstructionRepository.DeleteInstructionsOlderThan(pruneDate); - } } } } diff --git a/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs index 89fb1b6ad1..a6380df4b4 100644 --- a/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Infrastructure/Sync/BatchedDatabaseServerMessenger.cs @@ -16,12 +16,41 @@ namespace Umbraco.Cms.Infrastructure.Sync; /// public class BatchedDatabaseServerMessenger : DatabaseServerMessenger { - private readonly IRequestAccessor _requestAccessor; private readonly IRequestCache _requestCache; /// /// Initializes a new instance of the class. /// + public BatchedDatabaseServerMessenger( + IMainDom mainDom, + CacheRefresherCollection cacheRefreshers, + ILogger logger, + ISyncBootStateAccessor syncBootStateAccessor, + IHostingEnvironment hostingEnvironment, + ICacheInstructionService cacheInstructionService, + IJsonSerializer jsonSerializer, + IRequestCache requestCache, + LastSyncedFileManager lastSyncedFileManager, + IOptionsMonitor globalSettings) + : base( + mainDom, + cacheRefreshers, + logger, + true, + syncBootStateAccessor, + hostingEnvironment, + cacheInstructionService, + jsonSerializer, + lastSyncedFileManager, + globalSettings) + { + _requestCache = requestCache; + } + + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in V18.")] public BatchedDatabaseServerMessenger( IMainDom mainDom, CacheRefresherCollection cacheRefreshers, @@ -35,11 +64,18 @@ public class BatchedDatabaseServerMessenger : DatabaseServerMessenger IRequestAccessor requestAccessor, LastSyncedFileManager lastSyncedFileManager, IOptionsMonitor globalSettings) - : base(mainDom, cacheRefreshers, serverRoleAccessor, logger, true, syncBootStateAccessor, hostingEnvironment, - cacheInstructionService, jsonSerializer, lastSyncedFileManager, globalSettings) + : this( + mainDom, + cacheRefreshers, + logger, + syncBootStateAccessor, + hostingEnvironment, + cacheInstructionService, + jsonSerializer, + requestCache, + lastSyncedFileManager, + globalSettings) { - _requestCache = requestCache; - _requestAccessor = requestAccessor; } /// diff --git a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs index ee85d6c3a8..096318d349 100644 --- a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs @@ -31,11 +31,9 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable */ private readonly IMainDom _mainDom; - private readonly IServerRoleAccessor _serverRoleAccessor; private readonly ISyncBootStateAccessor _syncBootStateAccessor; private readonly ManualResetEvent _syncIdle; private bool _disposedValue; - private DateTime _lastPruned; private DateTime _lastSync; private bool _syncing; @@ -45,7 +43,6 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable protected DatabaseServerMessenger( IMainDom mainDom, CacheRefresherCollection cacheRefreshers, - IServerRoleAccessor serverRoleAccessor, ILogger logger, bool distributedEnabled, ISyncBootStateAccessor syncBootStateAccessor, @@ -59,7 +56,6 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable _cancellationToken = _cancellationTokenSource.Token; _mainDom = mainDom; _cacheRefreshers = cacheRefreshers; - _serverRoleAccessor = serverRoleAccessor; _hostingEnvironment = hostingEnvironment; Logger = logger; _syncBootStateAccessor = syncBootStateAccessor; @@ -67,7 +63,7 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable JsonSerializer = jsonSerializer; _lastSyncedFileManager = lastSyncedFileManager; GlobalSettings = globalSettings.CurrentValue; - _lastPruned = _lastSync = DateTime.UtcNow; + _lastSync = DateTime.UtcNow; _syncIdle = new ManualResetEvent(true); globalSettings.OnChange(x => GlobalSettings = x); @@ -84,6 +80,36 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable _initialized = new Lazy(InitializeWithMainDom); } + /// + /// Initializes a new instance of the class. + /// + [Obsolete("Use the non-obsolete constructor. Scheduled for removal in V18.")] + protected DatabaseServerMessenger( + IMainDom mainDom, + CacheRefresherCollection cacheRefreshers, + IServerRoleAccessor serverRoleAccessor, + ILogger logger, + bool distributedEnabled, + ISyncBootStateAccessor syncBootStateAccessor, + IHostingEnvironment hostingEnvironment, + ICacheInstructionService cacheInstructionService, + IJsonSerializer jsonSerializer, + LastSyncedFileManager lastSyncedFileManager, + IOptionsMonitor globalSettings) + : this( + mainDom, + cacheRefreshers, + logger, + distributedEnabled, + syncBootStateAccessor, + hostingEnvironment, + cacheInstructionService, + jsonSerializer, + lastSyncedFileManager, + globalSettings) + { + } + public GlobalSettings GlobalSettings { get; private set; } protected ILogger Logger { get; } @@ -146,17 +172,10 @@ public abstract class DatabaseServerMessenger : ServerMessengerBase, IDisposable { ProcessInstructionsResult result = CacheInstructionService.ProcessInstructions( _cacheRefreshers, - _serverRoleAccessor.CurrentServerRole, _cancellationToken, LocalIdentity, - _lastPruned, _lastSyncedFileManager.LastSyncedId); - if (result.InstructionsWerePruned) - { - _lastPruned = _lastSync; - } - if (result.LastId > 0) { _lastSyncedFileManager.SaveLastSyncedId(result.LastId); diff --git a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs index 936609db54..2fd5348f4a 100644 --- a/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/src/Umbraco.Web.Common/DependencyInjection/UmbracoBuilderExtensions.cs @@ -189,6 +189,7 @@ public static partial class UmbracoBuilderExtensions builder.Services.AddRecurringBackgroundJob(); builder.Services.AddRecurringBackgroundJob(); builder.Services.AddRecurringBackgroundJob(); + builder.Services.AddRecurringBackgroundJob(); builder.Services.AddSingleton(RecurringBackgroundJobHostedService.CreateHostedServiceFactory); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs index 7759874b8a..7633297bfd 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs @@ -20,8 +20,10 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest private const string LocalIdentity = "localIdentity"; private const string AlternateIdentity = "alternateIdentity"; - private CancellationToken CancellationToken => new(); + private CancellationToken CancellationToken => CancellationToken.None; + private CacheRefresherCollection CacheRefreshers => GetRequiredService(); + private IServerRoleAccessor ServerRoleAccessor => GetRequiredService(); [Test] @@ -150,33 +152,16 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest // Create three instruction records, each with two instructions. First two records are for a different identity. CreateAndDeliveryMultipleInstructions(sut); - var result = sut.ProcessInstructions(CacheRefreshers, ServerRoleAccessor.CurrentServerRole, CancellationToken, - LocalIdentity, DateTime.UtcNow.AddSeconds(-1), -1); + var result = sut.ProcessInstructions(CacheRefreshers, CancellationToken, LocalIdentity, -1); Assert.Multiple(() => { Assert.AreEqual(3, result.LastId); // 3 records found. Assert.AreEqual(2, result.NumberOfInstructionsProcessed); // 2 records processed (as one is for the same identity). - Assert.IsFalse(result.InstructionsWerePruned); }); } - [Test] - public void Can_Process_And_Purge_Instructions() - { - // Purging of instructions only occurs on single or master servers, so we need to ensure this is set before running the test. - EnsureServerRegistered(); - var sut = (CacheInstructionService)GetRequiredService(); - - CreateAndDeliveryMultipleInstructions(sut); - - var result = sut.ProcessInstructions(CacheRefreshers, ServerRoleAccessor.CurrentServerRole, CancellationToken, - LocalIdentity, DateTime.UtcNow.AddHours(-1), -1); - - Assert.IsTrue(result.InstructionsWerePruned); - } - [Test] public void Processes_No_Instructions_When_CancellationToken_is_Cancelled() { @@ -187,14 +172,12 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest var cancellationTokenSource = new CancellationTokenSource(); cancellationTokenSource.Cancel(); - var result = sut.ProcessInstructions(CacheRefreshers, ServerRoleAccessor.CurrentServerRole, - cancellationTokenSource.Token, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), -1); + var result = sut.ProcessInstructions(CacheRefreshers, cancellationTokenSource.Token, LocalIdentity, -1); Assert.Multiple(() => { Assert.AreEqual(0, result.LastId); Assert.AreEqual(0, result.NumberOfInstructionsProcessed); - Assert.IsFalse(result.InstructionsWerePruned); }); } @@ -209,14 +192,12 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest var lastId = -1; // Run once - var result = sut.ProcessInstructions(CacheRefreshers, ServerRoleAccessor.CurrentServerRole, CancellationToken, - LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + var result = sut.ProcessInstructions(CacheRefreshers, CancellationToken, LocalIdentity, lastId); Assert.Multiple(() => { Assert.AreEqual(3, result.LastId); // 3 records found. Assert.AreEqual(2, result.NumberOfInstructionsProcessed); // 2 records processed (as one is for the same identity). - Assert.IsFalse(result.InstructionsWerePruned); }); // DatabaseServerMessenger stores the LastID after ProcessInstructions has been run. @@ -224,13 +205,7 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest // The instructions has now been processed and shouldn't be processed on the next call... // Run again. - var secondResult = sut.ProcessInstructions( - CacheRefreshers, - ServerRoleAccessor.CurrentServerRole, - CancellationToken, - LocalIdentity, - DateTime.UtcNow.AddSeconds(-1), - lastId); + var secondResult = sut.ProcessInstructions(CacheRefreshers, CancellationToken, LocalIdentity, lastId); Assert.Multiple(() => { Assert.AreEqual( @@ -238,7 +213,6 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest secondResult .LastId); // No instructions was processed so LastId is 0, this is consistent with behavior from V8 Assert.AreEqual(0, secondResult.NumberOfInstructionsProcessed); // Nothing was processed. - Assert.IsFalse(secondResult.InstructionsWerePruned); }); } @@ -249,8 +223,7 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest CreateAndDeliveryMultipleInstructions(sut); var lastId = -1; - var result = sut.ProcessInstructions(CacheRefreshers, ServerRoleAccessor.CurrentServerRole, CancellationToken, - LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + var result = sut.ProcessInstructions(CacheRefreshers, CancellationToken, LocalIdentity, lastId); Assert.AreEqual(3, result.LastId); // Make sure LastId is 3, the rest is tested in other test. lastId = result.LastId; @@ -259,14 +232,12 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest var instructions = CreateInstructions(); sut.DeliverInstructions(instructions, AlternateIdentity); - var secondResult = sut.ProcessInstructions(CacheRefreshers, ServerRoleAccessor.CurrentServerRole, - CancellationToken, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + var secondResult = sut.ProcessInstructions(CacheRefreshers, CancellationToken, LocalIdentity, lastId); Assert.Multiple(() => { Assert.AreEqual(4, secondResult.LastId); Assert.AreEqual(1, secondResult.NumberOfInstructionsProcessed); - Assert.IsFalse(secondResult.InstructionsWerePruned); }); } @@ -277,8 +248,7 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest CreateAndDeliveryMultipleInstructions(sut); var lastId = -1; - var result = sut.ProcessInstructions(CacheRefreshers, ServerRoleAccessor.CurrentServerRole, CancellationToken, - LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + var result = sut.ProcessInstructions(CacheRefreshers, CancellationToken, LocalIdentity, lastId); Assert.AreEqual(3, result.LastId); // Make sure LastId is 3, the rest is tested in other test. lastId = result.LastId; @@ -287,14 +257,12 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest var instructions = CreateInstructions(); sut.DeliverInstructions(instructions, LocalIdentity); - var secondResult = sut.ProcessInstructions(CacheRefreshers, ServerRoleAccessor.CurrentServerRole, - CancellationToken, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + var secondResult = sut.ProcessInstructions(CacheRefreshers, CancellationToken, LocalIdentity, lastId); Assert.Multiple(() => { Assert.AreEqual(4, secondResult.LastId); Assert.AreEqual(0, secondResult.NumberOfInstructionsProcessed); - Assert.IsFalse(secondResult.InstructionsWerePruned); }); } @@ -306,10 +274,4 @@ internal sealed class CacheInstructionServiceTests : UmbracoIntegrationTest sut.DeliverInstructions(instructions, i == 2 ? LocalIdentity : AlternateIdentity); } } - - private void EnsureServerRegistered() - { - var serverRegistrationService = GetRequiredService(); - serverRegistrationService.TouchServer("http://localhost", TimeSpan.FromMinutes(10)); - } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJobTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJobTests.cs new file mode 100644 index 0000000000..5ac59498fb --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/BackgroundJobs/Jobs/CacheInstructionsPruningJobTests.cs @@ -0,0 +1,85 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System.Data; +using Microsoft.Extensions.Options; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Infrastructure.BackgroundJobs.Jobs; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.BackgroundJobs.Jobs; + +public class CacheInstructionsPruningJobTests +{ + private readonly Mock> _globalSettingsMock = new(MockBehavior.Strict); + private readonly Mock _cacheInstructionRepositoryMock = new(MockBehavior.Strict); + private readonly Mock _scopeProviderMock = new(MockBehavior.Strict); + private readonly Mock _timeProviderMock = new(MockBehavior.Strict); + + [Test] + public void Run_Period_Is_Retrieved_From_GlobalSettings() + { + var timeBetweenPruneOperations = TimeSpan.FromMinutes(2); + var job = CreateCacheInstructionsPruningJob(timeBetweenPruneOperations); + Assert.AreEqual(timeBetweenPruneOperations, job.Period, "The run period should be the same as 'TimeBetweenPruneOperations'."); + } + + [Test] + public async Task RunJobAsync_Calls_DeleteInstructionsOlderThan_With_Expected_Date() + { + SetupScopeProviderMock(); + + var timeToRetainInstructions = TimeSpan.FromMinutes(30); + var now = DateTime.UtcNow; + var expectedPruneDate = now - timeToRetainInstructions; + + _timeProviderMock.Setup(tp => tp.GetUtcNow()).Returns(now); + _cacheInstructionRepositoryMock.Setup(repo => repo + .DeleteInstructionsOlderThan(expectedPruneDate)); + + var job = CreateCacheInstructionsPruningJob(timeToRetainInstructions: timeToRetainInstructions); + + await job.RunJobAsync(); + + _cacheInstructionRepositoryMock.Verify(repo => repo.DeleteInstructionsOlderThan(expectedPruneDate), Times.Once); + } + + private CacheInstructionsPruningJob CreateCacheInstructionsPruningJob( + TimeSpan? timeBetweenPruneOperations = null, + TimeSpan? timeToRetainInstructions = null) + { + timeBetweenPruneOperations ??= TimeSpan.FromMinutes(5); + timeToRetainInstructions ??= TimeSpan.FromMinutes(20); + + var globalSettings = new GlobalSettings + { + DatabaseServerMessenger = new DatabaseServerMessengerSettings + { + TimeBetweenPruneOperations = timeBetweenPruneOperations.Value, + TimeToRetainInstructions = timeToRetainInstructions.Value, + }, + }; + + _globalSettingsMock + .Setup(g => g.Value) + .Returns(globalSettings); + + return new CacheInstructionsPruningJob(_globalSettingsMock.Object, _cacheInstructionRepositoryMock.Object, _scopeProviderMock.Object, _timeProviderMock.Object); + } + + private void SetupScopeProviderMock() => + _scopeProviderMock + .Setup(x => x.CreateCoreScope( + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny(), + It.IsAny())) + .Returns(Mock.Of()); +}