* 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>
136 lines
5.8 KiB
C#
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);
|
|
}
|
|
}
|