diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs index a1df07b14260..37f08dbccc10 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Indexer/AllIndexerController.cs @@ -32,16 +32,27 @@ public AllIndexerController( [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] [EndpointSummary("Gets a collection of indexers.")] [EndpointDescription("Gets a collection of configured search indexers in the Umbraco installation.")] - public Task> All( + public async Task> All( CancellationToken cancellationToken, int skip = 0, int take = 100) { - IndexResponseModel[] indexes = _examineManager.Indexes - .Select(_indexPresentationFactory.Create) - .OrderBy(indexModel => indexModel.Name.TrimEnd("Indexer")).ToArray(); + var indexes = new List(); - var viewModel = new PagedViewModel { Items = indexes.Skip(skip).Take(take), Total = indexes.Length }; - return Task.FromResult(viewModel); + foreach (IIndex index in _examineManager.Indexes) + { + indexes.Add(await _indexPresentationFactory.CreateAsync(index)); + } + + indexes.Sort((a, b) => + string.Compare(a.Name.TrimEnd("Indexer"), b.Name.TrimEnd("Indexer"), StringComparison.Ordinal)); + + var viewModel = new PagedViewModel + { + Items = indexes.Skip(skip).Take(take), + Total = indexes.Count, + }; + + return viewModel; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Indexer/DetailsIndexerController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Indexer/DetailsIndexerController.cs index 889c9345d66e..9ec00194fe7e 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Indexer/DetailsIndexerController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Indexer/DetailsIndexerController.cs @@ -37,11 +37,11 @@ public DetailsIndexerController( [ProducesResponseType(typeof(IndexResponseModel), StatusCodes.Status200OK)] [EndpointSummary("Gets indexer details.")] [EndpointDescription("Gets detailed information about the indexer identified by the provided name.")] - public Task> Details(CancellationToken cancellationToken, string indexName) + public async Task> Details(CancellationToken cancellationToken, string indexName) { if (_examineManager.TryGetIndex(indexName, out IIndex? index)) { - return Task.FromResult>(_indexPresentationFactory.Create(index)); + return await _indexPresentationFactory.CreateAsync(index); } var invalidModelProblem = new ProblemDetails @@ -52,6 +52,6 @@ public DetailsIndexerController( Type = "Error", }; - return Task.FromResult>(NotFound(invalidModelProblem)); + return NotFound(invalidModelProblem); } } diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/SearchManagementBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/SearchManagementBuilderExtensions.cs index 5852907b8820..f9317f04d02b 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/SearchManagementBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/SearchManagementBuilderExtensions.cs @@ -1,5 +1,8 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Services; @@ -13,7 +16,16 @@ internal static IUmbracoBuilder AddSearchManagement(this IUmbracoBuilder builder { // Add examine service builder.Services.AddTransient(); - builder.Services.AddTransient(); + + // TODO (V19): Revert to simple AddTransient() + // when the obsolete constructors in IndexingRebuilderService are removed. + // The explicit factory is needed to avoid ambiguous constructor resolution. + builder.Services.AddTransient(sp => + new IndexingRebuilderService( + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService(), + sp.GetRequiredService())); // Add factories builder.Services.AddTransient(); diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs index 80ea4fd2ff00..1498ff556e5d 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.Examine.cs @@ -72,8 +72,6 @@ public static IUmbracoBuilder AddExamine(this IUmbracoBuilder builder) builder.Services.AddUnique(); - builder.Services.AddTransient(); - builder.AddNotificationHandler(); builder.AddNotificationHandler(); builder.AddNotificationHandler(); diff --git a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs index 6e6d8e813182..4eb91dcc3ab8 100644 --- a/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs +++ b/src/Umbraco.Infrastructure/Examine/ExamineIndexRebuilder.cs @@ -1,26 +1,27 @@ // Copyright (c) Umbraco. // See LICENSE for more details. +using System.Collections.Concurrent; using Examine; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.HostedServices; using Umbraco.Cms.Infrastructure.Models; -using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Examine; internal class ExamineIndexRebuilder : IIndexRebuilder { - private const string RebuildAllOperationTypeName = "RebuildAllExamineIndexes"; + private const string RebuildAllKey = "__rebuild_all__"; + private readonly IBackgroundTaskQueue _backgroundTaskQueue; + private readonly ConcurrentDictionary _rebuilding = new(); private readonly IExamineManager _examineManager; private readonly ILogger _logger; private readonly IMainDom _mainDom; private readonly IEnumerable _populators; - private readonly ILongRunningOperationService _longRunningOperationService; private readonly IRuntimeState _runtimeState; /// @@ -32,14 +33,14 @@ public ExamineIndexRebuilder( ILogger logger, IExamineManager examineManager, IEnumerable populators, - ILongRunningOperationService longRunningOperationService) + IBackgroundTaskQueue backgroundTaskQueue) { _mainDom = mainDom; _runtimeState = runtimeState; _logger = logger; _examineManager = examineManager; _populators = populators; - _longRunningOperationService = longRunningOperationService; + _backgroundTaskQueue = backgroundTaskQueue; } /// @@ -56,157 +57,238 @@ public bool CanRebuild(string indexName) /// [Obsolete("Use RebuildIndexAsync() instead. Scheduled for removal in Umbraco 19.")] public virtual void RebuildIndex(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true) - => RebuildIndexAsync(indexName, delay, useBackgroundThread).GetAwaiter().GetResult(); + { + if (delay == null) + { + delay = TimeSpan.Zero; + } + + if (!CanRun()) + { + return; + } + + if (useBackgroundThread) + { + _logger.LogInformation("Starting async background thread for rebuilding index {indexName}.", indexName); + + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => + { + // Do not flow AsyncLocal to the child thread + using (ExecutionContext.SuppressFlow()) + { + Task.Run(() => RebuildIndexCoreAsync(indexName, delay.Value, cancellationToken)); + + // immediately return so the queue isn't waiting. + return Task.CompletedTask; + } + }); + } + else + { + RebuildIndexCoreAsync(indexName, delay.Value, CancellationToken.None).GetAwaiter().GetResult(); + } + } /// public virtual async Task> RebuildIndexAsync(string indexName, TimeSpan? delay = null, bool useBackgroundThread = true) { - delay ??= TimeSpan.Zero; - if (!CanRun()) { return Attempt.Fail(IndexRebuildResult.NotAllowedToRun); } - Attempt attempt = await _longRunningOperationService.RunAsync( - GetRebuildOperationTypeName(indexName), - async ct => - { - await RebuildIndex(indexName, delay.Value, ct); - return Task.CompletedTask; - }, - allowConcurrentExecution: false, - runInBackground: useBackgroundThread); + if (!_examineManager.TryGetIndex(indexName, out IIndex index)) + { + return Attempt.Fail(IndexRebuildResult.Unknown); + } - if (attempt.Success) + if (!HasRegisteredPopulator(index)) { - return Attempt.Succeed(IndexRebuildResult.Success); + return Attempt.Fail(IndexRebuildResult.Unknown); } - return attempt.Status switch + if (useBackgroundThread) + { + _logger.LogInformation("Starting async background thread for rebuilding index {indexName}.", indexName); + + _backgroundTaskQueue.QueueBackgroundWorkItem( + ct => RebuildIndexCoreAsync(indexName, delay ?? TimeSpan.Zero, ct)); + } + else { - LongRunningOperationEnqueueStatus.AlreadyRunning => Attempt.Fail(IndexRebuildResult.AlreadyRebuilding), - _ => Attempt.Fail(IndexRebuildResult.Unknown), - }; + await RebuildIndexCoreAsync(indexName, delay ?? TimeSpan.Zero, CancellationToken.None); + } + + return Attempt.Succeed(IndexRebuildResult.Success); } /// [Obsolete("Use RebuildIndexesAsync() instead. Scheduled for removal in Umbraco 19.")] public virtual void RebuildIndexes(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true) - => RebuildIndexesAsync(onlyEmptyIndexes, delay, useBackgroundThread).GetAwaiter().GetResult(); + { + if (delay == null) + { + delay = TimeSpan.Zero; + } + + if (!CanRun()) + { + return; + } + + if (useBackgroundThread) + { + if (_logger.IsEnabled(Microsoft.Extensions.Logging.LogLevel.Debug)) + { + _logger.LogDebug($"Queuing background job for {nameof(RebuildIndexes)}."); + } + + _backgroundTaskQueue.QueueBackgroundWorkItem( + cancellationToken => + { + // Do not flow AsyncLocal to the child thread + using (ExecutionContext.SuppressFlow()) + { + // This is a fire/forget task spawned by the background thread queue (which means we + // don't need to worry about ExecutionContext flowing). + Task.Run(() => RebuildIndexesCoreAsync(onlyEmptyIndexes, delay.Value, cancellationToken)); + + // immediately return so the queue isn't waiting. + return Task.CompletedTask; + } + }); + } + else + { + RebuildIndexesCoreAsync(onlyEmptyIndexes, delay.Value, CancellationToken.None).GetAwaiter().GetResult(); + } + } /// public virtual async Task> RebuildIndexesAsync(bool onlyEmptyIndexes, TimeSpan? delay = null, bool useBackgroundThread = true) { - delay ??= TimeSpan.Zero; - if (!CanRun()) { return Attempt.Fail(IndexRebuildResult.NotAllowedToRun); } - Attempt attempt = await _longRunningOperationService.RunAsync( - RebuildAllOperationTypeName, - async ct => + if (useBackgroundThread) + { + if (_logger.IsEnabled(LogLevel.Debug)) { - await RebuildIndexes(onlyEmptyIndexes, delay.Value, ct); - return Task.CompletedTask; - }, - allowConcurrentExecution: false, - runInBackground: useBackgroundThread); + _logger.LogDebug($"Queuing background job for {nameof(RebuildIndexes)}."); + } - if (attempt.Success) + _backgroundTaskQueue.QueueBackgroundWorkItem( + ct => RebuildIndexesCoreAsync(onlyEmptyIndexes, delay ?? TimeSpan.Zero, ct)); + } + else { - return Attempt.Succeed(IndexRebuildResult.Success); + await RebuildIndexesCoreAsync(onlyEmptyIndexes, delay ?? TimeSpan.Zero, CancellationToken.None); } - return attempt.Status switch - { - LongRunningOperationEnqueueStatus.AlreadyRunning => Attempt.Fail(IndexRebuildResult.AlreadyRebuilding), - _ => Attempt.Fail(IndexRebuildResult.Unknown), - }; + return Attempt.Succeed(IndexRebuildResult.Success); } /// - public async Task IsRebuildingAsync(string indexName) - => (await _longRunningOperationService.GetByTypeAsync(GetRebuildOperationTypeName(indexName), 0, 0)).Total != 0; - - private static string GetRebuildOperationTypeName(string indexName) - { - // Truncate to a maximum of 200 characters to ensure the type name doesn't overflow the database field. - const int TypeFieldSize = 200; - return $"RebuildExamineIndex-{indexName}".TruncateWithUniqueHash(TypeFieldSize); - } + public Task IsRebuildingAsync(string indexName) + => Task.FromResult(_rebuilding.ContainsKey(indexName) || _rebuilding.ContainsKey(RebuildAllKey)); private bool CanRun() => _mainDom.IsMainDom && _runtimeState.Level == RuntimeLevel.Run; - private async Task RebuildIndex(string indexName, TimeSpan delay, CancellationToken cancellationToken) + private async Task RebuildIndexCoreAsync(string indexName, TimeSpan delay, CancellationToken cancellationToken) { if (delay > TimeSpan.Zero) { await Task.Delay(delay, cancellationToken); } - if (!_examineManager.TryGetIndex(indexName, out IIndex index)) + if (!_rebuilding.TryAdd(indexName, 0)) { - throw new InvalidOperationException($"No index found with name {indexName}"); + _logger.LogWarning("Call was made to RebuildIndex but a rebuild for {IndexName} is already running", indexName); + return; } - index.CreateIndex(); // clear the index - foreach (IIndexPopulator populator in _populators) + try { - if (cancellationToken.IsCancellationRequested) + if (!_examineManager.TryGetIndex(indexName, out IIndex index)) { - return; + throw new InvalidOperationException($"No index found with name {indexName}"); } - populator.Populate(index); + index.CreateIndex(); // clear the index + foreach (IIndexPopulator populator in _populators) + { + if (cancellationToken.IsCancellationRequested) + { + return; + } + + populator.Populate(index); + } + } + finally + { + _rebuilding.TryRemove(indexName, out _); } } - private async Task RebuildIndexes(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken) + private async Task RebuildIndexesCoreAsync(bool onlyEmptyIndexes, TimeSpan delay, CancellationToken cancellationToken) { if (delay > TimeSpan.Zero) { await Task.Delay(delay, cancellationToken); } - // If an index exists but it has zero docs we'll consider it empty and rebuild - // Only include indexes that have at least one populator registered, this is to avoid emptying out indexes - // that we have no chance of repopulating, for example our own search package - IIndex[] indexes = (onlyEmptyIndexes - ? _examineManager.Indexes.Where(ShouldRebuild) - : _examineManager.Indexes) - .Where(HasRegisteredPopulator) - .ToArray(); - - if (indexes.Length == 0) + if (!_rebuilding.TryAdd(RebuildAllKey, 0)) { + _logger.LogWarning($"Call was made to {nameof(RebuildIndexes)} but the task runner for rebuilding is already running"); return; } - foreach (IIndex index in indexes) - { - index.CreateIndex(); // clear the index - } - - // run each populator over the indexes - foreach (IIndexPopulator populator in _populators) + try { - if (cancellationToken.IsCancellationRequested) + // If an index exists but it has zero docs we'll consider it empty and rebuild + IIndex[] indexes = (onlyEmptyIndexes + ? _examineManager.Indexes.Where(ShouldRebuild) + : _examineManager.Indexes) + .Where(HasRegisteredPopulator) + .ToArray(); + + if (indexes.Length == 0) { return; } - try + foreach (IIndex index in indexes) { - populator.Populate(indexes); + index.CreateIndex(); // clear the index } - catch (Exception e) + + // run each populator over the indexes + foreach (IIndexPopulator populator in _populators) { - _logger.LogError(e, "Index populating failed for populator {Populator}", populator.GetType()); + if (cancellationToken.IsCancellationRequested) + { + return; + } + + try + { + populator.Populate(indexes); + } + catch (Exception e) + { + _logger.LogError(e, "Index populating failed for populator {Populator}", populator.GetType()); + } } } + finally + { + _rebuilding.TryRemove(RebuildAllKey, out _); + } } private bool ShouldRebuild(IIndex index) diff --git a/src/Umbraco.Infrastructure/Services/IndexingRebuilderService.cs b/src/Umbraco.Infrastructure/Services/IndexingRebuilderService.cs index 00cc9d7d022f..1f0d5b975576 100644 --- a/src/Umbraco.Infrastructure/Services/IndexingRebuilderService.cs +++ b/src/Umbraco.Infrastructure/Services/IndexingRebuilderService.cs @@ -1,7 +1,14 @@ +using System.Diagnostics.CodeAnalysis; using Examine; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.OperationStatus; +using Umbraco.Cms.Core.Sync; using Umbraco.Cms.Infrastructure.Examine; using Umbraco.Cms.Infrastructure.Models; @@ -10,27 +17,58 @@ namespace Umbraco.Cms.Infrastructure.Services; /// public class IndexingRebuilderService : IIndexingRebuilderService { + private const string OperationTypePrefix = "ExamineIndexRebuild"; + private readonly IIndexRebuilder _indexRebuilder; private readonly ILogger _logger; + private readonly ILongRunningOperationService _longRunningOperationService; + private readonly IServerRoleAccessor _serverRoleAccessor; + [ActivatorUtilitiesConstructor] public IndexingRebuilderService( IIndexRebuilder indexRebuilder, - ILogger logger) + ILogger logger, + ILongRunningOperationService longRunningOperationService, + IServerRoleAccessor serverRoleAccessor) { _indexRebuilder = indexRebuilder; _logger = logger; + _longRunningOperationService = longRunningOperationService; + _serverRoleAccessor = serverRoleAccessor; } - [Obsolete("Use the non-obsolete constructor instead. Scheduled for removal in Umbraco 19.")] + [Obsolete("Use the constructor with all parameters. Scheduled for removal in Umbraco 19.")] public IndexingRebuilderService( - AppCaches runtimeCache, + AppCaches appCaches, IIndexRebuilder indexRebuilder, ILogger logger) + : this( + indexRebuilder, + logger, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [Obsolete("Use the constructor with all parameters. Scheduled for removal in Umbraco 19.")] + public IndexingRebuilderService( + IIndexRebuilder indexRebuilder, + ILogger logger) + : this( + indexRebuilder, + logger, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { - _indexRebuilder = indexRebuilder; - _logger = logger; } + private static string GetOperationType(string indexName) => $"{OperationTypePrefix}:{indexName}"; + + // Only use database tracking on servers with write access. + // Subscriber servers have read-only DB access and don't serve the backoffice. + private bool UseDatabaseOperationTracking => + _serverRoleAccessor.CurrentServerRole is ServerRole.Single or ServerRole.SchedulingPublisher; + /// public bool CanRebuild(string indexName) => _indexRebuilder.CanRebuild(indexName); @@ -44,12 +82,34 @@ public async Task TryRebuildAsync(IIndex index, string indexName) { try { - Attempt attempt = await _indexRebuilder.RebuildIndexAsync(indexName); - return attempt.Success; + if (UseDatabaseOperationTracking is false) + { + // Subscriber/Unknown servers: delegate directly without operation tracking. + Attempt result = await _indexRebuilder.RebuildIndexAsync(indexName); + return result.Success; + } + + Attempt enqueueResult = + await _longRunningOperationService.RunAsync( + GetOperationType(indexName), + async ct => + { + // useBackgroundThread: false because ILongRunningOperationService already handles backgrounding. + await _indexRebuilder.RebuildIndexAsync(indexName, useBackgroundThread: false); + }, + allowConcurrentExecution: false); + + if (enqueueResult.Status == LongRunningOperationEnqueueStatus.AlreadyRunning) + { + _logger.LogWarning("Index rebuild for {IndexName} is already running", indexName); + return false; + } + + return enqueueResult.Success; } catch (Exception exception) { - _logger.LogError(exception, "An error occurred rebuilding index"); + _logger.LogError(exception, "An error occurred rebuilding index {IndexName}", indexName); return false; } } @@ -60,6 +120,34 @@ public bool IsRebuilding(string indexName) => IsRebuildingAsync(indexName).GetAwaiter().GetResult(); /// - public Task IsRebuildingAsync(string indexName) - => _indexRebuilder.IsRebuildingAsync(indexName); + public async Task IsRebuildingAsync(string indexName) + { + // Check local in-memory state first (fast path — covers this instance). + if (await _indexRebuilder.IsRebuildingAsync(indexName)) + { + return true; + } + + if (UseDatabaseOperationTracking is false) + { + return false; + } + + // Check database for cross-server visibility (load-balanced backoffice). + try + { + PagedModel activeOps = + await _longRunningOperationService.GetByTypeAsync( + GetOperationType(indexName), + skip: 0, + take: 0); + + return activeOps.Total > 0; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to check long-running operation status for index rebuild; falling back to local state only"); + return false; + } + } } diff --git a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs index 51445c86e509..dc75c856f2df 100644 --- a/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs +++ b/tests/Umbraco.Tests.Integration/DependencyInjection/UmbracoBuilderExtensions.cs @@ -20,8 +20,9 @@ using Umbraco.Cms.Core.Runtime; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Sync; +using Umbraco.Cms.Core.HostedServices; using Umbraco.Cms.Infrastructure.Examine; -using Umbraco.Cms.Infrastructure.HostedServices; +using Umbraco.Cms.Infrastructure.Services; using Umbraco.Cms.Infrastructure.PublishedCache; using Umbraco.Cms.Persistence.EFCore.Locking; using Umbraco.Cms.Persistence.EFCore.Scoping; @@ -155,14 +156,14 @@ public TestBackgroundIndexRebuilder( ILogger logger, IExamineManager examineManager, IEnumerable populators, - ILongRunningOperationService longRunningOperationService) + IBackgroundTaskQueue backgroundTaskQueue) : base( mainDom, runtimeState, logger, examineManager, populators, - longRunningOperationService) + backgroundTaskQueue) { } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Factories/IndexPresentationFactoryTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Factories/IndexPresentationFactoryTests.cs index 87c5ae7b5dec..9bb0d1443b65 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Factories/IndexPresentationFactoryTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Cms.Api.Management/Factories/IndexPresentationFactoryTests.cs @@ -14,7 +14,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Cms.Api.Management.Factories; public class IndexPresentationFactoryTests { [Test] - public void Create_Should_Set_HealthStatusMessage_On_Diagnostics_Failure() + public async Task Create_Should_Set_HealthStatusMessage_On_Diagnostics_Failure() { var indexDiagnosticsFailureMessage = "something is wrong"; // arrange @@ -46,8 +46,8 @@ public void Create_Should_Set_HealthStatusMessage_On_Diagnostics_Failure() var indexRebuilderServiceMock = new Mock(); indexRebuilderServiceMock - .Setup(rebuilder => rebuilder.IsRebuilding(It.IsAny())) - .Returns(false); + .Setup(rebuilder => rebuilder.IsRebuildingAsync(It.IsAny())) + .ReturnsAsync(false); var factory = new IndexPresentationFactory( indexDiagnosticsFactoryMock.Object, @@ -57,7 +57,7 @@ public void Create_Should_Set_HealthStatusMessage_On_Diagnostics_Failure() // act - var responseModel = factory.Create(indexMock.Object); + var responseModel = await factory.CreateAsync(indexMock.Object); // assert Assert.AreEqual(indexDiagnosticsFailureMessage, responseModel.HealthStatus.Message);