Skip to content

Commit 76a3342

Browse files
committed
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.
1 parent c91dc0b commit 76a3342

2 files changed

Lines changed: 56 additions & 116 deletions

File tree

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

Lines changed: 44 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -13,131 +13,82 @@ namespace Elsa.Workflows.ComponentTests.Scenarios.DistributedLockResilience;
1313

1414
public class DistributedLockResilienceTests(App app) : AppComponentTest(app)
1515
{
16-
[Fact]
17-
public async Task AcquireLockWithRetry_TransientFailureOnAcquisition_ShouldRetryAndSucceed()
16+
private TestDistributedLockProvider MockProvider => Scope.ServiceProvider.GetRequiredService<TestDistributedLockProvider>();
17+
private IDistributedLockProvider LockProvider => Scope.ServiceProvider.GetRequiredService<IDistributedLockProvider>();
18+
private ITransientExceptionDetectionService TransientDetectionService => Scope.ServiceProvider.GetRequiredService<ITransientExceptionDetectionService>();
19+
private ILogger<DistributedLockResilienceTests> Logger => Scope.ServiceProvider.GetRequiredService<ILogger<DistributedLockResilienceTests>>();
20+
private DistributedLockingOptions LockOptions => Scope.ServiceProvider.GetRequiredService<IOptions<DistributedLockingOptions>>().Value;
21+
private ResiliencePipeline RetryPipeline => CreateRetryPipeline(TransientDetectionService, Logger);
22+
23+
[Theory]
24+
[InlineData(1, 2, false)] // Single failure, succeeds on retry
25+
[InlineData(2, 3, false)] // Two failures, succeeds on third attempt
26+
[InlineData(4, 4, true)] // Four failures, exhausts retries (MaxRetryAttempts = 3)
27+
public async Task AcquireLockWithRetry_AcquisitionFailures_BehavesAsExpected(int failureCount, int expectedAttemptCount, bool shouldThrow)
1828
{
1929
// Arrange
20-
var mockProvider = Scope.ServiceProvider.GetRequiredService<TestDistributedLockProvider>();
21-
mockProvider.Reset();
22-
mockProvider.FailAcquisitionOnce();
23-
24-
var lockProvider = Scope.ServiceProvider.GetRequiredService<IDistributedLockProvider>();
25-
var transientDetectionService = Scope.ServiceProvider.GetRequiredService<ITransientExceptionDetectionService>();
26-
var logger = Scope.ServiceProvider.GetRequiredService<ILogger<DistributedLockResilienceTests>>();
27-
var lockOptions = Scope.ServiceProvider.GetRequiredService<IOptions<DistributedLockingOptions>>();
28-
29-
var retryPipeline = CreateRetryPipeline(transientDetectionService, logger);
30-
31-
// Act - should succeed after retry
32-
var handle = await retryPipeline.ExecuteAsync(async ct =>
33-
await lockProvider.AcquireLockAsync("test-lock-1", lockOptions.Value.LockAcquisitionTimeout, ct),
34-
CancellationToken.None);
30+
MockProvider.Reset();
31+
MockProvider.FailAcquisitionTimes(failureCount);
3532

36-
// Assert
37-
Assert.NotNull(handle);
38-
Assert.Equal(2, mockProvider.AcquisitionAttemptCount); // First attempt fails, second succeeds
39-
40-
await handle.DisposeAsync();
41-
}
42-
43-
[Fact]
44-
public async Task AcquireLockWithRetry_TransientFailureOnRelease_ShouldLogButNotThrow()
45-
{
46-
// Arrange
47-
var mockProvider = Scope.ServiceProvider.GetRequiredService<TestDistributedLockProvider>();
48-
mockProvider.Reset();
49-
mockProvider.FailReleaseOnce();
50-
51-
var lockProvider = Scope.ServiceProvider.GetRequiredService<IDistributedLockProvider>();
52-
var lockOptions = Scope.ServiceProvider.GetRequiredService<IOptions<DistributedLockingOptions>>();
53-
var logger = Scope.ServiceProvider.GetRequiredService<ILogger<DistributedLockResilienceTests>>();
54-
55-
var handle = await lockProvider.AcquireLockAsync("test-lock-2", lockOptions.Value.LockAcquisitionTimeout);
56-
57-
// Act & Assert - should not throw despite release failure (mimics production try-catch behavior)
58-
try
33+
// Act & Assert
34+
if (shouldThrow)
5935
{
60-
await handle.DisposeAsync();
36+
await Assert.ThrowsAsync<TimeoutException>(async () => await AcquireLockWithRetryAsync($"test-lock-{failureCount}"));
6137
}
62-
catch (Exception ex)
38+
else
6339
{
64-
logger.LogWarning(ex, "Failed to release distributed lock (expected test behavior)");
40+
await using var handle = await AcquireLockWithRetryAsync($"test-lock-{failureCount}");
41+
Assert.NotNull(handle);
6542
}
6643

67-
Assert.Equal(1, mockProvider.ReleaseAttemptCount);
44+
Assert.Equal(expectedAttemptCount, MockProvider.AcquisitionAttemptCount);
6845
}
6946

7047
[Fact]
71-
public async Task AcquireLockWithRetry_MultipleTransientFailures_ShouldRetryAndSucceed()
48+
public async Task AcquireLockWithRetry_TransientFailureOnRelease_ShouldLogButNotThrow()
7249
{
7350
// Arrange
74-
var mockProvider = Scope.ServiceProvider.GetRequiredService<TestDistributedLockProvider>();
75-
mockProvider.Reset();
76-
mockProvider.FailAcquisitionTimes(2); // Fail twice, succeed on third attempt
51+
MockProvider.Reset();
52+
MockProvider.FailReleaseOnce();
7753

78-
var lockProvider = Scope.ServiceProvider.GetRequiredService<IDistributedLockProvider>();
79-
var transientDetectionService = Scope.ServiceProvider.GetRequiredService<ITransientExceptionDetectionService>();
80-
var logger = Scope.ServiceProvider.GetRequiredService<ILogger<DistributedLockResilienceTests>>();
81-
var lockOptions = Scope.ServiceProvider.GetRequiredService<IOptions<DistributedLockingOptions>>();
54+
// Act & Assert - mimics production try-catch behavior where release exceptions are logged but not thrown
55+
await using var handle = await LockProvider.AcquireLockAsync("test-lock-release", LockOptions.LockAcquisitionTimeout);
56+
await SafeDisposeAsync(handle);
8257

83-
var retryPipeline = CreateRetryPipeline(transientDetectionService, logger);
58+
Assert.Equal(1, MockProvider.ReleaseAttemptCount);
59+
}
8460

85-
// Act - should succeed after multiple retries
86-
var handle = await retryPipeline.ExecuteAsync(async ct =>
87-
await lockProvider.AcquireLockAsync("test-lock-3", lockOptions.Value.LockAcquisitionTimeout, ct),
61+
private async Task<IDistributedSynchronizationHandle?> AcquireLockWithRetryAsync(string lockName) =>
62+
await RetryPipeline.ExecuteAsync(async ct =>
63+
await LockProvider.AcquireLockAsync(lockName, LockOptions.LockAcquisitionTimeout, ct),
8864
CancellationToken.None);
8965

90-
// Assert
91-
Assert.NotNull(handle);
92-
Assert.Equal(3, mockProvider.AcquisitionAttemptCount); // Two attempts fail, third succeeds
93-
94-
await handle.DisposeAsync();
95-
}
96-
97-
[Fact]
98-
public async Task AcquireLockWithRetry_ExhaustedRetries_ShouldThrowException()
66+
private async Task SafeDisposeAsync(IDistributedSynchronizationHandle handle)
9967
{
100-
// Arrange
101-
var mockProvider = Scope.ServiceProvider.GetRequiredService<TestDistributedLockProvider>();
102-
mockProvider.Reset();
103-
mockProvider.FailAcquisitionTimes(4); // Fail 4 times (exceeds max retry attempts of 3)
104-
105-
var lockProvider = Scope.ServiceProvider.GetRequiredService<IDistributedLockProvider>();
106-
var transientDetectionService = Scope.ServiceProvider.GetRequiredService<ITransientExceptionDetectionService>();
107-
var logger = Scope.ServiceProvider.GetRequiredService<ILogger<DistributedLockResilienceTests>>();
108-
var lockOptions = Scope.ServiceProvider.GetRequiredService<IOptions<DistributedLockingOptions>>();
109-
110-
var retryPipeline = CreateRetryPipeline(transientDetectionService, logger);
111-
112-
// Act & Assert - should throw after exhausting retries
113-
await Assert.ThrowsAsync<TimeoutException>(async () =>
68+
try
11469
{
115-
await retryPipeline.ExecuteAsync(async ct =>
116-
await lockProvider.AcquireLockAsync("test-lock-4", lockOptions.Value.LockAcquisitionTimeout, ct),
117-
CancellationToken.None);
118-
});
119-
120-
Assert.Equal(4, mockProvider.AcquisitionAttemptCount);
70+
await handle.DisposeAsync();
71+
}
72+
catch (Exception ex)
73+
{
74+
Logger.LogWarning(ex, "Failed to release distributed lock (expected test behavior)");
75+
}
12176
}
12277

