From cdd44b6da75e74b96bb0448fd87ca6a345f7f963 Mon Sep 17 00:00:00 2001 From: mole Date: Wed, 10 Dec 2025 12:43:52 +0100 Subject: [PATCH 1/2] Sync: Fix SyncBootStateAccessor to use ILastSyncedManager MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Update SyncBootStateAccessor to use the new ILastSyncedManager interface instead of the deprecated LastSyncedFileManager, which was causing cold boots to be triggered every time. - Replace LastSyncedFileManager field with ILastSyncedManager - Add new primary constructor accepting ILastSyncedManager - Add obsolete constructors for backwards compatibility using StaticServiceProvider - Update GetSyncBootState to use GetLastSyncedExternalAsync 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Sync/SyncBootStateAccessor.cs | 41 ++++++++++++++++--- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Infrastructure/Sync/SyncBootStateAccessor.cs b/src/Umbraco.Infrastructure/Sync/SyncBootStateAccessor.cs index e2180cfbcd8a..7daee38b1a23 100644 --- a/src/Umbraco.Infrastructure/Sync/SyncBootStateAccessor.cs +++ b/src/Umbraco.Infrastructure/Sync/SyncBootStateAccessor.cs @@ -1,6 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; @@ -9,7 +11,7 @@ namespace Umbraco.Cms.Infrastructure.Sync; public class SyncBootStateAccessor : ISyncBootStateAccessor { private readonly ICacheInstructionService _cacheInstructionService; - private readonly LastSyncedFileManager _lastSyncedFileManager; + private readonly ILastSyncedManager _lastSyncedManager; private readonly ILogger _logger; private GlobalSettings _globalSettings; @@ -19,24 +21,53 @@ public class SyncBootStateAccessor : ISyncBootStateAccessor public SyncBootStateAccessor( ILogger logger, - LastSyncedFileManager lastSyncedFileManager, IOptionsMonitor globalSettings, - ICacheInstructionService cacheInstructionService) + ICacheInstructionService cacheInstructionService, + ILastSyncedManager lastSyncedManager) { _logger = logger; - _lastSyncedFileManager = lastSyncedFileManager; + _lastSyncedManager = lastSyncedManager; _globalSettings = globalSettings.CurrentValue; _cacheInstructionService = cacheInstructionService; globalSettings.OnChange(x => _globalSettings = x); } + [Obsolete("Please use the constructor without LastSyncedFileManager. Scheduled for removal in Umbraco 18.")] + public SyncBootStateAccessor( + ILogger logger, + LastSyncedFileManager lastSyncedFileManager, + IOptionsMonitor globalSettings, + ICacheInstructionService cacheInstructionService, + ILastSyncedManager lastSyncedManager) + : this( + logger, + globalSettings, + cacheInstructionService, + lastSyncedManager) + { + } + + [Obsolete("Please use the constructor with ILastSyncedManager. Scheduled for removal in Umbraco 18.")] + public SyncBootStateAccessor( + ILogger logger, + LastSyncedFileManager lastSyncedFileManager, + IOptionsMonitor globalSettings, + ICacheInstructionService cacheInstructionService) + : this( + logger, + globalSettings, + cacheInstructionService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + public SyncBootState GetSyncBootState() => LazyInitializer.EnsureInitialized( ref _syncBootState, ref _syncBootStateReady, ref _syncBootStateLock, - () => InitializeColdBootState(_lastSyncedFileManager.LastSyncedId)); + () => InitializeColdBootState(_lastSyncedManager.GetLastSyncedExternalAsync().GetAwaiter().GetResult() ?? -1)); private SyncBootState InitializeColdBootState(int lastId) { From 7c939da86a7491be908f471d91fa78e8fa337d96 Mon Sep 17 00:00:00 2001 From: mole Date: Wed, 10 Dec 2025 12:44:13 +0100 Subject: [PATCH 2/2] Sync: Add integration tests for SyncBootStateAccessor MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add integration tests to verify SyncBootStateAccessor correctly determines cold boot vs warm boot state based on ILastSyncedManager. - Test cold boot when no last synced ID exists - Test warm boot when last synced external ID matches a cache instruction 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../Sync/SyncBootStateAccessorTest.cs | 64 +++++++++++++++++++ 1 file changed, 64 insertions(+) create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Sync/SyncBootStateAccessorTest.cs diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Sync/SyncBootStateAccessorTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Sync/SyncBootStateAccessorTest.cs new file mode 100644 index 000000000000..76a0ff0fa4c4 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Sync/SyncBootStateAccessorTest.cs @@ -0,0 +1,64 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; +using Umbraco.Cms.Infrastructure.Scoping; +using Umbraco.Cms.Infrastructure.Sync; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Sync; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class SyncBootStateAccessorTest : UmbracoIntegrationTest +{ + private ILastSyncedManager LastSyncedManager => GetRequiredService(); + + private SyncBootStateAccessor CreateSyncBootStateAccessor() => new( + GetRequiredService>(), + GetRequiredService>(), + GetRequiredService(), + LastSyncedManager); + + [Test] + public void Returns_ColdBoot_When_No_Last_Synced_Id() + { + // Arrange - the database is fresh with no last synced id saved + var syncBootStateAccessor = CreateSyncBootStateAccessor(); + + // Act + var result = syncBootStateAccessor.GetSyncBootState(); + + // Assert + Assert.AreEqual(SyncBootState.ColdBoot, result); + } + + [Test] + public async Task Returns_WarmBoot_When_Last_Synced_Id_Exists() + { + // Arrange - create a cache instruction (ID is auto-incremented starting at 1) + // and save the last synced external id matching it + using (var scope = ScopeProvider.CreateScope()) + { + var repo = new CacheInstructionRepository((IScopeAccessor)ScopeProvider); + repo.Add(new CacheInstruction(0, DateTime.Now, "{}", "Test", 1)); + scope.Complete(); + } + + // The auto-incremented ID will be 1 in a fresh database + await LastSyncedManager.SaveLastSyncedExternalAsync(1); + + var syncBootStateAccessor = CreateSyncBootStateAccessor(); + + // Act + var result = syncBootStateAccessor.GetSyncBootState(); + + // Assert + Assert.AreEqual(SyncBootState.WarmBoot, result); + } +}