Abstract submit and poll operations (#19688)
* Started implementing new LongRunningOperationService and adjusting tasks to use this service This service will manage operations that require status to be synced between servers (load balanced setup). * Missing migration to add new lock. Other simplifications. * Add job to cleanup the LongRunningOperations entries * Add new DatabaseCacheRebuilder.RebuildAsync method This is both async and returns an attempt, which will fail if a rebuild operation is already running. * Missing LongRunningOperation database table creation on clean install * Store expire date in the long running operation. Better handling of non-background operations. Storing an expiration date allows setting different expiration times depending on the type of operation, and whether it is running in the background or not. * Added integration tests for LongRunningOperationRepository * Added unit tests for LongRunningOperationService * Add type as a parameter to more repository calls. Distinguish between expiration and deletion in `LongRunningOperationRepository.CleanOperations`. * Fix failing unit test * Fixed `PerformPublishBranchAsync` result not being deserialized correctly * Remove unnecessary DatabaseCacheRebuildResult value * Add status to `LongRunningOperationService.GetResult` attempt to inform on why a result could not be retrieved * General improvements * Missing rename * Improve the handling of long running operations that are not in background and stale operations * Fix failing unit tests * Fixed small mismatch between interface and implementation * Use a fire and forget task instead of the background queue * Apply suggestions from code review Co-authored-by: Andy Butland <abutland73@gmail.com> * Make sure exceptions are caught when running in the background * Alignment with other repositories (async + pagination) * Additional fixes * Add Async suffix to service methods * Missing adjustment * Moved hardcoded settings to IOptions * Fix issue in SQL Server where 0 is not accepted as requested number of rows * Fix issue in SQL Server where query provided to count cannot contain orderby * Additional SQL Server fixes --------- Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.HostedServices;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PublishedCache;
|
||||
using Umbraco.Cms.Core.Scoping;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Services.OperationStatus;
|
||||
using Umbraco.Cms.Infrastructure.HybridCache.Persistence;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
@@ -17,7 +18,7 @@ namespace Umbraco.Cms.Infrastructure.HybridCache;
|
||||
internal sealed class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
|
||||
{
|
||||
private const string NuCacheSerializerKey = "Umbraco.Web.PublishedCache.NuCache.Serializer";
|
||||
private const string IsRebuildingDatabaseCacheRuntimeCacheKey = "temp_database_cache_rebuild_op";
|
||||
private const string RebuildOperationName = "DatabaseCacheRebuild";
|
||||
|
||||
private readonly IDatabaseCacheRepository _databaseCacheRepository;
|
||||
private readonly ICoreScopeProvider _coreScopeProvider;
|
||||
@@ -25,8 +26,7 @@ internal sealed class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
|
||||
private readonly IKeyValueService _keyValueService;
|
||||
private readonly ILogger<DatabaseCacheRebuilder> _logger;
|
||||
private readonly IProfilingLogger _profilingLogger;
|
||||
private readonly IBackgroundTaskQueue _backgroundTaskQueue;
|
||||
private readonly IAppPolicyCache _runtimeCache;
|
||||
private readonly ILongRunningOperationService _longRunningOperationService;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DatabaseCacheRebuilder"/> class.
|
||||
@@ -38,8 +38,7 @@ internal sealed class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
|
||||
IKeyValueService keyValueService,
|
||||
ILogger<DatabaseCacheRebuilder> logger,
|
||||
IProfilingLogger profilingLogger,
|
||||
IBackgroundTaskQueue backgroundTaskQueue,
|
||||
IAppPolicyCache runtimeCache)
|
||||
ILongRunningOperationService longRunningOperationService)
|
||||
{
|
||||
_databaseCacheRepository = databaseCacheRepository;
|
||||
_coreScopeProvider = coreScopeProvider;
|
||||
@@ -47,65 +46,60 @@ internal sealed class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
|
||||
_keyValueService = keyValueService;
|
||||
_logger = logger;
|
||||
_profilingLogger = profilingLogger;
|
||||
_backgroundTaskQueue = backgroundTaskQueue;
|
||||
_runtimeCache = runtimeCache;
|
||||
_longRunningOperationService = longRunningOperationService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool IsRebuilding() => _runtimeCache.Get(IsRebuildingDatabaseCacheRuntimeCacheKey) is not null;
|
||||
public bool IsRebuilding() => IsRebuildingAsync().GetAwaiter().GetResult();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task<bool> IsRebuildingAsync()
|
||||
=> (await _longRunningOperationService.GetByTypeAsync(RebuildOperationName, 0, 0)).Total != 0;
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("Use the overload with the useBackgroundThread parameter. Scheduled for removal in Umbraco 17.")]
|
||||
public void Rebuild() => Rebuild(false);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Rebuild(bool useBackgroundThread)
|
||||
{
|
||||
if (useBackgroundThread)
|
||||
{
|
||||
_logger.LogInformation("Starting async background thread for rebuilding database cache.");
|
||||
|
||||
_backgroundTaskQueue.QueueBackgroundWorkItem(
|
||||
cancellationToken =>
|
||||
{
|
||||
using (ExecutionContext.SuppressFlow())
|
||||
{
|
||||
Task.Run(() => PerformRebuild());
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
PerformRebuild();
|
||||
}
|
||||
}
|
||||
|
||||
private void PerformRebuild()
|
||||
{
|
||||
try
|
||||
{
|
||||
SetIsRebuilding();
|
||||
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
_databaseCacheRepository.Rebuild();
|
||||
scope.Complete();
|
||||
}
|
||||
finally
|
||||
{
|
||||
ClearIsRebuilding();
|
||||
}
|
||||
}
|
||||
|
||||
private void SetIsRebuilding() => _runtimeCache.Insert(IsRebuildingDatabaseCacheRuntimeCacheKey, () => "tempValue", TimeSpan.FromMinutes(10));
|
||||
|
||||
private void ClearIsRebuilding() => _runtimeCache.Clear(IsRebuildingDatabaseCacheRuntimeCacheKey);
|
||||
[Obsolete("Use RebuildAsync instead. Scheduled for removal in Umbraco 18.")]
|
||||
public void Rebuild(bool useBackgroundThread) =>
|
||||
RebuildAsync(useBackgroundThread).GetAwaiter().GetResult();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RebuildDatabaseCacheIfSerializerChanged()
|
||||
public async Task<Attempt<DatabaseCacheRebuildResult>> RebuildAsync(bool useBackgroundThread)
|
||||
{
|
||||
Attempt<Guid, LongRunningOperationEnqueueStatus> attempt = await _longRunningOperationService.RunAsync(
|
||||
RebuildOperationName,
|
||||
_ => PerformRebuild(),
|
||||
allowConcurrentExecution: false,
|
||||
runInBackground: useBackgroundThread);
|
||||
|
||||
if (attempt.Success)
|
||||
{
|
||||
return Attempt.Succeed(DatabaseCacheRebuildResult.Success);
|
||||
}
|
||||
|
||||
return attempt.Status switch
|
||||
{
|
||||
LongRunningOperationEnqueueStatus.AlreadyRunning => Attempt.Fail(DatabaseCacheRebuildResult.AlreadyRunning),
|
||||
_ => throw new InvalidOperationException(
|
||||
$"Unexpected status {attempt.Status} when trying to enqueue the database cache rebuild operation."),
|
||||
};
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void RebuildDatabaseCacheIfSerializerChanged() =>
|
||||
RebuildDatabaseCacheIfSerializerChangedAsync().GetAwaiter().GetResult();
|
||||
|
||||
/// <inheritdoc/>
|
||||
public async Task RebuildDatabaseCacheIfSerializerChangedAsync()
|
||||
{
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
NuCacheSerializerType serializer = _nucacheSettings.Value.NuCacheSerializerType;
|
||||
var currentSerializerValue = _keyValueService.GetValue(NuCacheSerializerKey);
|
||||
string? currentSerializerValue;
|
||||
using (ICoreScope scope = _coreScopeProvider.CreateCoreScope(autoComplete: true))
|
||||
{
|
||||
currentSerializerValue = _keyValueService.GetValue(NuCacheSerializerKey);
|
||||
}
|
||||
|
||||
if (Enum.TryParse(currentSerializerValue, out NuCacheSerializerType currentSerializer) && serializer == currentSerializer)
|
||||
{
|
||||
@@ -119,10 +113,24 @@ internal sealed class DatabaseCacheRebuilder : IDatabaseCacheRebuilder
|
||||
|
||||
using (_profilingLogger.TraceDuration<DatabaseCacheRebuilder>($"Rebuilding database cache with {serializer} serializer"))
|
||||
{
|
||||
Rebuild(false);
|
||||
_keyValueService.SetValue(NuCacheSerializerKey, serializer.ToString());
|
||||
await RebuildAsync(false);
|
||||
}
|
||||
}
|
||||
|
||||
private Task PerformRebuild()
|
||||
{
|
||||
using ICoreScope scope = _coreScopeProvider.CreateCoreScope();
|
||||
_databaseCacheRepository.Rebuild();
|
||||
|
||||
// If the serializer type has changed, we also need to update it in the key value store.
|
||||
var currentSerializerValue = _keyValueService.GetValue(NuCacheSerializerKey);
|
||||
if (!Enum.TryParse(currentSerializerValue, out NuCacheSerializerType currentSerializer) ||
|
||||
_nucacheSettings.Value.NuCacheSerializerType != currentSerializer)
|
||||
{
|
||||
_keyValueService.SetValue(NuCacheSerializerKey, _nucacheSettings.Value.NuCacheSerializerType.ToString());
|
||||
}
|
||||
|
||||
scope.Complete();
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user