* 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>
418 lines
16 KiB
C#
418 lines
16 KiB
C#
using System.Data;
|
|
using Microsoft.Extensions.Logging;
|
|
using Microsoft.Extensions.Options;
|
|
using Moq;
|
|
using NUnit.Framework;
|
|
using Umbraco.Cms.Core.Configuration.Models;
|
|
using Umbraco.Cms.Core.Events;
|
|
using Umbraco.Cms.Core.Models;
|
|
using Umbraco.Cms.Core.Persistence.Repositories;
|
|
using Umbraco.Cms.Core.Scoping;
|
|
using Umbraco.Cms.Core.Services;
|
|
using Umbraco.Cms.Core.Services.OperationStatus;
|
|
|
|
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Services;
|
|
|
|
[TestFixture]
|
|
public class LongRunningOperationServiceTests
|
|
{
|
|
private ILongRunningOperationService _longRunningOperationService;
|
|
private Mock<ICoreScopeProvider> _scopeProviderMock;
|
|
private Mock<ILongRunningOperationRepository> _longRunningOperationRepositoryMock;
|
|
private Mock<TimeProvider> _timeProviderMock;
|
|
private Mock<ICoreScope> _scopeMock;
|
|
|
|
[SetUp]
|
|
public void Setup()
|
|
{
|
|
_scopeProviderMock = new Mock<ICoreScopeProvider>(MockBehavior.Strict);
|
|
_longRunningOperationRepositoryMock = new Mock<ILongRunningOperationRepository>(MockBehavior.Strict);
|
|
_timeProviderMock = new Mock<TimeProvider>(MockBehavior.Strict);
|
|
_scopeMock = new Mock<ICoreScope>();
|
|
|
|
_longRunningOperationService = new LongRunningOperationService(
|
|
Options.Create(new LongRunningOperationsSettings()),
|
|
_longRunningOperationRepositoryMock.Object,
|
|
_scopeProviderMock.Object,
|
|
_timeProviderMock.Object,
|
|
Mock.Of<ILogger<LongRunningOperationService>>());
|
|
}
|
|
|
|
[Test]
|
|
public async Task Run_ReturnsFailedAttempt_WhenOperationIsAlreadyRunning()
|
|
{
|
|
SetupScopeProviderMock();
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetByTypeAsync("Test", It.IsAny<LongRunningOperationStatus[]>(), 0, 0))
|
|
.Callback<string, LongRunningOperationStatus[], int, int>((_, statuses, _, _) =>
|
|
{
|
|
Assert.AreEqual(2, statuses.Length);
|
|
Assert.Contains(LongRunningOperationStatus.Enqueued, statuses);
|
|
Assert.Contains(LongRunningOperationStatus.Running, statuses);
|
|
})
|
|
.ReturnsAsync(
|
|
new PagedModel<LongRunningOperation>
|
|
{
|
|
Total = 1,
|
|
Items = new List<LongRunningOperation> { new() { Id = Guid.NewGuid(), Type = "Test", Status = LongRunningOperationStatus.Running } },
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var result = await _longRunningOperationService.RunAsync(
|
|
"Test",
|
|
_ => Task.CompletedTask,
|
|
allowConcurrentExecution: false,
|
|
runInBackground: true);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
|
|
Assert.IsFalse(result.Success);
|
|
Assert.AreEqual(LongRunningOperationEnqueueStatus.AlreadyRunning, result.Status);
|
|
}
|
|
|
|
[Test]
|
|
public async Task Run_CreatesAndRunsOperation_WhenNotInBackground()
|
|
{
|
|
SetupScopeProviderMock();
|
|
|
|
_timeProviderMock.Setup(repo => repo.GetUtcNow())
|
|
.Returns(() => DateTime.UtcNow)
|
|
.Verifiable(Times.Exactly(2));
|
|
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.CreateAsync(It.IsAny<LongRunningOperation>(), It.IsAny<DateTimeOffset>()))
|
|
.Callback<LongRunningOperation, DateTimeOffset>((op, exp) =>
|
|
{
|
|
Assert.AreEqual("Test", op.Type);
|
|
Assert.IsNotNull(op.Id);
|
|
Assert.AreEqual(LongRunningOperationStatus.Enqueued, op.Status);
|
|
})
|
|
.Returns(Task.CompletedTask)
|
|
.Verifiable(Times.Once);
|
|
|
|
_scopeProviderMock.Setup(scopeProvider => scopeProvider.Context)
|
|
.Returns(default(IScopeContext?))
|
|
.Verifiable(Times.Exactly(1));
|
|
|
|
var expectedStatuses = new List<LongRunningOperationStatus>
|
|
{
|
|
LongRunningOperationStatus.Enqueued,
|
|
LongRunningOperationStatus.Running,
|
|
LongRunningOperationStatus.Success,
|
|
};
|
|
|
|
_longRunningOperationRepositoryMock.Setup(repo => repo.UpdateStatusAsync(It.IsAny<Guid>(), It.IsAny<LongRunningOperationStatus>(), It.IsAny<DateTimeOffset>()))
|
|
.Callback<Guid, LongRunningOperationStatus, DateTimeOffset>((id, status, exp) =>
|
|
{
|
|
Assert.Contains(status, expectedStatuses);
|
|
})
|
|
.Returns(Task.CompletedTask);
|
|
|
|
var opCalls = 0;
|
|
var result = await _longRunningOperationService.RunAsync(
|
|
"Test",
|
|
_ =>
|
|
{
|
|
opCalls++;
|
|
return Task.CompletedTask;
|
|
},
|
|
allowConcurrentExecution: true,
|
|
runInBackground: false);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
|
|
Assert.IsTrue(result.Success);
|
|
Assert.AreEqual(LongRunningOperationEnqueueStatus.Success, result.Status);
|
|
Assert.AreEqual(1, opCalls, "Operation should have run and increased the call count, since it's not configured to run in the background.");
|
|
}
|
|
|
|
[Test]
|
|
public void Run_ThrowsException_WhenAttemptingToRunOperationNotInBackgroundInsideAScope()
|
|
{
|
|
SetupScopeProviderMock();
|
|
|
|
_scopeProviderMock.Setup(scopeProvider => scopeProvider.Context)
|
|
.Returns(new ScopeContext())
|
|
.Verifiable(Times.Exactly(1));
|
|
|
|
var opCalls = 0;
|
|
Assert.ThrowsAsync<InvalidOperationException>(async () => await _longRunningOperationService.RunAsync(
|
|
"Test",
|
|
_ =>
|
|
{
|
|
opCalls++;
|
|
return Task.CompletedTask;
|
|
},
|
|
allowConcurrentExecution: true,
|
|
runInBackground: false));
|
|
Assert.AreEqual(0, opCalls, "The operation should not have been called.");
|
|
}
|
|
|
|
[Test]
|
|
public async Task Run_CreatesAndQueuesOperation_WhenInBackground()
|
|
{
|
|
SetupScopeProviderMock();
|
|
|
|
_timeProviderMock.Setup(repo => repo.GetUtcNow())
|
|
.Returns(() => DateTime.UtcNow)
|
|
.Verifiable(Times.Exactly(2));
|
|
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.CreateAsync(It.IsAny<LongRunningOperation>(), It.IsAny<DateTimeOffset>()))
|
|
.Callback<LongRunningOperation, DateTimeOffset>((op, exp) =>
|
|
{
|
|
Assert.AreEqual("Test", op.Type);
|
|
Assert.IsNotNull(op.Id);
|
|
Assert.AreEqual(LongRunningOperationStatus.Enqueued, op.Status);
|
|
})
|
|
.Returns(Task.CompletedTask)
|
|
.Verifiable(Times.Once);
|
|
|
|
var result = await _longRunningOperationService.RunAsync(
|
|
"Test",
|
|
_ => Task.CompletedTask,
|
|
allowConcurrentExecution: true,
|
|
runInBackground: true);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
|
|
Assert.IsTrue(result.Success);
|
|
Assert.AreEqual(LongRunningOperationEnqueueStatus.Success, result.Status);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetStatus_ReturnsExpectedStatus_WhenOperationExists()
|
|
{
|
|
SetupScopeProviderMock();
|
|
var operationId = Guid.NewGuid();
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetStatusAsync(operationId))
|
|
.ReturnsAsync(LongRunningOperationStatus.Running)
|
|
.Verifiable(Times.Once);
|
|
|
|
var status = await _longRunningOperationService.GetStatusAsync(operationId);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
Assert.IsTrue(status.HasValue);
|
|
Assert.AreEqual(LongRunningOperationStatus.Running, status.Value);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetStatus_ReturnsNull_WhenOperationDoesNotExist()
|
|
{
|
|
SetupScopeProviderMock();
|
|
var operationId = Guid.NewGuid();
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetStatusAsync(operationId))
|
|
.ReturnsAsync((LongRunningOperationStatus?)null)
|
|
.Verifiable(Times.Once);
|
|
|
|
var status = await _longRunningOperationService.GetStatusAsync(operationId);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
Assert.IsFalse(status.HasValue);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetByType_ReturnsExpectedOperations_WhenOperationsExist()
|
|
{
|
|
SetupScopeProviderMock();
|
|
const string operationType = "Test";
|
|
var operations = new List<LongRunningOperation>
|
|
{
|
|
new() { Id = Guid.NewGuid(), Type = operationType, Status = LongRunningOperationStatus.Running },
|
|
new() { Id = Guid.NewGuid(), Type = operationType, Status = LongRunningOperationStatus.Enqueued },
|
|
};
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetByTypeAsync(operationType, It.IsAny<LongRunningOperationStatus[]>(), 0, 100))
|
|
.Callback<string, LongRunningOperationStatus[], int, int>((_, statuses, _, _) =>
|
|
{
|
|
Assert.AreEqual(2, statuses.Length);
|
|
Assert.Contains(LongRunningOperationStatus.Enqueued, statuses);
|
|
Assert.Contains(LongRunningOperationStatus.Running, statuses);
|
|
})
|
|
.ReturnsAsync(
|
|
new PagedModel<LongRunningOperation>
|
|
{
|
|
Total = 2,
|
|
Items = operations,
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var result = await _longRunningOperationService.GetByTypeAsync(operationType, 0, 100);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(2, result.Items.Count());
|
|
Assert.AreEqual(2, result.Total);
|
|
Assert.IsTrue(result.Items.All(op => op.Type == operationType));
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetByType_ReturnsExpectedOperations_WhenOperationsExistWithProvidedStatuses()
|
|
{
|
|
SetupScopeProviderMock();
|
|
const string operationType = "Test";
|
|
var operations = new List<LongRunningOperation>
|
|
{
|
|
new() { Id = Guid.NewGuid(), Type = operationType, Status = LongRunningOperationStatus.Failed },
|
|
};
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetByTypeAsync(operationType, It.IsAny<LongRunningOperationStatus[]>(), 0, 30))
|
|
.Callback<string, LongRunningOperationStatus[], int, int>((type, statuses, _, _) =>
|
|
{
|
|
Assert.AreEqual(1, statuses.Length);
|
|
Assert.Contains(LongRunningOperationStatus.Failed, statuses);
|
|
})
|
|
.ReturnsAsync(
|
|
new PagedModel<LongRunningOperation>
|
|
{
|
|
Total = 1,
|
|
Items = operations,
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var result = await _longRunningOperationService.GetByTypeAsync(operationType, 0, 30, [LongRunningOperationStatus.Failed]);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
Assert.IsNotNull(result);
|
|
Assert.AreEqual(1, result.Total);
|
|
Assert.AreEqual(1, result.Items.Count());
|
|
Assert.IsTrue(result.Items.All(op => op.Type == operationType));
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetResult_ReturnsExpectedResult_WhenOperationExists()
|
|
{
|
|
SetupScopeProviderMock();
|
|
const string operationType = "Test";
|
|
var operationId = Guid.NewGuid();
|
|
const string expectedResult = "TestResult";
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetAsync<string>(operationId))
|
|
.ReturnsAsync(
|
|
new LongRunningOperation<string>
|
|
{
|
|
Id = operationId,
|
|
Type = operationType,
|
|
Status = LongRunningOperationStatus.Success,
|
|
Result = expectedResult,
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
Assert.IsTrue(result.Success);
|
|
Assert.AreEqual(LongRunningOperationResultStatus.Success, result.Status);
|
|
Assert.AreEqual(expectedResult, result.Result);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetResult_ReturnsFailedAttempt_WhenOperationDoesNotExist()
|
|
{
|
|
SetupScopeProviderMock();
|
|
var operationId = Guid.NewGuid();
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetAsync<string>(operationId))
|
|
.ReturnsAsync(default(LongRunningOperation<string>))
|
|
.Verifiable(Times.Once);
|
|
|
|
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
Assert.IsFalse(result.Success);
|
|
Assert.AreEqual(result.Status, LongRunningOperationResultStatus.OperationNotFound);
|
|
Assert.IsNull(result.Result);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetResult_ReturnsFailedAttempt_WhenOperationFailed()
|
|
{
|
|
SetupScopeProviderMock();
|
|
const string operationType = "Test";
|
|
var operationId = Guid.NewGuid();
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetAsync<string>(operationId))
|
|
.ReturnsAsync(
|
|
new LongRunningOperation<string>
|
|
{
|
|
Id = operationId,
|
|
Type = operationType,
|
|
Status = LongRunningOperationStatus.Failed,
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
Assert.IsFalse(result.Success);
|
|
Assert.AreEqual(result.Status, LongRunningOperationResultStatus.OperationFailed);
|
|
Assert.IsNull(result.Result);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetResult_ReturnsFailedAttempt_WhenOperationIsRunning()
|
|
{
|
|
SetupScopeProviderMock();
|
|
const string operationType = "Test";
|
|
var operationId = Guid.NewGuid();
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetAsync<string>(operationId))
|
|
.ReturnsAsync(
|
|
new LongRunningOperation<string>
|
|
{
|
|
Id = operationId,
|
|
Type = operationType,
|
|
Status = LongRunningOperationStatus.Running,
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
Assert.IsFalse(result.Success);
|
|
Assert.AreEqual(result.Status, LongRunningOperationResultStatus.OperationPending);
|
|
Assert.IsNull(result.Result);
|
|
}
|
|
|
|
[Test]
|
|
public async Task GetResult_ReturnsFailedAttempt_WhenOperationIsEnqueued()
|
|
{
|
|
SetupScopeProviderMock();
|
|
const string operationType = "Test";
|
|
var operationId = Guid.NewGuid();
|
|
_longRunningOperationRepositoryMock
|
|
.Setup(repo => repo.GetAsync<string>(operationId))
|
|
.ReturnsAsync(
|
|
new LongRunningOperation<string>
|
|
{
|
|
Id = operationId,
|
|
Type = operationType,
|
|
Status = LongRunningOperationStatus.Enqueued,
|
|
})
|
|
.Verifiable(Times.Once);
|
|
|
|
var result = await _longRunningOperationService.GetResultAsync<string>(operationId);
|
|
|
|
_longRunningOperationRepositoryMock.VerifyAll();
|
|
Assert.IsFalse(result.Success);
|
|
Assert.AreEqual(result.Status, LongRunningOperationResultStatus.OperationPending);
|
|
Assert.IsNull(result.Result);
|
|
}
|
|
|
|
private void SetupScopeProviderMock() =>
|
|
_scopeProviderMock
|
|
.Setup(x => x.CreateCoreScope(
|
|
It.IsAny<IsolationLevel>(),
|
|
It.IsAny<RepositoryCacheMode>(),
|
|
It.IsAny<IEventDispatcher>(),
|
|
It.IsAny<IScopedNotificationPublisher>(),
|
|
It.IsAny<bool?>(),
|
|
It.IsAny<bool>(),
|
|
It.IsAny<bool>()))
|
|
.Returns(_scopeMock.Object);
|
|
}
|
|
|
|
|