Merge pull request #10123 from umbraco/netcore/bugfix/only-process-cache-notification-once

Netcore: Only process each cache instruction from DatabaseServerMessenger once.
This commit is contained in:
Bjarke Berg
2021-04-13 07:07:21 +02:00
committed by GitHub
4 changed files with 99 additions and 11 deletions

View File

@@ -39,6 +39,7 @@ namespace Umbraco.Cms.Core.Services
/// <param name="released">Flag indicating if process is shutting now and operations should exit.</param> /// <param name="released">Flag indicating if process is shutting now and operations should exit.</param>
/// <param name="localIdentity">Local identity of the executing AppDomain.</param> /// <param name="localIdentity">Local identity of the executing AppDomain.</param>
/// <param name="lastPruned">Date of last prune operation.</param> /// <param name="lastPruned">Date of last prune operation.</param>
CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned); /// <param name="lastId">Id of the latest processed instruction</param>
CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned, int lastId);
} }
} }

View File

@@ -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)); new CacheInstruction(0, DateTime.UtcNow, JsonConvert.SerializeObject(instructions, Formatting.None), localIdentity, instructions.Sum(x => x.JsonIdCount));
/// <inheritdoc/> /// <inheritdoc/>
public CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned) public CacheInstructionServiceProcessInstructionsResult ProcessInstructions(bool released, string localIdentity, DateTime lastPruned, int lastId)
{ {
using (_profilingLogger.DebugDuration<CacheInstructionService>("Syncing from database...")) using (_profilingLogger.DebugDuration<CacheInstructionService>("Syncing from database..."))
using (IScope scope = ScopeProvider.CreateScope()) using (IScope scope = ScopeProvider.CreateScope())
{ {
var numberOfInstructionsProcessed = ProcessDatabaseInstructions(released, localIdentity, out int lastId); var numberOfInstructionsProcessed = ProcessDatabaseInstructions(released, localIdentity, ref lastId);
// Check for pruning throttling. // Check for pruning throttling.
if (released || (DateTime.UtcNow - lastPruned) <= _globalSettings.DatabaseServerMessenger.TimeBetweenPruneOperations) 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. /// Thread safety: this is NOT thread safe. Because it is NOT meant to run multi-threaded.
/// </remarks> /// </remarks>
/// <returns>Number of instructions processed.</returns> /// <returns>Number of instructions processed.</returns>
private int ProcessDatabaseInstructions(bool released, string localIdentity, out int lastId) private int ProcessDatabaseInstructions(bool released, string localIdentity, ref int lastId)
{ {
// NOTE: // NOTE:
// We 'could' recurse to ensure that no remaining instructions are pending in the table before proceeding but I don't think that // 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 // 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 // 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. // performing some lookups which cannot be done with an active reader open.
IEnumerable<CacheInstruction> pendingInstructions = _cacheInstructionRepository.GetPendingInstructions(lastId, MaxInstructionsToRetrieve);
lastId = 0; 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 // 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. // continue processing anything otherwise we'll hold up the app domain shutdown.

View File

@@ -286,7 +286,7 @@ namespace Umbraco.Cms.Infrastructure.Sync
try try
{ {
CacheInstructionServiceProcessInstructionsResult result = CacheInstructionService.ProcessInstructions(_released, LocalIdentity, _lastPruned); CacheInstructionServiceProcessInstructionsResult result = CacheInstructionService.ProcessInstructions(_released, LocalIdentity, _lastPruned, _lastId);
if (result.InstructionsWerePruned) if (result.InstructionsWerePruned)
{ {
_lastPruned = _lastSync; _lastPruned = _lastSync;
@@ -307,8 +307,8 @@ namespace Umbraco.Cms.Infrastructure.Sync
_syncIdle.Set(); _syncIdle.Set();
} }
} }
/// <summary> /// <summary>
/// Reads the last-synced id from file into memory. /// Reads the last-synced id from file into memory.
/// </summary> /// </summary>

View File

@@ -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. // Create three instruction records, each with two instructions. First two records are for a different identity.
CreateAndDeliveryMultipleInstructions(sut); 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(() => Assert.Multiple(() =>
{ {
@@ -171,7 +171,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
CreateAndDeliveryMultipleInstructions(sut); 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); Assert.IsTrue(result.InstructionsWerePruned);
} }
@@ -183,7 +183,7 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services
CreateAndDeliveryMultipleInstructions(sut); 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(() => 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<ICacheInstructionService>();
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<ICacheInstructionService>();
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<RefreshInstruction> 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<ICacheInstructionService>();
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<RefreshInstruction> 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) private void CreateAndDeliveryMultipleInstructions(CacheInstructionService sut)
{ {
for (int i = 0; i < 3; i++) for (int i = 0; i < 3; i++)