diff --git a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs index 8e661aa75805..02a419f8d585 100644 --- a/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs +++ b/src/Umbraco.Core/PublishedCache/IPublishedSnapshotService.cs @@ -31,6 +31,12 @@ public interface IPublishedSnapshotService : IDisposable /// IPublishedSnapshot CreatePublishedSnapshot(string? previewToken); + /// + /// Indicates if the database cache is in the process of being rebuilt. + /// + /// + bool IsRebuilding() => false; + /// /// Rebuilds internal database caches (but does not reload). /// @@ -61,6 +67,38 @@ void Rebuild( IReadOnlyCollection? mediaTypeIds = null, IReadOnlyCollection? memberTypeIds = null); + /// + /// Rebuilds internal database caches (but does not reload). + /// + /// + /// If not null will process content for the matching content types, if empty will process all + /// content + /// + /// + /// If not null will process content for the matching media types, if empty will process all + /// media + /// + /// + /// If not null will process content for the matching members types, if empty will process all + /// members + /// + /// Flag indicating whether to use a background thread for the operation and immediately return to the caller. + /// + /// + /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches + /// may rely on a database table to store pre-serialized version of documents. + /// + /// + /// This does *not* reload the caches. Caches need to be reloaded, for instance via + /// RefreshAllPublishedSnapshot method. + /// + /// + void Rebuild( + bool useBackgroundThread, + IReadOnlyCollection? contentTypeIds = null, + IReadOnlyCollection? mediaTypeIds = null, + IReadOnlyCollection? memberTypeIds = null) => Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); + /// /// Rebuilds all internal database caches (but does not reload). @@ -77,6 +115,22 @@ void Rebuild( /// void RebuildAll() => Rebuild(Array.Empty(), Array.Empty(), Array.Empty()); + /// + /// Rebuilds all internal database caches (but does not reload). + /// + /// Flag indicating whether to use a background thread for the operation and immediately return to the caller. + /// + /// + /// Forces the snapshot service to rebuild its internal database caches. For instance, some caches + /// may rely on a database table to store pre-serialized version of documents. + /// + /// + /// This does *not* reload the caches. Caches need to be reloaded, for instance via + /// RefreshAllPublishedSnapshot method. + /// + /// + void RebuildAll(bool useBackgroundThread) => Rebuild(useBackgroundThread, Array.Empty(), Array.Empty(), Array.Empty()); + /* An IPublishedCachesService implementation can rely on transaction-level events to update * its internal, database-level data, as these events are purely internal. However, it cannot * rely on cache refreshers CacheUpdated events to update itself, as these events are external diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 8aa012d11f4a..af6b8ef4cbe2 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; using Umbraco.Cms.Infrastructure.PublishedCache.Persistence; using Umbraco.Extensions; @@ -31,6 +32,9 @@ internal class PublishedSnapshotService : IPublishedSnapshotService // means faster execution, but uses memory - not sure if we want it // so making it configurable. public static readonly bool FullCacheWhenPreviewing = true; + + private const string IsRebuildingDatabaseCacheRuntimeCacheKey = "temp_database_cache_rebuild_op"; + private readonly NuCacheSettings _config; private readonly ContentDataSerializer _contentDataSerializer; private readonly IDefaultCultureAccessor _defaultCultureAccessor; @@ -51,6 +55,8 @@ internal class PublishedSnapshotService : IPublishedSnapshotService private readonly object _storesLock = new(); private readonly ISyncBootStateAccessor _syncBootStateAccessor; private readonly IVariationContextAccessor _variationContextAccessor; + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly IAppPolicyCache _runtimeCache; private long _contentGen; @@ -91,7 +97,9 @@ public PublishedSnapshotService( IPublishedModelFactory publishedModelFactory, IHostingEnvironment hostingEnvironment, IOptions config, - ContentDataSerializer contentDataSerializer) + ContentDataSerializer contentDataSerializer, + IBackgroundTaskQueue backgroundTaskQueue, + AppCaches appCaches) { _options = options; _syncBootStateAccessor = syncBootStateAccessor; @@ -111,6 +119,8 @@ public PublishedSnapshotService( _contentDataSerializer = contentDataSerializer; _config = config.Value; _publishedModelFactory = publishedModelFactory; + _backgroundTaskQueue = backgroundTaskQueue; + _runtimeCache = appCaches.RuntimeCache; } protected PublishedSnapshot? CurrentPublishedSnapshot @@ -349,12 +359,66 @@ public IPublishedSnapshot CreatePublishedSnapshot(string? previewToken) return new PublishedSnapshot(this, preview); } + /// + public bool IsRebuilding() => _runtimeCache.Get(IsRebuildingDatabaseCacheRuntimeCacheKey) is not null; + /// public void Rebuild( IReadOnlyCollection? contentTypeIds = null, IReadOnlyCollection? mediaTypeIds = null, IReadOnlyCollection? memberTypeIds = null) - => _publishedContentService.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); + => Rebuild(false, contentTypeIds, mediaTypeIds, memberTypeIds); + + /// + public void Rebuild( + bool useBackgroundThread, + IReadOnlyCollection? contentTypeIds = null, + IReadOnlyCollection? mediaTypeIds = null, + IReadOnlyCollection? memberTypeIds = null) + { + if (useBackgroundThread) + { + _logger.LogInformation("Starting async background thread for rebuilding database cache."); + + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => + { + // Do not flow AsyncLocal to the child thread + using (ExecutionContext.SuppressFlow()) + { + Task.Run(() => PerformRebuild(contentTypeIds, mediaTypeIds, memberTypeIds)); + + // immediately return so the request isn't waiting. + return Task.CompletedTask; + } + }); + } + else + { + PerformRebuild(contentTypeIds, mediaTypeIds, memberTypeIds); + } + } + + private void PerformRebuild( + IReadOnlyCollection? contentTypeIds = null, + IReadOnlyCollection? mediaTypeIds = null, + IReadOnlyCollection? memberTypeIds = null) + { + try + { + SetIsRebuilding(); + + _publishedContentService.Rebuild(contentTypeIds, mediaTypeIds, memberTypeIds); + } + finally + { + ClearIsRebuilding(); + } + } + + private void SetIsRebuilding() => _runtimeCache.Insert(IsRebuildingDatabaseCacheRuntimeCacheKey, () => "tempValue", TimeSpan.FromMinutes(10)); + + private void ClearIsRebuilding() => _runtimeCache.Clear(IsRebuildingDatabaseCacheRuntimeCacheKey); public async Task CollectAsync() { diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs index c706e35ca6f4..61213bf2e524 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotStatus.cs @@ -29,6 +29,11 @@ public string GetStatus() $"The current {typeof(IPublishedSnapshotService)} is not the default type. A status cannot be determined."; } + if (_service.IsRebuilding()) + { + return "Rebuild in progress. Please wait."; + } + // TODO: This should be private _service.EnsureCaches(); diff --git a/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs index 91cd16a0f6ba..33750d434bfe 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/PublishedSnapshotCacheStatusController.cs @@ -37,13 +37,32 @@ public PublishedSnapshotCacheStatusController( [HttpPost] public string RebuildDbCache() { - //Rebuild All + if (_publishedSnapshotService.IsRebuilding()) + { + return "Rebuild already in progress."; + } + _publishedSnapshotService.RebuildAll(); return _publishedSnapshotStatus.GetStatus(); } /// - /// Gets a status report + /// Rebuilds the Database cache on a background thread. + /// + [HttpPost] + public IActionResult RebuildDbCacheInBackground() + { + if (_publishedSnapshotService.IsRebuilding()) + { + return BadRequest("Rebuild already in progress."); + } + + _publishedSnapshotService.RebuildAll(true); + return Ok(); + } + + /// + /// Gets a status report. /// [HttpGet] public string GetStatus() => _publishedSnapshotStatus.GetStatus(); diff --git a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js index 5270892fa59a..264e24d5641d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/dashboard/settings/publishedsnapshotcache.controller.js @@ -1,4 +1,4 @@ -function publishedSnapshotCacheController($scope, $http, umbRequestHelper, localizationService, overlayService) { +function publishedSnapshotCacheController($scope, $http, umbRequestHelper, localizationService, overlayService) { var vm = this; @@ -94,12 +94,23 @@ vm.working = true; umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("publishedSnapshotCacheStatusBaseUrl", "RebuildDbCache")), - 'Failed to rebuild the cache.') - .then(function (result) { - vm.working = false; - vm.status = result; - }); + $http.post(umbRequestHelper.getApiUrl("publishedSnapshotCacheStatusBaseUrl", "RebuildDbCacheInBackground")), "Failed to queue the rebuild task.") + .then(function () { + const interval = setInterval(function () { + $http.get(umbRequestHelper.getApiUrl("publishedSnapshotCacheStatusBaseUrl", "GetStatus")) + .then(function (result) { + if (!result.data.toString().startsWith("Rebuild in progress")) { + vm.working = false; + vm.status = result.data; + clearInterval(interval); + } + }, function () { + vm.working = false; + vm.status = "Could not retrieve rebuild cache status"; + }); + + }, 2000); + }); } function init() { diff --git a/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs index 46f950ee603f..dded4a1bf54f 100644 --- a/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs +++ b/tests/Umbraco.Tests.UnitTests/TestHelpers/PublishedSnapshotServiceTestBase.cs @@ -7,6 +7,7 @@ using Moq; using NUnit.Framework; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Configuration.Models; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Hosting; @@ -22,6 +23,7 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.HostedServices; using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Infrastructure.PublishedCache.DataSource; using Umbraco.Cms.Infrastructure.Serialization; @@ -280,7 +282,9 @@ protected void InitializedCache( PublishedModelFactory, TestHelper.GetHostingEnvironment(), Options.Create(nuCacheSettings), - new ContentDataSerializer(new DictionaryOfPropertyDataSerializer())); + new ContentDataSerializer(new DictionaryOfPropertyDataSerializer()), + Mock.Of(), + AppCaches.NoCache); // invariant is the current default VariationContextAccessor.VariationContext = new VariationContext();