Skip to content

Commit 9d2fcca

Browse files
authored
Merge pull request #252 from madelson/release-2.7
Release 2.7
2 parents 94483ed + e59f787 commit 9d2fcca

File tree

13 files changed

+217
-58
lines changed

13 files changed

+217
-58
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,6 +141,8 @@ Contributions are welcome! If you are interested in contributing towards a new o
141141
Setup steps for working with the repository locally are documented [here](docs/Developing%20DistributedLock.md).
142142

143143
## Release notes
144+
- 2.7
145+
- Add support for fetching a Redis-based semaphore's current available count. Thanks [@teesoftech](https://github.com/teesofttech) for implementing! ([#234](https://github.com/madelson/DistributedLock/issues/234), DistributedLock.Redis 1.1)
144146
- 2.6
145147
- Add support for acquiring transaction-scoped Postgres locks using externally-owned transactions. Thanks [@Tzachi009](https://github.com/Tzachi009) for implementing! ([#213](https://github.com/madelson/DistributedLock/issues/213), DistributedLock.Postgres 1.3)
146148
- 2.5.1

src/Directory.Packages.props

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
<PackageVersion Include="Oracle.ManagedDataAccess.Core" Version="23.6.1" Condition="'$(TargetFramework)' == 'netstandard2.1'" />
1414
<PackageVersion Include="Oracle.ManagedDataAccess" Version="23.6.1" Condition="'$(TargetFramework)' == 'net472'" />
1515
<PackageVersion Include="Npgsql" Version="8.0.6" />
16-
<PackageVersion Include="StackExchange.Redis" Version="2.7.27" />
16+
<PackageVersion Include="StackExchange.Redis" Version="2.7.33" />
1717
<PackageVersion Include="Microsoft.Data.SqlClient" Version="5.2.2" />
1818
<PackageVersion Include="nunit" Version="3.14.0" />
1919
<PackageVersion Include="nunit3testadapter" Version="4.5.0" />

src/DistributedLock.Redis/DistributedLock.Redis.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
</PropertyGroup>
1212

1313
<PropertyGroup>
14-
<Version>1.0.3</Version>
14+
<Version>1.1.0</Version>
1515
<AssemblyVersion>1.0.0.0</AssemblyVersion>
1616
<Authors>Michael Adelson</Authors>
1717
<Description>Provides distributed locking primitives based on Redis</Description>

src/DistributedLock.Redis/Primitives/RedisSemaphorePrimitive.cs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,4 +78,25 @@ public Task ReleaseAsync(IDatabaseAsync database, bool fireAndForget) =>
7878
public Task<bool> TryExtendAsync(IDatabaseAsync database) => ExtendScript.ExecuteAsync(database, this).AsBooleanTask();
7979

8080
public bool IsConnected(IDatabase database) => database.IsConnected(this._key, CommandFlags.DemandMaster);
81+
82+
public async ValueTask<long> GetAcquiredCountAsync(IDatabase database)
83+
{
84+
long held;
85+
if (SyncViaAsync.IsSynchronous)
86+
{
87+
held = database.SortedSetLength(
88+
this._key,
89+
flags: CommandFlags.DemandMaster
90+
);
91+
}
92+
else
93+
{
94+
held = await database.SortedSetLengthAsync(
95+
this._key,
96+
flags: CommandFlags.DemandMaster
97+
).ConfigureAwait(false);
98+
}
99+
100+
return held;
101+
}
81102
}

