Move cache instructions pruning to background job (#19598)

* Remove pruning logic from `CacheInstructionService.ProcessInstructions()`

* Add and register `CacheInstructionsPruningJob` background job

* Add unit tests

* Remove breaking change in ICacheInstructionService

* Adjust some obsoletion messages to mention v17

* Added missing scope

* Update tests

* Fix obsoletion messages version

* Update ProcessInstructions methods summary
This commit is contained in:
Laura Neto
2025-07-01 11:17:59 +02:00
committed by GitHub
parent 3b4639de08
commit dcd8b42522
9 changed files with 269 additions and 108 deletions

View File

@@ -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<CacheRefresherCollection>();
private IServerRoleAccessor ServerRoleAccessor => GetRequiredService<IServerRoleAccessor>();
[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<ICacheInstructionService>();
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<IServerRegistrationService>();
serverRegistrationService.TouchServer("http://localhost", TimeSpan.FromMinutes(10));
}
}

View File

@@ -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<IOptions<GlobalSettings>> _globalSettingsMock = new(MockBehavior.Strict);
private readonly Mock<ICacheInstructionRepository> _cacheInstructionRepositoryMock = new(MockBehavior.Strict);
private readonly Mock<ICoreScopeProvider> _scopeProviderMock = new(MockBehavior.Strict);
private readonly Mock<TimeProvider> _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<IsolationLevel>(),
It.IsAny<RepositoryCacheMode>(),
It.IsAny<IEventDispatcher>(),
It.IsAny<IScopedNotificationPublisher>(),
It.IsAny<bool?>(),
It.IsAny<bool>(),
It.IsAny<bool>()))
.Returns(Mock.Of<ICoreScope>());
}