elsa-core/test/unit/Elsa.Resilience.Core.UnitTests/ResilienceStrategyConfigEvaluatorTests.cs
Sipke Schoorstra ca268c16ad
Enhances distributed lock handling with resilience (#7161)
* Add retry mechanism for distributed locks with transient error handling and logging

- Introduced Polly-based retry pipeline for distributed lock acquisition in `DistributedWorkflowClient` to handle transient errors such as network issues or database connection failures.
- Added detailed logging for retry attempts and lock release errors.
- Updated project dependencies to include Polly.

* Refactor transient exception handling to shared resilience module.

Migrated transient exception detection logic from scheduling module to a new shared resilience module. Updated services, jobs, and features to utilize the centralized `ITransientExceptionDetectionService`. This change improves maintainability and promotes reusability across modules.

* Add unit tests for transient exception detection and resilience strategy evaluation.

- Introduced comprehensive unit tests for `DefaultTransientExceptionDetector`, `ResilienceStrategyCatalog`, `ResilienceStrategyConfigEvaluator`, and `TransientExceptionDetectionService`.
- Added helper classes and test data factories to facilitate reusable test patterns for resilience modules.
- Updated solution to include `Elsa.Resilience.Core.UnitTests` project.

* Add component tests for distributed lock resilience

- Introduced new tests to verify retry behavior during transient lock acquisition and release failures.
- Added `TestDistributedLockProvider` and related mocks for simulating transient failures.
- Updated `WorkflowServer` test services to support the new distributed lock test scenarios.

* Refactor distributed lock resilience tests

- Consolidated test logic: streamlined test providers, injected services, and reusable test patterns.
- Simplified `TestDistributedLockProvider` implementation with enhanced initialization and failure simulation.
- Reorganized tests for transient acquisition/release failures to use parameterized `Theory` for improved maintainability.

* Refactor transient exception handling: rename interfaces and classes for consistency, update references across codebase, and improve code readability.

* Apply suggestion from @Copilot

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Simplify WorkflowServer setup and DistributedLockResilienceTests by replacing IDistributedLockProvider with TestDistributedLockProvider.

* Remove unused `using` directives in unit tests to improve code cleanliness.

* Remove `TransientExceptionTypes` helper and inline its usage in tests for improved maintainability.

* Add descriptive `DisplayName` attributes to unit tests for improved test clarity.

* Fix redundant exception checking in TransientExceptionDetector (#7162)

* Initial plan

* Fix redundant exception checking in TransientExceptionDetector

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Extract MaxRetryAttempts constant in DistributedLockResilienceTests (#7164)

* Initial plan

* Extract MaxRetryAttempts constant to eliminate hardcoded magic number

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Make TestDistributedLockProvider thread-safe with Interlocked operations (#7163)

* Initial plan

* Make TestDistributedLockProvider thread-safe using Interlocked operations

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Update src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update test/component/Elsa.Workflows.ComponentTests/Scenarios/DistributedLockResilience/DistributedLockResilienceTests.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/Elsa.Workflows.Runtime.Distributed/Services/DistributedWorkflowClient.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update src/modules/Elsa.Resilience.Core/Services/DefaultTransientExceptionStrategy.cs

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Include `CancellationToken` in distributed lock handling methods for improved cancellation support.

* Initial plan (#7168)

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>

* Cache detector list in TransientExceptionDetector to avoid repeated allocations (#7167)

* Initial plan

* Cache detector list in field to avoid repeated allocations

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Use IReadOnlyList instead of List for better intent expression

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Fix TestDistributedLockProvider registration to properly decorate IDistributedLockProvider (#7166)

* Initial plan

* Fix TestDistributedLockProvider registration to use Decorate pattern and fix variable reference bug

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Add runtime check for TestDistributedLockProvider registration

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Refactor `DistributedWorkflowClient` to simplify `Lazy<ResiliencePipeline>` initialization.

* Add integration tests for DistributedWorkflowClient lock resilience (#7165)

* Initial plan

* Fix compilation error: use correct parameter name transientExceptionDetector

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Add integration tests for DistributedWorkflowClient lock resilience

- Add SimpleWorkflow for testing distributed lock scenarios
- Add tests exercising RunInstanceAsync with transient lock failures
- Verify retry logic works correctly with actual workflow execution
- Test both acquisition and release failure scenarios
- Decorate IDistributedLockProvider to use TestDistributedLockProvider

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

* Address code review feedback

- Add explanatory comment for TestDistributedLockProvider cast
- Remove unnecessary blank line for consistent formatting

Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
Co-authored-by: Sipke Schoorstra <sipkeschoorstra@outlook.com>

* Simplify transient exception strategy by refactoring message pattern matching logic.

* Enhance distributed lock mock to support per-lock failure configuration and improve resilience tests.

* Refactor `TestDistributedLockProvider` to streamline failure handling logic and improve code clarity.

* Remove unused methods and redundant test case from `DistributedLockResilienceTests`.

* Refactor `DistributedLockResilienceTests` to simplify workflow client creation, consolidate assertion logic, and remove redundant test cases.

* Format `ResilienceStrategyCatalogTests` by removing redundant line breaks in test setup.

* Refactor `TransientExceptionDetectorTests` to simplify test setup, consolidate test cases, and remove redundant logic.

* Handle `InvalidOperationException` in `XunitLogger` to suppress logging errors during inactive tests.

* Update workflows to use .NET 10 and adjust resilience tests project configuration.

* Refactor distributed runtime feature and integrations to improve resilience handling, configure services fluently, and add cancellation safeguards in background services.

* Update `base_version` to `3.7.0` in GitHub workflow configuration.

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com>
Co-authored-by: sfmskywalker <938393+sfmskywalker@users.noreply.github.com>
2025-12-29 19:53:48 +01:00

136 lines
5.8 KiB
C#

using Elsa.Expressions.Contracts;
using Elsa.Expressions.Models;
using Elsa.Resilience.Core.UnitTests.TestHelpers;
using Elsa.Resilience.Models;
using Elsa.Resilience.Options;
using Elsa.Resilience.Serialization;
using NSubstitute;
namespace Elsa.Resilience.Core.UnitTests;
public class ResilienceStrategyConfigEvaluatorTests
{
private readonly IResilienceStrategyCatalog _catalog = Substitute.For<IResilienceStrategyCatalog>();
private readonly IExpressionEvaluator _expressionEvaluator = Substitute.For<IExpressionEvaluator>();
private readonly ResilienceStrategyConfigEvaluator _evaluator;
private readonly ExpressionExecutionContext _context = new(Substitute.For<IServiceProvider>(), null!);
public ResilienceStrategyConfigEvaluatorTests()
{
var options = Microsoft.Extensions.Options.Options.Create(new ResilienceOptions());
var serializer = new ResilienceStrategySerializer(options);
_evaluator = new(_catalog, _expressionEvaluator, serializer);
}
[Fact(DisplayName = "Evaluator should return null when config is null")]
public async Task EvaluateAsync_NullConfig_ReturnsNull()
{
var result = await _evaluator.EvaluateAsync(null, _context);
Assert.Null(result);
}
[Fact(DisplayName = "Evaluator in identifier mode should resolve strategy from catalog")]
public async Task EvaluateAsync_IdentifierMode_WithValidId_ReturnsStrategyFromCatalog()
{
var strategy = TestDataFactory.CreateStrategy("test-strategy", "Test Strategy");
SetupCatalogStrategy("test-strategy", strategy);
var config = CreateConfig(ResilienceStrategyConfigMode.Identifier, "test-strategy");
var result = await _evaluator.EvaluateAsync(config, _context);
Assert.NotNull(result);
Assert.Equal("test-strategy", result.Id);
await _catalog.Received(1).GetAsync("test-strategy", Arg.Any<CancellationToken>());
}
[Theory(DisplayName = "Evaluator in identifier mode should return null for invalid strategy IDs")]
[InlineData("")]
[InlineData(" ")]
[InlineData(null)]
public async Task EvaluateAsync_IdentifierMode_WithInvalidId_ReturnsNull(string? strategyId)
{
var config = CreateConfig(ResilienceStrategyConfigMode.Identifier, strategyId);
var result = await _evaluator.EvaluateAsync(config, _context);
Assert.Null(result);
await _catalog.DidNotReceive().GetAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
[Fact(DisplayName = "Evaluator in expression mode should return null when expression is null")]
public async Task EvaluateAsync_ExpressionMode_WithNullExpression_ReturnsNull()
{
var config = CreateConfig(ResilienceStrategyConfigMode.Expression);
var result = await _evaluator.EvaluateAsync(config, _context);
Assert.Null(result);
await _expressionEvaluator.DidNotReceive().EvaluateAsync<object>(Arg.Any<Expression>(), Arg.Any<ExpressionExecutionContext>(), Arg.Any<ExpressionEvaluatorOptions>());
}
[Fact(DisplayName = "Evaluator in expression mode should resolve string IDs from catalog")]
public async Task EvaluateAsync_ExpressionMode_ReturnsStringId_ResolvesFromCatalog()
{
var expression = new Expression("C#", "\"test-strategy\"");
var strategy = TestDataFactory.CreateStrategy("test-strategy", "Test Strategy");
SetupExpressionResult(expression, "test-strategy");
SetupCatalogStrategy("test-strategy", strategy);
var config = CreateConfig(ResilienceStrategyConfigMode.Expression, expression: expression);
var result = await _evaluator.EvaluateAsync(config, _context);
Assert.NotNull(result);
Assert.Equal("test-strategy", result.Id);
await _catalog.Received(1).GetAsync("test-strategy", Arg.Any<CancellationToken>());
}
[Fact(DisplayName = "Evaluator in expression mode should return strategy objects directly")]
public async Task EvaluateAsync_ExpressionMode_ReturnsStrategyObject_ReturnsStrategyDirectly()
{
var expression = new Expression("C#", "strategy");
var strategy = TestDataFactory.CreateStrategy("direct-strategy", "Direct Strategy");
SetupExpressionResult(expression, strategy);
var config = CreateConfig(ResilienceStrategyConfigMode.Expression, expression: expression);
var result = await _evaluator.EvaluateAsync(config, _context);
Assert.NotNull(result);
Assert.Same(strategy, result);
await _catalog.DidNotReceive().GetAsync(Arg.Any<string>(), Arg.Any<CancellationToken>());
}
[Theory(DisplayName = "Evaluator in expression mode should return null for unexpected result types")]
[InlineData("null", null)]
[InlineData("42", 42)]
public async Task EvaluateAsync_ExpressionMode_ReturnsUnexpectedType_ReturnsNull(string expressionCode, object? expressionResult)
{
var expression = new Expression("C#", expressionCode);
SetupExpressionResult(expression, expressionResult);
var config = CreateConfig(ResilienceStrategyConfigMode.Expression, expression: expression);
var result = await _evaluator.EvaluateAsync(config, _context);
Assert.Null(result);
}
private ResilienceStrategyConfig CreateConfig(ResilienceStrategyConfigMode mode, string? strategyId = null, Expression? expression = null)
{
return new()
{
Mode = mode,
StrategyId = strategyId,
Expression = expression
};
}
private void SetupCatalogStrategy(string id, IResilienceStrategy strategy)
{
_catalog.GetAsync(id, Arg.Any<CancellationToken>()).Returns(strategy);
}
private void SetupExpressionResult(Expression expression, object? result)
{
_expressionEvaluator.EvaluateAsync<object>(expression, _context, Arg.Any<ExpressionEvaluatorOptions>()).Returns(result);
}
}