From a8ff1952c3ff2a3fea259beef69c4c49e69dc56f Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Mon, 8 Mar 2021 17:13:37 +0100 Subject: [PATCH] Added integration tests for CacheInstructionService. --- ...ructionServiceProcessInstructionsResult.cs | 10 +- .../Implement/CacheInstructionService.cs | 20 +- .../Services/CacheInstructionServiceTests.cs | 196 ++++++++++++++++++ 3 files changed, 218 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs diff --git a/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs b/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs index 79d8ec1bbb..84116584a2 100644 --- a/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs +++ b/src/Umbraco.Core/Services/CacheInstructionServiceProcessInstructionsResult.cs @@ -11,14 +11,16 @@ namespace Umbraco.Cms.Core.Services { } + public int NumberOfInstructionsProcessed { get; private set; } + public int LastId { get; private set; } public bool InstructionsWerePruned { get; private set; } - public static CacheInstructionServiceProcessInstructionsResult AsCompleted(int lastId) => - new CacheInstructionServiceProcessInstructionsResult { LastId = lastId }; + public static CacheInstructionServiceProcessInstructionsResult AsCompleted(int numberOfInstructionsProcessed, int lastId) => + new CacheInstructionServiceProcessInstructionsResult { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId }; - public static CacheInstructionServiceProcessInstructionsResult AsCompletedAndPruned(int lastId) => - new CacheInstructionServiceProcessInstructionsResult { LastId = lastId, InstructionsWerePruned = true }; + public static CacheInstructionServiceProcessInstructionsResult AsCompletedAndPruned(int numberOfInstructionsProcessed, int lastId) => + new CacheInstructionServiceProcessInstructionsResult { NumberOfInstructionsProcessed = numberOfInstructionsProcessed, LastId = lastId, InstructionsWerePruned = true }; }; } diff --git a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs index a2b400293a..31c018d41c 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs @@ -165,6 +165,7 @@ namespace Umbraco.Cms.Core.Services.Implement using (IScope scope = ScopeProvider.CreateScope()) { _cacheInstructionRepository.Add(entity); + scope.Complete(); } } @@ -193,25 +194,30 @@ namespace Umbraco.Cms.Core.Services.Implement using (_profilingLogger.DebugDuration("Syncing from database...")) using (IScope scope = ScopeProvider.CreateScope()) { - ProcessDatabaseInstructions(released, localIdentity, out int lastId); + var numberOfInstructionsProcessed = ProcessDatabaseInstructions(released, localIdentity, out int lastId); // Check for pruning throttling. if (released || (DateTime.UtcNow - lastPruned) <= _globalSettings.DatabaseServerMessenger.TimeBetweenPruneOperations) { scope.Complete(); - return CacheInstructionServiceProcessInstructionsResult.AsCompleted(lastId); + return CacheInstructionServiceProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId); } + var instructionsWerePruned = false; switch (_serverRoleAccessor.CurrentServerRole) { case ServerRole.Single: case ServerRole.Master: PruneOldInstructions(); + instructionsWerePruned = true; break; } scope.Complete(); - return CacheInstructionServiceProcessInstructionsResult.AsCompletedAndPruned(lastId); + + return instructionsWerePruned + ? CacheInstructionServiceProcessInstructionsResult.AsCompletedAndPruned(numberOfInstructionsProcessed, lastId) + : CacheInstructionServiceProcessInstructionsResult.AsCompleted(numberOfInstructionsProcessed, lastId); } } @@ -221,7 +227,8 @@ namespace Umbraco.Cms.Core.Services.Implement /// /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. /// - private void ProcessDatabaseInstructions(bool released, string localIdentity, out int lastId) + /// Number of instructions processed. + private int ProcessDatabaseInstructions(bool released, string localIdentity, out int lastId) { // NOTE: // We 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that @@ -243,6 +250,7 @@ namespace Umbraco.Cms.Core.Services.Implement // Tracks which ones have already been processed to avoid duplicates var processed = new HashSet(); + var numberOfInstructionsProcessed = 0; // It would have been nice to do this in a Query instead of Fetch using a data reader to save // some memory however we cannot do that because inside of this loop the cache refreshers are also @@ -282,7 +290,11 @@ namespace Umbraco.Cms.Core.Services.Implement _logger.LogInformation("The current batch of instructions was not processed, app is shutting down"); break; } + + numberOfInstructionsProcessed++; } + + return numberOfInstructionsProcessed; } /// diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs new file mode 100644 index 0000000000..155bdaabaf --- /dev/null +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs @@ -0,0 +1,196 @@ +// Copyright (c) Umbraco. +// See LICENSE for more details. + +using System; +using System.Collections.Generic; +using System.Linq; +using NPoco; +using NUnit.Framework; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Scoping; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Implement; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services +{ + [TestFixture] + [UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] + public class CacheInstructionServiceTests : UmbracoIntegrationTest + { + private const string LocalIdentity = "localIdentity"; + private const string AlternateIdentity = "alternateIdentity"; + + [Test] + public void Can_Ensure_Initialized_With_No_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + CacheInstructionServiceInitializationResult result = sut.EnsureInitialized(false, 0); + + Assert.Multiple(() => + { + Assert.IsFalse(result.ColdBootRequired); + Assert.AreEqual(0, result.MaxId); + Assert.AreEqual(0, result.LastId); + }); + } + + [Test] + public void Can_Ensure_Initialized_With_UnSynced_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + List instructions = CreateInstructions(); + sut.DeliverInstructions(instructions, LocalIdentity); + + CacheInstructionServiceInitializationResult result = sut.EnsureInitialized(false, 0); + + Assert.Multiple(() => + { + Assert.IsTrue(result.ColdBootRequired); + Assert.AreEqual(1, result.MaxId); + Assert.AreEqual(-1, result.LastId); + }); + } + + [Test] + public void Can_Ensure_Initialized_With_Synced_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + List instructions = CreateInstructions(); + sut.DeliverInstructions(instructions, LocalIdentity); + + CacheInstructionServiceInitializationResult result = sut.EnsureInitialized(false, 1); + + Assert.Multiple(() => + { + Assert.IsFalse(result.ColdBootRequired); + Assert.AreEqual(1, result.LastId); + }); + } + + [Test] + public void Can_Deliver_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + List instructions = CreateInstructions(); + + sut.DeliverInstructions(instructions, LocalIdentity); + + AssertDeliveredInstructions(); + } + + [Test] + public void Can_Deliver_Instructions_In_Batches() + { + var sut = (CacheInstructionService)GetRequiredService(); + + List instructions = CreateInstructions(); + + sut.DeliverInstructionsInBatches(instructions, LocalIdentity); + + AssertDeliveredInstructions(); + } + + private List CreateInstructions() => new List + { + new RefreshInstruction(UserCacheRefresher.UniqueId, RefreshMethodType.RefreshByIds, Guid.Empty, 0, "[-1]", null), + new RefreshInstruction(UserCacheRefresher.UniqueId, RefreshMethodType.RefreshByIds, Guid.Empty, 0, "[-1]", null), + }; + + private void AssertDeliveredInstructions() + { + List cacheInstructions; + ISqlContext sqlContext = GetRequiredService(); + Sql sql = sqlContext.Sql() + .Select() + .From(); + using (IScope scope = ScopeProvider.CreateScope()) + { + cacheInstructions = scope.Database.Fetch(sql); + scope.Complete(); + } + + Assert.Multiple(() => + { + Assert.AreEqual(1, cacheInstructions.Count); + + CacheInstructionDto firstInstruction = cacheInstructions.First(); + Assert.AreEqual(2, firstInstruction.InstructionCount); + Assert.AreEqual(LocalIdentity, firstInstruction.OriginIdentity); + }); + } + + [Test] + public void Can_Process_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + + // Create three instruction records, each with two instructions. First two records are for a different identity. + CreateMultipleInstructions(sut); + + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-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(); + + CreateMultipleInstructions(sut); + + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddHours(-1)); + + Assert.IsTrue(result.InstructionsWerePruned); + } + + [Test] + public void Processes_No_Instructions_When_Released() + { + var sut = (CacheInstructionService)GetRequiredService(); + + CreateMultipleInstructions(sut); + + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(true, LocalIdentity, DateTime.UtcNow.AddSeconds(-1)); + + Assert.Multiple(() => + { + Assert.AreEqual(0, result.LastId); + Assert.AreEqual(0, result.NumberOfInstructionsProcessed); + Assert.IsFalse(result.InstructionsWerePruned); + }); + } + + private void CreateMultipleInstructions(CacheInstructionService sut) + { + for (int i = 0; i < 3; i++) + { + List instructions = CreateInstructions(); + sut.DeliverInstructions(instructions, i == 2 ? LocalIdentity : AlternateIdentity); + } + } + + private void EnsureServerRegistered() + { + IServerRegistrationService serverRegistrationService = GetRequiredService(); + serverRegistrationService.TouchServer("http://localhost", TimeSpan.FromMinutes(10)); + } + } +}