Repository navigation
feat: enhance distributed lock service with Redis integration and con… #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,25 +1,14 @@ | ||
| using DistributedLockManager.Interfaces; | ||
| using RedLockNet.SERedis; | ||
| using RedLockNet.SERedis.Configuration; | ||
| using StackExchange.Redis; | ||
| using RedLockNet; | ||
|
|
||
| namespace DistributedLockManager.Services; | ||
|
|
||
| public class DistributedLockService : IDistributedLockService | ||
| public class DistributedLockService(IDistributedLockFactory distributedLockFactory) : IDistributedLockService | ||
| { | ||
| private readonly RedLockFactory _redLockFactory; | ||
| public DistributedLockService(IConnectionMultiplexer connectionMultiplexer) | ||
| { | ||
| var endPoints = connectionMultiplexer.GetEndPoints(); | ||
| List<RedLockEndPoint> redLockEndPoints = [.. endPoints]; | ||
|
|
||
| _redLockFactory = RedLockFactory.Create(redLockEndPoints); | ||
| } | ||
|
|
||
| public async Task RunWithLockAsync(Func<Task> func, string key, CancellationToken cancellationToken, int expiryInSecond = 30, int waitInSecond = 10, int retryInSecond = 1) | ||
| { | ||
| // blocks until acquired or 'wait' timeout | ||
| await using var redLock = await _redLockFactory.CreateLockAsync(key, TimeSpan.FromSeconds(expiryInSecond), TimeSpan.FromSeconds(waitInSecond), | ||
| await using var redLock = await distributedLockFactory.CreateLockAsync(key, TimeSpan.FromSeconds(expiryInSecond), TimeSpan.FromSeconds(waitInSecond), | ||
| TimeSpan.FromSeconds(retryInSecond), cancellationToken); | ||
|
|
||
| if (redLock.IsAcquired) | ||
|
|
@@ -29,29 +18,20 @@ public async Task RunWithLockAsync(Func<Task> func, string key, CancellationToke | |
| public async Task RunWithLockAsync(Task task, string key, CancellationToken cancellationToken, int expiryInSecond = 30, int waitInSecond = 10, int retryInSecond = 1) | ||
| { | ||
| // blocks until acquired or 'wait' timeout | ||
| await using var redLock = await _redLockFactory.CreateLockAsync(key, TimeSpan.FromSeconds(expiryInSecond), TimeSpan.FromSeconds(waitInSecond), | ||
| await using var redLock = await distributedLockFactory.CreateLockAsync(key, TimeSpan.FromSeconds(expiryInSecond), TimeSpan.FromSeconds(waitInSecond), | ||
| TimeSpan.FromSeconds(retryInSecond), cancellationToken); | ||
|
|
||
| if (redLock.IsAcquired) | ||
| await task; | ||
| } | ||
|
Comment on lines
18
to
26
|
||
| } | ||
|
|
||
| public class DistributedLockService<TResult> : IDistributedLockService<TResult> | ||
| public class DistributedLockService<TResult>(IDistributedLockFactory distributedLockFactory) : IDistributedLockService<TResult> | ||
| { | ||
| private readonly RedLockFactory _redLockFactory; | ||
| public DistributedLockService(IConnectionMultiplexer connectionMultiplexer) | ||
| { | ||
| var endPoints = connectionMultiplexer.GetEndPoints(); | ||
| List<RedLockEndPoint> redLockEndPoints = [.. endPoints]; | ||
|
|
||
| _redLockFactory = RedLockFactory.Create(redLockEndPoints); | ||
| } | ||
|
|
||
| public async Task<TResult> RunWithLockAsync(Func<Task<TResult>> func, string key, CancellationToken cancellationToken, int expiryInSecond = 30, int waitInSecond = 10, int retryInSecond = 1) | ||
| { | ||
| // blocks until acquired or 'wait' timeout | ||
| await using var redLock = await _redLockFactory.CreateLockAsync(key, | ||
| await using var redLock = await distributedLockFactory.CreateLockAsync(key, | ||
| expiryTime: TimeSpan.FromSeconds(expiryInSecond), | ||
| waitTime: TimeSpan.FromSeconds(waitInSecond), | ||
| retryTime: TimeSpan.FromSeconds(retryInSecond), cancellationToken); | ||
|
|
@@ -66,7 +46,7 @@ public async Task<TResult> RunWithLockAsync(Func<Task<TResult>> func, string key | |
| public async Task<TResult> RunWithLockAsync(Task<TResult> task, string key, CancellationToken cancellationToken, int expiryInSecond = 30, int waitInSecond = 10, int retryInSecond = 1) | ||
| { | ||
| // blocks until acquired or 'wait' timeout | ||
| await using var redLock = await _redLockFactory.CreateLockAsync(key, TimeSpan.FromSeconds(expiryInSecond), TimeSpan.FromSeconds(waitInSecond), | ||
| await using var redLock = await distributedLockFactory.CreateLockAsync(key, TimeSpan.FromSeconds(expiryInSecond), TimeSpan.FromSeconds(waitInSecond), | ||
| TimeSpan.FromSeconds(retryInSecond), cancellationToken); | ||
|
|
||
| if (redLock.IsAcquired) | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -17,33 +17,39 @@ public class DistributedLockServiceIntegrationTests | |||||||
| { | ||||||||
| private IServiceScope? scope; | ||||||||
| private IContainer? _redisContainer; | ||||||||
| private IConnectionMultiplexer? _connectionMultiplexer; | ||||||||
| private const string RedisImage = "redis:latest"; | ||||||||
| private const int RedisPort = 6379; | ||||||||
|
|
||||||||
| [OneTimeSetUp] | ||||||||
| public async Task OneTimeSetUp() | ||||||||
| { | ||||||||
| _redisContainer = new ContainerBuilder() | ||||||||
| .WithImage(RedisImage) | ||||||||
| _redisContainer = new ContainerBuilder(RedisImage) | ||||||||
| .WithCleanUp(true) | ||||||||
| .WithName($"dtm-redis-{Guid.NewGuid():N}") | ||||||||
| .WithPortBinding(RedisPort, true) | ||||||||
| .WithWaitStrategy(Wait.ForUnixContainer().UntilExternalTcpPortIsAvailable(RedisPort)) | ||||||||
| .Build(); | ||||||||
|
|
||||||||
| if (_redisContainer == null) Assert.Fail("Redis container not started"); | ||||||||
|
|
||||||||
| await _redisContainer!.StartAsync(); | ||||||||
|
|
||||||||
| // Wait a bit for Redis to be fully ready | ||||||||
| await Task.Delay(1000); | ||||||||
|
|
||||||||
|
Comment on lines
+38
to
+40
|
||||||||
| // Wait a bit for Redis to be fully ready | |
| await Task.Delay(1000); | |
Copilot
AI
Jan 21, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The service provider should be disposed before disposing the ConnectionMultiplexer to ensure proper cleanup order. The scope's service provider may still hold references to services that depend on the ConnectionMultiplexer. Consider storing the ServiceProvider and disposing it explicitly before disposing the ConnectionMultiplexer to prevent potential resource cleanup issues.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The factory registration requires ILoggerFactory but this dependency isn't documented. If ILoggerFactory is not registered in the DI container when AddDistributedLockManager is called, GetRequiredService will throw an exception. Consider either documenting this prerequisite in XML comments or making the logger optional by using GetService and providing a null logger to the factory if logging isn't configured.