11using Elsa . Common . DistributedHosting ;
2+ using Elsa . Common . Models ;
23using Elsa . Resilience ;
34using Elsa . Workflows . ComponentTests . Abstractions ;
45using Elsa . Workflows . ComponentTests . Fixtures ;
56using Elsa . Workflows . ComponentTests . Scenarios . DistributedLockResilience . Mocks ;
7+ using Elsa . Workflows . ComponentTests . Scenarios . DistributedLockResilience . Workflows ;
8+ using Elsa . Workflows . Management ;
9+ using Elsa . Workflows . Models ;
10+ using Elsa . Workflows . Runtime ;
11+ using Elsa . Workflows . Runtime . Messages ;
612using Medallion . Threading ;
713using Microsoft . Extensions . DependencyInjection ;
814using Microsoft . Extensions . Logging ;
@@ -15,7 +21,9 @@ public class DistributedLockResilienceTests(App app) : AppComponentTest(app)
1521{
1622 private const int MaxRetryAttempts = 3 ;
1723
18- private TestDistributedLockProvider MockProvider => Scope . ServiceProvider . GetRequiredService < TestDistributedLockProvider > ( ) ;
24+ // The IDistributedLockProvider is decorated with TestDistributedLockProvider in WorkflowServer.ConfigureTestServices
25+ // This cast is safe because the decorator pattern ensures TestDistributedLockProvider wraps the actual provider
26+ private TestDistributedLockProvider MockProvider => ( TestDistributedLockProvider ) Scope . ServiceProvider . GetRequiredService < IDistributedLockProvider > ( ) ;
1927 private ITransientExceptionDetector TransientExceptionDetector => Scope . ServiceProvider . GetRequiredService < ITransientExceptionDetector > ( ) ;
2028 private ILogger < DistributedLockResilienceTests > Logger => Scope . ServiceProvider . GetRequiredService < ILogger < DistributedLockResilienceTests > > ( ) ;
2129 private DistributedLockingOptions LockOptions => Scope . ServiceProvider . GetRequiredService < IOptions < DistributedLockingOptions > > ( ) . Value ;
@@ -59,6 +67,107 @@ public async Task AcquireLockWithRetry_TransientFailureOnRelease_ShouldLogButNot
5967 Assert . Equal ( 1 , MockProvider . ReleaseAttemptCount ) ;
6068 }
6169
70+ [ Theory ]
71+ [ InlineData ( 1 , 2 , false ) ] // Single failure, succeeds on retry
72+ [ InlineData ( 2 , 3 , false ) ] // Two failures, succeeds on third attempt
73+ [ InlineData ( 4 , 4 , true ) ] // Four failures, exhausts retries (MaxRetryAttempts = 3)
74+ public async Task RunInstanceAsync_TransientLockFailures_RetriesCorrectly ( int failureCount , int expectedAttemptCount , bool shouldThrow )
75+ {
76+ // Arrange
77+ var workflowRuntime = Scope . ServiceProvider . GetRequiredService < IWorkflowRuntime > ( ) ;
78+ var workflowClient = await workflowRuntime . CreateClientAsync ( ) ;
79+
80+ var createRequest = new CreateAndRunWorkflowInstanceRequest
81+ {
82+ WorkflowDefinitionHandle = WorkflowDefinitionHandle . ByDefinitionId ( SimpleWorkflow . DefinitionId , VersionOptions . Latest )
83+ } ;
84+
85+ // Reset and configure failures right before the operation to minimize interference from background workers
86+ MockProvider . Reset ( ) ;
87+ MockProvider . FailAcquisitionTimes ( failureCount ) ;
88+
89+ // Act & Assert
90+ if ( shouldThrow )
91+ {
92+ // Should throw after exhausting retries
93+ await Assert . ThrowsAsync < TimeoutException > ( async ( ) =>
94+ await workflowClient . CreateAndRunInstanceAsync ( createRequest ) ) ;
95+ }
96+ else
97+ {
98+ // Should succeed after retries
99+ var response = await workflowClient . CreateAndRunInstanceAsync ( createRequest ) ;
100+ Assert . NotNull ( response ) ;
101+ Assert . NotNull ( response . WorkflowInstanceId ) ;
102+ }
103+
104+ // Verify retries occurred (allow for some background noise, but verify minimum attempts)
105+ Assert . True ( MockProvider . AcquisitionAttemptCount >= expectedAttemptCount ,
106+ $ "Expected at least { expectedAttemptCount } acquisition attempts, but got { MockProvider . AcquisitionAttemptCount } ") ;
107+ }
108+
109+ [ Fact ]
110+ public async Task RunInstanceAsync_TransientLockFailureOnSecondRun_RetriesCorrectly ( )
111+ {
112+ // Arrange
113+ var workflowRuntime = Scope . ServiceProvider . GetRequiredService < IWorkflowRuntime > ( ) ;
114+
115+ // First run - reset and don't inject failures
116+ MockProvider . Reset ( ) ;
117+ var workflowClient = await workflowRuntime . CreateClientAsync ( ) ;
118+
119+ var createRequest = new CreateAndRunWorkflowInstanceRequest
120+ {
121+ WorkflowDefinitionHandle = WorkflowDefinitionHandle . ByDefinitionId ( SimpleWorkflow . DefinitionId , VersionOptions . Latest )
122+ } ;
123+
124+ var firstResponse = await workflowClient . CreateAndRunInstanceAsync ( createRequest ) ;
125+ Assert . NotNull ( firstResponse . WorkflowInstanceId ) ;
126+
127+ // Verify first run succeeded (at least 1 attempt, possibly more from background workers)
128+ Assert . True ( MockProvider . AcquisitionAttemptCount >= 1 ) ;
129+
130+ // Setup second workflow with failure - reset right before to minimize background noise
131+ MockProvider . Reset ( ) ;
132+ MockProvider . FailAcquisitionTimes ( 2 ) ; // Fail twice, succeed on third
133+
134+ var secondClient = await workflowRuntime . CreateClientAsync ( ) ;
135+ var secondResponse = await secondClient . CreateAndRunInstanceAsync ( createRequest ) ;
136+
137+ // Act & Assert - Second run should succeed after retries
138+ Assert . NotNull ( secondResponse ) ;
139+ Assert . NotNull ( secondResponse . WorkflowInstanceId ) ;
140+ Assert . NotEqual ( firstResponse . WorkflowInstanceId , secondResponse . WorkflowInstanceId ) ;
141+
142+ // Verify retries occurred (at least 3 attempts: 2 failures + 1 success, possibly more from background)
143+ Assert . True ( MockProvider . AcquisitionAttemptCount >= 3 ,
144+ $ "Expected at least 3 acquisition attempts, but got { MockProvider . AcquisitionAttemptCount } ") ;
145+ }
146+
147+ [ Fact ]
148+ public async Task RunInstanceAsync_TransientReleaseFailure_ShouldLogButNotThrow ( )
149+ {
150+ // Arrange
151+ MockProvider . Reset ( ) ;
152+ MockProvider . FailReleaseOnce ( ) ;
153+
154+ var workflowRuntime = Scope . ServiceProvider . GetRequiredService < IWorkflowRuntime > ( ) ;
155+ var workflowClient = await workflowRuntime . CreateClientAsync ( ) ;
156+
157+ var createRequest = new CreateAndRunWorkflowInstanceRequest
158+ {
159+ WorkflowDefinitionHandle = WorkflowDefinitionHandle . ByDefinitionId ( SimpleWorkflow . DefinitionId , VersionOptions . Latest )
160+ } ;
161+
162+ // Act - Release failure should be caught and logged, not thrown
163+ var response = await workflowClient . CreateAndRunInstanceAsync ( createRequest ) ;
164+
165+ // Assert
166+ Assert . NotNull ( response ) ;
167+ Assert . NotNull ( response . WorkflowInstanceId ) ;
168+ Assert . Equal ( 1 , MockProvider . ReleaseAttemptCount ) ;
169+ }
170+
62171 private async Task < IDistributedSynchronizationHandle ? > AcquireLockWithRetryAsync ( string lockName ) =>
63172 await RetryPipeline . ExecuteAsync ( async ct =>
64173 await MockProvider . AcquireLockAsync ( lockName , LockOptions . LockAcquisitionTimeout , ct ) ,
0 commit comments