123-
private static ResiliencePipeline CreateRetryPipeline(
124-
ITransientExceptionDetectionService transientExceptionDetectionService,
125-
ILogger logger)
126-
{
127-
return new ResiliencePipelineBuilder()
78+
private static ResiliencePipeline CreateRetryPipeline(ITransientExceptionDetectionService transientExceptionDetectionService, ILogger logger) =>
79+
new ResiliencePipelineBuilder()
12880
.AddRetry(new()
12981
{
13082
MaxRetryAttempts = 3,
131-
Delay = TimeSpan.FromMilliseconds(10), // Shorter delay for tests
83+
Delay = TimeSpan.FromMilliseconds(10),
13284
BackoffType = DelayBackoffType.Constant,
13385
UseJitter = false,
134-
ShouldHandle = new PredicateBuilder().Handle<Exception>(ex => transientExceptionDetectionService.IsTransient(ex)),
86+
ShouldHandle = new PredicateBuilder().Handle<Exception>(transientExceptionDetectionService.IsTransient),
13587
OnRetry = args =>
13688
{
13789
logger.LogWarning(args.Outcome.Exception, "Transient error acquiring lock. Attempt {AttemptNumber} of {MaxAttempts}.", args.AttemptNumber + 1, 3);
13890
return ValueTask.CompletedTask;
13991
}
14092
})
14193
.Build();
142-
}
14394
}