src/DistributedLock.Redis/PublicAPI.Shipped.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ Medallion.Threading.Redis.RedisDistributedSemaphore.Acquire(System.TimeSpan? tim
3333
Medallion.Threading.Redis.RedisDistributedSemaphore.AcquireAsync(System.TimeSpan? timeout = null, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Medallion.Threading.Redis.RedisDistributedSemaphoreHandle!>
3434
Medallion.Threading.Redis.RedisDistributedSemaphore.MaxCount.get -> int
3535
Medallion.Threading.Redis.RedisDistributedSemaphore.Name.get -> string!
36+
Medallion.Threading.Redis.RedisDistributedSemaphore.GetCurrentCount() -> int
37+
Medallion.Threading.Redis.RedisDistributedSemaphore.GetCurrentCountAsync() -> System.Threading.Tasks.ValueTask<int>
3638
Medallion.Threading.Redis.RedisDistributedSemaphore.RedisDistributedSemaphore(StackExchange.Redis.RedisKey key, int maxCount, StackExchange.Redis.IDatabase! database, System.Action<Medallion.Threading.Redis.RedisDistributedSynchronizationOptionsBuilder!>? options = null) -> void
3739
Medallion.Threading.Redis.RedisDistributedSemaphore.TryAcquire(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> Medallion.Threading.Redis.RedisDistributedSemaphoreHandle?
3840
Medallion.Threading.Redis.RedisDistributedSemaphore.TryAcquireAsync(System.TimeSpan timeout = default(System.TimeSpan), System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) -> System.Threading.Tasks.ValueTask<Medallion.Threading.Redis.RedisDistributedSemaphoreHandle?>
@@ -50,4 +52,4 @@ Medallion.Threading.Redis.RedisDistributedSynchronizationProvider.CreateLock(Sta
5052
Medallion.Threading.Redis.RedisDistributedSynchronizationProvider.CreateReaderWriterLock(string! name) -> Medallion.Threading.Redis.RedisDistributedReaderWriterLock!
5153
Medallion.Threading.Redis.RedisDistributedSynchronizationProvider.CreateSemaphore(StackExchange.Redis.RedisKey key, int maxCount) -> Medallion.Threading.Redis.RedisDistributedSemaphore!
5254
Medallion.Threading.Redis.RedisDistributedSynchronizationProvider.RedisDistributedSynchronizationProvider(StackExchange.Redis.IDatabase! database, System.Action<Medallion.Threading.Redis.RedisDistributedSynchronizationOptionsBuilder!>? options = null) -> void
53-
Medallion.Threading.Redis.RedisDistributedSynchronizationProvider.RedisDistributedSynchronizationProvider(System.Collections.Generic.IEnumerable<StackExchange.Redis.IDatabase!>! databases, System.Action<Medallion.Threading.Redis.RedisDistributedSynchronizationOptionsBuilder!>? options = null) -> void
55+
Medallion.Threading.Redis.RedisDistributedSynchronizationProvider.RedisDistributedSynchronizationProvider(System.Collections.Generic.IEnumerable<StackExchange.Redis.IDatabase!>! databases, System.Action<Medallion.Threading.Redis.RedisDistributedSynchronizationOptionsBuilder!>? options = null) -> void

src/DistributedLock.Redis/RedisDistributedSemaphore.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,24 @@ public RedisDistributedSemaphore(RedisKey key, int maxCount, IDatabase database,
3939
/// </summary>
4040
public string Name => this.Key.ToString();
4141

42+
/// <summary>
43+
/// Gets the current available count. Comparable to <see cref="SemaphoreSlim.CurrentCount"/>
44+
/// </summary>
45+
public int GetCurrentCount() => SyncViaAsync.Run(s => s.GetCurrentCountAsync(), state: this);
46+
47+
/// <summary>
48+
/// Asynchronously gets the current available count. Comparable to <see cref="SemaphoreSlim.CurrentCount"/>
49+
/// </summary>
50+
public async ValueTask<int> GetCurrentCountAsync()
51+
{
52+
var database = this._databases[0];
53+
var primitive = new RedisSemaphorePrimitive(this.Key, this.MaxCount, this._options.RedLockTimeouts);
54+
var acquiredCount = await primitive.GetAcquiredCountAsync(database).ConfigureAwait(false);
55+
56+
var available = this.MaxCount - acquiredCount;
57+
return (int)Math.Max(0, available);
58+
}
59+
4260
/// <summary>
4361
/// Implements <see cref="IDistributedSemaphore.MaxCount"/>
4462
/// </summary>

src/DistributedLock.Redis/packages.lock.json

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@
3535
},
3636
"StackExchange.Redis": {
3737
"type": "Direct",
38-
"requested": "[2.7.27, )",
39-
"resolved": "2.7.27",
40-
"contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
38+
"requested": "[2.7.33, )",
39+
"resolved": "2.7.33",
40+
"contentHash": "2kCX5fvhEE824a4Ab5Imyi8DRuGuTxyklXV01kegkRpsWJcPmO6+GAQ+HegKxvXAxlXZ8yaRspvWJ8t3mMClfQ==",
4141
"dependencies": {
4242
"Microsoft.Bcl.AsyncInterfaces": "5.0.0",
4343
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
@@ -197,9 +197,9 @@
197197
},
198198
"StackExchange.Redis": {
199199
"type": "Direct",
200-
"requested": "[2.7.27, )",
201-
"resolved": "2.7.27",
202-
"contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
200+
"requested": "[2.7.33, )",
201+
"resolved": "2.7.33",
202+
"contentHash": "2kCX5fvhEE824a4Ab5Imyi8DRuGuTxyklXV01kegkRpsWJcPmO6+GAQ+HegKxvXAxlXZ8yaRspvWJ8t3mMClfQ==",
203203
"dependencies": {
204204
"Microsoft.Bcl.AsyncInterfaces": "5.0.0",
205205
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
@@ -325,9 +325,9 @@
325325
},
326326
"StackExchange.Redis": {
327327
"type": "Direct",
328-
"requested": "[2.7.27, )",
329-
"resolved": "2.7.27",
330-
"contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
328+
"requested": "[2.7.33, )",
329+
"resolved": "2.7.33",
330+
"contentHash": "2kCX5fvhEE824a4Ab5Imyi8DRuGuTxyklXV01kegkRpsWJcPmO6+GAQ+HegKxvXAxlXZ8yaRspvWJ8t3mMClfQ==",
331331
"dependencies": {
332332
"Microsoft.Bcl.AsyncInterfaces": "5.0.0",
333333
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",

src/DistributedLock.Tests/AbstractTestCases/Redis/RedisSynchronizationCoreTestCases.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ public void TestMajorityFaultingDatabasesCauseAcquireToThrow()
3030
var @lock = this._provider.CreateLock("multi");
3131

3232
// we only get the one exception
33-
Assert.ThrowsAsync<TimeZoneNotFoundException>(() => @lock.TryAcquireAsync().AsTask());
33+
Assert.That(
34+
Assert.CatchAsync(() => @lock.TryAcquireAsync().AsTask()),
35+
Is.InstanceOf<TimeZoneNotFoundException>().Or.InstanceOf<ArrayTypeMismatchException>());
3436

3537
// single sync acquire flow is different
3638
this._provider.Strategy.DatabaseProvider.Databases = new[] { databases[2].Object };

src/DistributedLock.Tests/Tests/Redis/RedisDistributedSemaphoreTest.cs

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,118 @@ public void TestValidatesConstructorParameters()
2626
Assert.Throws<ArgumentOutOfRangeException>(() => new RedisDistributedSemaphore("key", -1, database));
2727
Assert.Throws<ArgumentNullException>(() => new RedisDistributedSemaphore("key", 2, default(IDatabase)!));
2828
}
29+
30+
[Test]
31+
[Category("CI")]
32+
public async Task TestGetCurrentCountAsync()
33+
{
34+
const int maxCount = 5;
35+
const int heldCount = 2;
36+
var expectedAvailable = maxCount - heldCount;
37+
38+
var databaseMock = new Mock<IDatabase>(MockBehavior.Strict);
39+
databaseMock
40+
.Setup(db => db.SortedSetLengthAsync(
41+
It.Is<RedisKey>(k => k == "test-key"),
42+
It.IsAny<double>(),
43+
It.IsAny<double>(),
44+
It.IsAny<Exclude>(),
45+
CommandFlags.DemandMaster))
46+
.ReturnsAsync(heldCount);
47+
48+
var semaphore = new RedisDistributedSemaphore("test-key", 5, databaseMock.Object);
49+
var available = await semaphore.GetCurrentCountAsync();
50+
51+
available.ShouldEqual(expectedAvailable);
52+
53+
databaseMock.VerifyAll();
54+
}
55+
56+
[Test]
57+
[Category("CI")]
58+
public void TestGetCurrentCountSync()
59+
{
60+
const int maxCount = 4;
61+
const int heldCount = 3;
62+
var expectedAvailable = maxCount - heldCount;
63+
64+
var databaseMock = new Mock<IDatabase>(MockBehavior.Strict);
65+
databaseMock
66+
.Setup(db => db.SortedSetLength(
67+
It.Is<RedisKey>(k => k == "test-key"),
68+
It.IsAny<double>(),
69+
It.IsAny<double>(),
70+
It.IsAny<Exclude>(),
71+
CommandFlags.DemandMaster))
72+
.Returns(heldCount);
73+
74+
var semaphore = new RedisDistributedSemaphore("test-key", maxCount, databaseMock.Object);
75+
var available = semaphore.GetCurrentCount();
76+
77+
expectedAvailable.ShouldEqual(available);
78+
databaseMock.VerifyAll();
79+
}
80+
81+
82+
[Test]
83+
public async Task TestGetCurrentCountReflectsAcquisitionsAndReleases()
84+
{
85+
var db = RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase();
86+
87+
const int maxCount = 3;
88+
var key = TestHelper.UniqueName + ":sem";
89+
var semaphore = new RedisDistributedSemaphore(key, maxCount, db);
90+
91+
maxCount.ShouldEqual(semaphore.GetCurrentCount());
92+
maxCount.ShouldEqual(await semaphore.GetCurrentCountAsync());
93+
94+
// Acquire one
95+
var handle1 = await semaphore.AcquireAsync();
96+
97+
Assert.IsNotNull(handle1);
98+
Assert.That(semaphore.GetCurrentCount(), Is.EqualTo(maxCount - 1));
99+
Assert.That(await semaphore.GetCurrentCountAsync(), Is.EqualTo(maxCount - 1));
100+
101+
// Acquire second
102+
var handle2 = await semaphore.AcquireAsync();
103+
Assert.IsNotNull(handle2);
104+
Assert.That(semaphore.GetCurrentCount(), Is.EqualTo(maxCount - 2));
105+
Assert.That(await semaphore.GetCurrentCountAsync(), Is.EqualTo(maxCount - 2));
106+
107+
// Release first
108+
await handle1.DisposeAsync();
109+
Assert.That(semaphore.GetCurrentCount(), Is.EqualTo(maxCount - 1));
110+
Assert.That(await semaphore.GetCurrentCountAsync(), Is.EqualTo(maxCount - 1));
111+
112+
// Release second
113+
await handle2.DisposeAsync();
114+
Assert.That(semaphore.GetCurrentCount(), Is.EqualTo(maxCount));
115+
Assert.That(await semaphore.GetCurrentCountAsync(), Is.EqualTo(maxCount));
116+
}
117+
118+
[Test]
119+
public async Task TestGetCurrentCountNeverNegativeWhenOverReleased()
120+
{
121+
var db = RedisServer.GetDefaultServer(0).Multiplexer.GetDatabase();
122+
123+
const int maxCount = 2;
124+
var key = TestHelper.UniqueName + ":sem2";
125+
var semaphore = new RedisDistributedSemaphore(key, maxCount, db);
126+
127+
// Acquire more than maxCount (simulate drift)
128+
var h1 = await semaphore.AcquireAsync();
129+
var h2 = await semaphore.AcquireAsync();
130+
// manually add a phantom entry to exceed maxCount
131+
await db.SortedSetAddAsync(key, "phantom", 0);
132+
133+
// Now phantom + two real => acquiredCount == 3 > maxCount
134+
// GetCurrentCount should floor at 0
135+
Assert.That(semaphore.GetCurrentCount(), Is.EqualTo(0));
136+
Assert.That(await semaphore.GetCurrentCountAsync(), Is.EqualTo(0));
137+
138+
// Cleanup
139+
await h1.DisposeAsync();
140+
await h2.DisposeAsync();
141+
await db.KeyDeleteAsync(key);
142+
}
29143
}

src/DistributedLock.Tests/packages.lock.json

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -425,11 +425,11 @@
425425
"type": "Project",
426426
"dependencies": {
427427
"DistributedLock.Azure": "[1.0.2, )",
428-
"DistributedLock.FileSystem": "[1.0.2, )",
428+
"DistributedLock.FileSystem": "[1.0.3, )",
429429
"DistributedLock.MySql": "[1.0.2, )",
430430
"DistributedLock.Oracle": "[1.0.4, )",
431-
"DistributedLock.Postgres": "[1.2.0, )",
432-
"DistributedLock.Redis": "[1.0.3, )",
431+
"DistributedLock.Postgres": "[1.3.0, )",
432+
"DistributedLock.Redis": "[1.1.0, )",
433433
"DistributedLock.SqlServer": "[1.0.6, )",
434434
"DistributedLock.WaitHandles": "[1.0.1, )",
435435
"DistributedLock.ZooKeeper": "[1.0.0, )"
@@ -480,7 +480,7 @@
480480
"type": "Project",
481481
"dependencies": {
482482
"DistributedLock.Core": "[1.0.8, )",
483-
"StackExchange.Redis": "[2.7.27, )"
483+
"StackExchange.Redis": "[2.7.33, )"
484484
}
485485
},
486486
"distributedlock.sqlserver": {
@@ -570,9 +570,9 @@
570570
},
571571
"StackExchange.Redis": {
572572
"type": "CentralTransitive",
573-
"requested": "[2.7.27, )",
574-
"resolved": "2.7.27",
575-
"contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
573+
"requested": "[2.7.33, )",
574+
"resolved": "2.7.33",
575+
"contentHash": "2kCX5fvhEE824a4Ab5Imyi8DRuGuTxyklXV01kegkRpsWJcPmO6+GAQ+HegKxvXAxlXZ8yaRspvWJ8t3mMClfQ==",
576576
"dependencies": {
577577
"Microsoft.Bcl.AsyncInterfaces": "5.0.0",
578578
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
@@ -1068,11 +1068,11 @@
10681068
"type": "Project",
10691069
"dependencies": {
10701070
"DistributedLock.Azure": "[1.0.2, )",
1071-
"DistributedLock.FileSystem": "[1.0.2, )",
1071+
"DistributedLock.FileSystem": "[1.0.3, )",
10721072
"DistributedLock.MySql": "[1.0.2, )",
10731073
"DistributedLock.Oracle": "[1.0.4, )",
1074-
"DistributedLock.Postgres": "[1.2.0, )",
1075-
"DistributedLock.Redis": "[1.0.3, )",
1074+
"DistributedLock.Postgres": "[1.3.0, )",
1075+
"DistributedLock.Redis": "[1.1.0, )",
10761076
"DistributedLock.SqlServer": "[1.0.6, )",
10771077
"DistributedLock.WaitHandles": "[1.0.1, )",
10781078
"DistributedLock.ZooKeeper": "[1.0.0, )"
@@ -1119,7 +1119,7 @@
11191119
"type": "Project",
11201120
"dependencies": {
11211121
"DistributedLock.Core": "[1.0.8, )",
1122-
"StackExchange.Redis": "[2.7.27, )"
1122+
"StackExchange.Redis": "[2.7.33, )"
11231123
}
11241124
},
11251125
"distributedlock.sqlserver": {
@@ -1189,9 +1189,9 @@
11891189
},
11901190
"StackExchange.Redis": {
11911191
"type": "CentralTransitive",
1192-
"requested": "[2.7.27, )",
1193-
"resolved": "2.7.27",
1194-
"contentHash": "Uqc2OQHglqj9/FfGQ6RkKFkZfHySfZlfmbCl+hc+u2I/IqunfelQ7QJi7ZhvAJxUtu80pildVX6NPLdDaUffOw==",
1192+
"requested": "[2.7.33, )",
1193+
"resolved": "2.7.33",
1194+
"contentHash": "2kCX5fvhEE824a4Ab5Imyi8DRuGuTxyklXV01kegkRpsWJcPmO6+GAQ+HegKxvXAxlXZ8yaRspvWJ8t3mMClfQ==",
11951195
"dependencies": {
11961196
"Microsoft.Extensions.Logging.Abstractions": "6.0.0",
11971197
"Pipelines.Sockets.Unofficial": "2.2.8"

0 commit comments

Comments
 (0)