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();