Skip to content

Commit 9ceaad1

Browse files
authored
Added SqlServer for distributed lock (#4)
* Added sql statments * Added test for Distributed lock * Refactored test
1 parent 1efa70d commit 9ceaad1

8 files changed

+210
-8
lines changed
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,20 @@
11
using Locks.Configurators;
2+
using Locks.Internals.Distributed.Storage;
3+
using Locks.SqlServer.Internals;
4+
using Microsoft.Extensions.DependencyInjection;
25

3-
namespace Locks.SqlServer.Extensions
6+
namespace Locks
47
{
58
public static class ExntesionUseSqlServer
69
{
710
public static void UseSqlServer(
8-
this IDistributedLockStorageConfigurator configurator)
11+
this IDistributedLockStorageConfigurator configurator,
12+
string connectionString)
913
{
1014
var services = configurator.Services;
15+
16+
services.AddSingleton(new SqlServerConnectionFactory(connectionString));
17+
services.AddSingleton<IDistributedLockRepository, SqlServerDistributedLockRepository>();
1118
}
1219
}
1320
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
using Microsoft.Data.SqlClient;
2+
using System.Data.Common;
3+
4+
namespace Locks.SqlServer.Internals
5+
{
6+
internal sealed class SqlServerConnectionFactory
7+
{
8+
private readonly string _connectionString;
9+
10+
internal SqlServerConnectionFactory(string connectionString) => _connectionString = connectionString;
11+
12+
public SqlConnection Create() => new SqlConnection(_connectionString);
13+
}
14+
}
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,85 @@
11
using System;
22
using System.Threading.Tasks;
33
using Locks.Internals.Distributed.Storage;
4+
using Microsoft.Data.SqlClient;
45

56
namespace Locks.SqlServer.Internals
67
{
78
internal sealed class SqlServerDistributedLockRepository : IDistributedLockRepository
89
{
9-
public Task AddFirstLock(DistributedLockStorageModel @lock)
10+
private readonly SqlServerConnectionFactory _connectionFactory;
11+
12+
public SqlServerDistributedLockRepository(SqlServerConnectionFactory connectionFactory)
1013
{
11-
throw new NotImplementedException();
14+
_connectionFactory = connectionFactory;
1215
}
1316

14-
public Task<bool> Release(DistributedLockStorageModel @lock, DateTime nowUtc)
17+
public async Task AddFirstLock(DistributedLockStorageModel @lock)
1518
{
16-
throw new NotImplementedException();
19+
const string queryString = "INSERT INTO [dbo].[DistributedLocks] VALUES (@Key, @ExpirationUtc)";
20+
21+
using (var connection = _connectionFactory.Create())
22+
{
23+
SqlCommand command = new SqlCommand(queryString, connection);
24+
25+
command.Parameters.Add(new SqlParameter("ExpirationUtc", @lock.ExpirationUtc));
26+
command.Parameters.Add(new SqlParameter("Key", @lock.Key));
27+
28+
connection.Open();
29+
30+
await command
31+
.ExecuteNonQueryAsync()
32+
.ConfigureAwait(false);
33+
}
1734
}
1835

19-
public Task<bool> TryAcquire(string key, DateTime nowUtc, DateTime newExpirationUtc)
36+
public async Task<bool> Release(DistributedLockStorageModel @lock, DateTime nowUtc)
2037
{
21-
throw new NotImplementedException();
38+
const string queryString = "UPDATE [dbo].[DistributedLocks] SET ExpirationUtc = @NowUtc WHERE [Key] = @Key AND ExpirationUtc = @ExpirationUtc";
39+
40+
int updatedRows = 0;
41+
42+
using (var connection = _connectionFactory.Create())
43+
{
44+
SqlCommand command = new SqlCommand(queryString, connection);
45+
46+
command.Parameters.Add(new SqlParameter("NowUtc", nowUtc));
47+
command.Parameters.Add(new SqlParameter("ExpirationUtc", @lock.ExpirationUtc));
48+
command.Parameters.Add(new SqlParameter("Key", @lock.Key));
49+
50+
51+
connection.Open();
52+
53+
updatedRows = await command
54+
.ExecuteNonQueryAsync()
55+
.ConfigureAwait(false);
56+
}
57+
58+
return updatedRows > 0;
59+
}
60+
61+
public async Task<bool> TryAcquire(string key, DateTime nowUtc, DateTime newExpirationUtc)
62+
{
63+
const string queryString = "UPDATE [dbo].[DistributedLocks] SET ExpirationUtc = @ExpirationUtc WHERE [Key] = @Key AND ExpirationUtc <= @NowUtc";
64+
65+
int updatedRows = 0;
66+
67+
using (var connection = _connectionFactory.Create())
68+
{
69+
SqlCommand command = new SqlCommand(queryString, connection);
70+
71+
command.Parameters.Add(new SqlParameter("ExpirationUtc", newExpirationUtc));
72+
command.Parameters.Add(new SqlParameter("Key", key));
73+
command.Parameters.Add(new SqlParameter("NowUtc", nowUtc));
74+
75+
connection.Open();
76+
77+
updatedRows = await command
78+
.ExecuteNonQueryAsync()
79+
.ConfigureAwait(false);
80+
}
81+
82+
return updatedRows > 0;
2283
}
2384
}
2485
}

source/Locks.SqlServer/Locks.SqlServer.csproj

+4
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@
44
<TargetFramework>netstandard2.0</TargetFramework>
55
</PropertyGroup>
66

7+
<ItemGroup>
8+
<PackageReference Include="Microsoft.Data.SqlClient" Version="5.2.1" />
9+
</ItemGroup>
10+
711
<ItemGroup>
812
<ProjectReference Include="..\Locks\Locks.csproj" />
913
</ItemGroup>
+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
using Locks.Tests.Seed;
2+
using Microsoft.Data.SqlClient;
3+
using Microsoft.Extensions.DependencyInjection;
4+
using Testcontainers.MsSql;
5+
using Xunit;
6+
7+
namespace Locks.Tests
8+
{
9+
public sealed class DistributedLockTests : IAsyncLifetime
10+
{
11+
private IServiceProvider _serviceProvider;
12+
13+
public async Task InitializeAsync()
14+
{
15+
var msSqlContainer = new MsSqlBuilder().Build();
16+
17+
await msSqlContainer.StartAsync();
18+
19+
var connectionString = msSqlContainer.GetConnectionString();
20+
21+
IServiceCollection services = new ServiceCollection();
22+
23+
services.AddDistributedLock(x => x.UseSqlServer(connectionString));
24+
25+
// KeyedInMemoryLock is disabled because we want multiple tasks to query the database
26+
services.AddSingleton<IMemoryLock, DisabledMemoryLock>();
27+
28+
_serviceProvider = services.BuildServiceProvider();
29+
30+
const string queryStringCreate = "CREATE TABLE [dbo].[DistributedLocks] ([Key] VARCHAR(60) NOT NULL PRIMARY KEY, [ExpirationUtc] DATETIME NOT NULL)";
31+
32+
using (var connection = new SqlConnection(connectionString))
33+
{
34+
SqlCommand command = new SqlCommand(queryStringCreate, connection);
35+
36+
connection.Open();
37+
38+
await command
39+
.ExecuteNonQueryAsync()
40+
.ConfigureAwait(false);
41+
}
42+
}
43+
44+
[Fact(DisplayName = "Distributed lock works correct in distributed enviroment")]
45+
public async Task Test()
46+
{
47+
var distributedLock = _serviceProvider.GetRequiredService<IDistributedLock>();
48+
49+
const string lockKeyA = "A";
50+
const string lockKeyB = "B";
51+
const string lockKeyC = "C";
52+
53+
for (var i = 0; i < 2; i++)
54+
{
55+
var a1 = distributedLock.AcquireAsync(lockKeyA);
56+
var b1 = distributedLock.AcquireAsync(lockKeyB);
57+
var c1 = distributedLock.AcquireAsync(lockKeyC);
58+
59+
await Task.WhenAll(a1, b1, c1);
60+
61+
var a2 = distributedLock.AcquireAsync(lockKeyA);
62+
var b2 = distributedLock.AcquireAsync(lockKeyB);
63+
var c2 = distributedLock.AcquireAsync(lockKeyC);
64+
65+
Assert.True(a1.IsCompleted, Message(nameof(a1)));
66+
Assert.True(b1.IsCompleted, Message(nameof(b1)));
67+
Assert.True(c1.IsCompleted, Message(nameof(c1)));
68+
69+
// Second tasks should wait when locks will be released
70+
Assert.False(a2.IsCompleted, Message(nameof(a2), nameof(a1)));
71+
Assert.False(b2.IsCompleted, Message(nameof(b2), nameof(b1)));
72+
Assert.False(c2.IsCompleted, Message(nameof(c2), nameof(c1)));
73+
74+
// When first tasks release locks, second tasks can continue
75+
await a1.Result.DisposeAsync();
76+
await b1.Result.DisposeAsync();
77+
await c1.Result.DisposeAsync();
78+
79+
await Task.WhenAll(a2, b2, c2);
80+
81+
Assert.True(a2.IsCompleted, Message(nameof(a2)));
82+
Assert.True(b2.IsCompleted, Message(nameof(b2)));
83+
Assert.True(c2.IsCompleted, Message(nameof(c2)));
84+
85+
await a2.Result.DisposeAsync();
86+
await b2.Result.DisposeAsync();
87+
await c2.Result.DisposeAsync();
88+
}
89+
}
90+
91+
public Task DisposeAsync() => Task.CompletedTask;
92+
93+
private string Message(string x) => $"Task {x} should be completed.";
94+
95+
private string Message(string x, string y) => $"Task {x} did not wait for task {y} to release the distributed lock.";
96+
}
97+
}

tests/Locks.Tests/Locks.Tests.csproj

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
<ItemGroup>
1313
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.0" />
1414
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.6.0" />
15+
<PackageReference Include="Testcontainers.MsSql" Version="3.9.0" />
1516
<PackageReference Include="xunit" Version="2.4.2" />
1617
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.5">
1718
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
@@ -24,6 +25,7 @@
2425
</ItemGroup>
2526

2627
<ItemGroup>
28+
<ProjectReference Include="..\..\source\Locks.SqlServer\Locks.SqlServer.csproj" />
2729
<ProjectReference Include="..\..\source\Locks\Locks.csproj" />
2830
</ItemGroup>
2931

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
namespace Locks.Tests.Seed
2+
{
3+
internal sealed class DisabledMemoryLock : IMemoryLock
4+
{
5+
public Task<IMemoryLockInstance> AcquireAsync(string key, CancellationToken cancellationToken = default)
6+
{
7+
return Task.FromResult<IMemoryLockInstance>(new DisabledMemoryLockInstance());
8+
}
9+
}
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Locks.Tests.Seed
2+
{
3+
internal sealed class DisabledMemoryLockInstance : IMemoryLockInstance
4+
{
5+
public void Dispose() { }
6+
}
7+
}

0 commit comments

Comments
 (0)