From b117f006c01d95cc8d2269cc4035abab73d2eccc Mon Sep 17 00:00:00 2001 From: Mole Date: Mon, 12 Apr 2021 13:27:25 +0200 Subject: [PATCH] Only process each CacheInstruction once. --- .../Services/ICacheInstructionService.cs | 3 +- .../Implement/CacheInstructionService.cs | 9 +- .../Sync/DatabaseServerMessenger.cs | 6 +- .../Services/CacheInstructionServiceTests.cs | 92 ++++++++++++++++++- 4 files changed, 99 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Core/Services/ICacheInstructionService.cs b/src/Umbraco.Core/Services/ICacheInstructionService.cs index cca80a0b0e..faf05f2237 100644 --- a/src/Umbraco.Core/Services/ICacheInstructionService.cs +++ b/src/Umbraco.Core/Services/ICacheInstructionService.cs @@ -39,6 +39,7 @@ namespace Umbraco.Cms.Core.Services /// Flag indicating if process is shutting now and operations should exit. /// Local identity of the executing AppDomain. /// Date of last prune operation. - CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned); + /// Id of the latest processed instruction + CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned, int lastId); } } diff --git a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs index 0968c3897c..0a6e945a23 100644 --- a/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs +++ b/src/Umbraco.Infrastructure/Services/Implement/CacheInstructionService.cs @@ -133,12 +133,12 @@ namespace Umbraco.Cms.Core.Services.Implement new CacheInstruction(0, DateTime.UtcNow, JsonConvert.SerializeObject(instructions, Formatting.None), localIdentity, instructions.Sum(x => x.JsonIdCount)); /// - public CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned) + public CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned, int lastId) { using (_profilingLogger.DebugDuration("Syncing from database...")) using (IScope scope = ScopeProvider.CreateScope()) { - var numberOfInstructionsProcessed = ProcessDatabaseInstructions(released, localIdentity, out int lastId); + var numberOfInstructionsProcessed = ProcessDatabaseInstructions(released, localIdentity, ref lastId); // Check for pruning throttling. if (released || (DateTime.UtcNow - lastPruned) <= _globalSettings.DatabaseServerMessenger.TimeBetweenPruneOperations) @@ -172,7 +172,7 @@ namespace Umbraco.Cms.Core.Services.Implement /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded. /// /// Number of instructions processed. - private int ProcessDatabaseInstructions(bool released, string localIdentity, out int lastId) + private int ProcessDatabaseInstructions(bool released, string localIdentity, ref 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 @@ -199,8 +199,9 @@ namespace Umbraco.Cms.Core.Services.Implement // 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 // performing some lookups which cannot be done with an active reader open. + IEnumerable pendingInstructions = _cacheInstructionRepository.GetPendingInstructions(lastId, MaxInstructionsToRetrieve); lastId = 0; - foreach (CacheInstruction instruction in _cacheInstructionRepository.GetPendingInstructions(lastId, MaxInstructionsToRetrieve)) + foreach (CacheInstruction instruction in pendingInstructions) { // If this flag gets set it means we're shutting down! In this case, we need to exit asap and cannot // continue processing anything otherwise we'll hold up the app domain shutdown. diff --git a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs index 2bd66d4954..0b2076a3a7 100644 --- a/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs +++ b/src/Umbraco.Infrastructure/Sync/DatabaseServerMessenger.cs @@ -286,7 +286,7 @@ namespace Umbraco.Cms.Infrastructure.Sync try { - CacheInstructionServiceProcessInstructionsResult result = CacheInstructionService.ProcessInstructions(_released, LocalIdentity, _lastPruned); + CacheInstructionServiceProcessInstructionsResult result = CacheInstructionService.ProcessInstructions(_released, LocalIdentity, _lastPruned, _lastId); if (result.InstructionsWerePruned) { _lastPruned = _lastSync; @@ -307,8 +307,8 @@ namespace Umbraco.Cms.Infrastructure.Sync _syncIdle.Set(); } - } - + } + /// /// Reads the last-synced id from file into memory. /// diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs index 53496e32a6..08fe94b2e6 100644 --- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs +++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/CacheInstructionServiceTests.cs @@ -152,7 +152,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services // Create three instruction records, each with two instructions. First two records are for a different identity. CreateAndDeliveryMultipleInstructions(sut); - CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-1)); + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), -1); Assert.Multiple(() => { @@ -171,7 +171,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services CreateAndDeliveryMultipleInstructions(sut); - CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddHours(-1)); + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddHours(-1), -1); Assert.IsTrue(result.InstructionsWerePruned); } @@ -183,7 +183,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services CreateAndDeliveryMultipleInstructions(sut); - CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(true, LocalIdentity, DateTime.UtcNow.AddSeconds(-1)); + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(true, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), -1); Assert.Multiple(() => { @@ -193,6 +193,92 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services }); } + [Test] + public void Processes_Instructions_Only_Once() + { + // This test shows what's happening in issue #10112 + // The DatabaseServerMessenger will run its sync operation every five seconds which calls CacheInstructionService.ProcessInstructions, + // which is why the CacheRefresherNotification keeps dispatching, because the cache instructions gets constantly processed. + var sut = (CacheInstructionService)GetRequiredService(); + CreateAndDeliveryMultipleInstructions(sut); + + var lastId = -1; + // Run once + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), 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. + lastId = result.LastId; + + // The instructions has now been processed and shouldn't be processed on the next call... + // Run again. + CacheInstructionServiceProcessInstructionsResult secondResult = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + Assert.Multiple(() => + { + Assert.AreEqual(0, 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); + }); + } + + [Test] + public void Processes_New_Instructions() + { + var sut = (CacheInstructionService)GetRequiredService(); + CreateAndDeliveryMultipleInstructions(sut); + + var lastId = -1; + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + + Assert.AreEqual(3, result.LastId); // Make sure LastId is 3, the rest is tested in other test. + lastId = result.LastId; + + // Add new instruction + List instructions = CreateInstructions(); + sut.DeliverInstructions(instructions, AlternateIdentity); + + CacheInstructionServiceProcessInstructionsResult secondResult = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + + Assert.Multiple(() => + { + Assert.AreEqual(4, secondResult.LastId); + Assert.AreEqual(1, secondResult.NumberOfInstructionsProcessed); + Assert.IsFalse(secondResult.InstructionsWerePruned); + }); + } + + [Test] + public void Correct_ID_For_Instruction_With_Same_Identity() + { + var sut = (CacheInstructionService)GetRequiredService(); + CreateAndDeliveryMultipleInstructions(sut); + + var lastId = -1; + CacheInstructionServiceProcessInstructionsResult result = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + + Assert.AreEqual(3, result.LastId); // Make sure LastId is 3, the rest is tested in other test. + lastId = result.LastId; + + // Add new instruction + List instructions = CreateInstructions(); + sut.DeliverInstructions(instructions, LocalIdentity); + + CacheInstructionServiceProcessInstructionsResult secondResult = sut.ProcessInstructions(false, LocalIdentity, DateTime.UtcNow.AddSeconds(-1), lastId); + + Assert.Multiple(() => + { + Assert.AreEqual(4, secondResult.LastId); + Assert.AreEqual(0, secondResult.NumberOfInstructionsProcessed); + Assert.IsFalse(secondResult.InstructionsWerePruned); + }); + } + private void CreateAndDeliveryMultipleInstructions(CacheInstructionService sut) { for (int i = 0; i < 3; i++)