test/component/Elsa.Workflows.ComponentTests/Scenarios/DistributedLockResilience/Mocks/TestDistributedLockProvider.cs

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,19 @@ namespace Elsa.Workflows.ComponentTests.Scenarios.DistributedLockResilience.Mock
55
/// <summary>
66
/// Test implementation of IDistributedLockProvider that allows simulating transient failures.
77
/// </summary>
8-
public class TestDistributedLockProvider : IDistributedLockProvider
8+
public class TestDistributedLockProvider(IDistributedLockProvider innerProvider) : IDistributedLockProvider
99
{
10-
private readonly IDistributedLockProvider _innerProvider;
1110
private int _acquisitionFailuresRemaining;
1211
private int _releaseFailuresRemaining;
1312
private int _acquisitionAttemptCount;
1413
private int _releaseAttemptCount;
1514

16-
public TestDistributedLockProvider(IDistributedLockProvider innerProvider)
17-
{
18-
_innerProvider = innerProvider;
19-
}
20-
2115
public int AcquisitionAttemptCount => _acquisitionAttemptCount;
2216
public int ReleaseAttemptCount => _releaseAttemptCount;
2317

2418
public void FailAcquisitionOnce() => _acquisitionFailuresRemaining = 1;
2519
public void FailAcquisitionTimes(int count) => _acquisitionFailuresRemaining = count;
2620
public void FailReleaseOnce() => _releaseFailuresRemaining = 1;
27-
public void FailReleaseTimes(int count) => _releaseFailuresRemaining = count;
2821

2922
public void Reset()
3023
{
@@ -36,30 +29,26 @@ public void Reset()
3629

3730
public IDistributedLock CreateLock(string name)
3831
{
39-
return new TestDistributedLock(
40-
_innerProvider.CreateLock(name),
41-
this);
32+
return new TestDistributedLock(innerProvider.CreateLock(name), this);
4233
}
4334

4435
internal bool ShouldFailAcquisition()
4536
{
4637
_acquisitionAttemptCount++;
47-
if (_acquisitionFailuresRemaining > 0)
48-
{
49-
_acquisitionFailuresRemaining--;
50-
return true;
51-
}
52-
return false;
38+
if (_acquisitionFailuresRemaining <= 0)
39+
return false;
40+
41+
_acquisitionFailuresRemaining--;
42+
return true;
5343
}
5444

5545
internal bool ShouldFailRelease()
5646
{
5747
_releaseAttemptCount++;
58-
if (_releaseFailuresRemaining > 0)
59-
{
60-
_releaseFailuresRemaining--;
61-
return true;
62-
}
63-
return false;
48+
if (_releaseFailuresRemaining <= 0)
49+
return false;
50+
51+
_releaseFailuresRemaining--;
52+
return true;
6453
}
6554
}

0 commit comments

Comments
 (0)