Skip to content

Commit 304afa5

Browse files
add etcdlockprovider and also add test, but did not pass yet
1 parent 1c9f2ac commit 304afa5

File tree

14 files changed

+570
-399
lines changed

14 files changed

+570
-399
lines changed

src/DistributedLock.Core/packages.lock.json

Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,6 @@
1111
"System.Threading.Tasks.Extensions": "4.5.4"
1212
}
1313
},
14-
"Microsoft.CodeAnalysis.PublicApiAnalyzers": {
15-
"type": "Direct",
16-
"requested": "[3.3.4, )",
17-
"resolved": "3.3.4",
18-
"contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
19-
},
2014
"Microsoft.NETFramework.ReferenceAssemblies": {
2115
"type": "Direct",
2216
"requested": "[1.0.3, )",
@@ -81,12 +75,6 @@
8175
"System.Threading.Tasks.Extensions": "4.5.4"
8276
}
8377
},
84-
"Microsoft.CodeAnalysis.PublicApiAnalyzers": {
85-
"type": "Direct",
86-
"requested": "[3.3.4, )",
87-
"resolved": "3.3.4",
88-
"contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
89-
},
9078
"Microsoft.SourceLink.GitHub": {
9179
"type": "Direct",
9280
"requested": "[8.0.0, )",
@@ -136,12 +124,6 @@
136124
}
137125
},
138126
".NETStandard,Version=v2.1": {
139-
"Microsoft.CodeAnalysis.PublicApiAnalyzers": {
140-
"type": "Direct",
141-
"requested": "[3.3.4, )",
142-
"resolved": "3.3.4",
143-
"contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
144-
},
145127
"Microsoft.SourceLink.GitHub": {
146128
"type": "Direct",
147129
"requested": "[8.0.0, )",
@@ -164,17 +146,11 @@
164146
}
165147
},
166148
"net8.0": {
167-
"Microsoft.CodeAnalysis.PublicApiAnalyzers": {
168-
"type": "Direct",
169-
"requested": "[3.3.4, )",
170-
"resolved": "3.3.4",
171-
"contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
172-
},
173149
"Microsoft.NET.ILLink.Tasks": {
174150
"type": "Direct",
175-
"requested": "[8.0.4, )",
176-
"resolved": "8.0.4",
177-
"contentHash": "PZb5nfQ+U19nhnmnR9T1jw+LTmozhuG2eeuzuW5A7DqxD/UXW2ucjmNJqnqOuh8rdPzM3MQXoF8AfFCedJdCUw=="
151+
"requested": "[8.0.17, )",
152+
"resolved": "8.0.17",
153+
"contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw=="
178154
},
179155
"Microsoft.SourceLink.GitHub": {
180156
"type": "Direct",

src/DistributedLock.Etcd/EtcdLeaseDistributedLock.cs

Lines changed: 142 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,21 +10,18 @@ namespace Medallion.Threading.Etcd;
1010
/// </summary>
1111
public sealed partial class EtcdLeaseDistributedLock : IInternalDistributedLock<EtcdLeaseDistributedLockHandle>
1212
{
13-
private static readonly TimeSpan MinBusyWaitSleepTime = TimeSpan.FromMilliseconds(25),
14-
MaxBusyWaitSleepTime = TimeSpan.FromSeconds(1);
15-
16-
1713
private readonly EtcdClientWrapper _etcdClient;
18-
1914

15+
private readonly (TimeoutValue duration, TimeoutValue renewalCadence, TimeoutValue minBusyWaitSleepTime,
16+
TimeoutValue maxBusyWaitSleepTime) _options;
2017

21-
public EtcdLeaseDistributedLock(IEtcdClient client, string lockName, int timeToLive = 60)
18+
public EtcdLeaseDistributedLock(IEtcdClient client, string lockName, Action<EtcdLeaseOptionsBuilder>? options = null)
2219
{
2320
this.LockName = lockName ?? throw new ArgumentNullException(nameof(lockName));
2421
if (lockName.Length == 0) { throw new FormatException($"{nameof(lockName)}: may not have an empty file name"); }
2522

2623
this._etcdClient = new EtcdClientWrapper(client);
27-
this.TimeToLive = timeToLive;
24+
this._options = EtcdLeaseOptionsBuilder.GetOptions(options);
2825
}
2926

3027
// todo revisit API
@@ -33,36 +30,169 @@ public EtcdLeaseDistributedLock(IEtcdClient client, string lockName, int timeToL
3330
/// </summary>
3431
public string LockName { get; }
3532

36-
public long TimeToLive { get; private set; }
3733

3834
ValueTask<EtcdLeaseDistributedLockHandle?> IInternalDistributedLock<EtcdLeaseDistributedLockHandle>.
3935
InternalTryAcquireAsync(TimeoutValue timeout, CancellationToken cancellationToken) =>
4036
BusyWaitHelper.WaitAsync(
4137
state: this,
4238
tryGetValue: (@this, token) => @this.TryAcquireAsync(token),
4339
timeout: timeout,
44-
minSleepTime: MinBusyWaitSleepTime,
45-
maxSleepTime: MaxBusyWaitSleepTime,
40+
minSleepTime: this._options.minBusyWaitSleepTime,
41+
maxSleepTime: this._options.maxBusyWaitSleepTime,
4642
cancellationToken
4743
);
4844

4945
private async ValueTask<EtcdLeaseDistributedLockHandle?> TryAcquireAsync(CancellationToken cancellationToken)
5046
{
5147
cancellationToken.ThrowIfCancellationRequested();
5248
// 1 lease per application is enough in my case
53-
var leaseResponse = await this._etcdClient.LeaseGrantAsync(new LeaseGrantRequest { TTL = this.TimeToLive },
49+
var leaseResponse = await this._etcdClient.LeaseGrantAsync(
50+
new LeaseGrantRequest { TTL = this._options.renewalCadence.InSeconds },
5451
cancellationToken: cancellationToken);
5552
var leaseId = leaseResponse.ID;
5653
var cancellationTokenSource = new CancellationTokenSource();
5754
// this will start a task to renew the lease every 1/3 of the ttl, should i use the ILeaseHandle here instead
5855
_ = this._etcdClient.LeaseKeepAlive(leaseId, cancellationTokenSource.Token);
5956
var response =
6057
await this._etcdClient.LockAsync(
61-
new LockRequest { Name = Google.Protobuf.ByteString.CopyFromUtf8(this.LockName), Lease = leaseId },
58+
new LockRequest { Name = Google.Protobuf.ByteString.CopyFromUtf8(this.LockName), Lease = leaseId, },
6259
cancellationToken: cancellationTokenSource.Token).ConfigureAwait(false);
6360
var actualKey = response.Key;
6461
return new EtcdLeaseDistributedLockHandle(actualKey.ToString(), this._etcdClient, leaseId);
6562
}
6663

6764
public string Name => this.LockName;
65+
66+
public static string GetSafeName(string name)
67+
{
68+
// TODO figure
69+
return DistributedLockHelpers.ToSafeName(name, 1000, s => s);
70+
}
71+
}
72+
73+
/// <summary>
74+
/// basically similar to AzureBlobLeaseDistributedLock
75+
/// </summary>
76+
public class EtcdLeaseOptionsBuilder
77+
{
78+
/// <summary>
79+
/// From https://docs.microsoft.com/en-us/rest/api/storageservices/lease-blob:
80+
/// "The lock duration can be 15 to 60 seconds, or can be infinite"
81+
/// </summary>
82+
internal static readonly TimeoutValue MinLeaseDuration = TimeSpan.FromSeconds(15),
83+
MaxNonInfiniteLeaseDuration = TimeSpan.FromSeconds(60),
84+
DefaultLeaseDuration = TimeSpan.FromSeconds(30);
85+
86+
private TimeoutValue? _duration, _renewalCadence, _minBusyWaitSleepTime, _maxBusyWaitSleepTime;
87+
88+
internal EtcdLeaseOptionsBuilder() { }
89+
90+
/// <summary>
91+
/// Specifies how long the lease will last, absent auto-renewal.
92+
///
93+
/// If auto-renewal is enabled (the default), then a shorter duration means more frequent auto-renewal requests,
94+
/// while an infinite duration means no auto-renewal requests. Furthermore, if the lease-holding process were to
95+
/// exit without explicitly releasing, then duration determines how long other processes would need to wait in
96+
/// order to acquire the lease.
97+
///
98+
/// If auto-renewal is disabled, then duration determines how long the lease will be held.
99+
///
100+
/// Defaults to 30s.
101+
/// </summary>
102+
public EtcdLeaseOptionsBuilder Duration(TimeSpan duration)
103+
{
104+
var durationTimeoutValue = new TimeoutValue(duration, nameof(duration));
105+
if (durationTimeoutValue.CompareTo(MinLeaseDuration) < 0
106+
|| (!durationTimeoutValue.IsInfinite && durationTimeoutValue.CompareTo(MaxNonInfiniteLeaseDuration) > 0))
107+
{
108+
throw new ArgumentOutOfRangeException(nameof(duration), duration,
109+
$"Must be infinite or in [{MinLeaseDuration}, {MaxNonInfiniteLeaseDuration}]");
110+
}
111+
112+
this._duration = durationTimeoutValue;
113+
return this;
114+
}
115+
116+
/// <summary>
117+
/// Determines how frequently the lease will be renewed when held. More frequent renewal means more unnecessary requests
118+
/// but also a lower chance of losing the lease due to the process hanging or otherwise failing to get its renewal request in
119+
/// before the lease duration expires.
120+
///
121+
/// To disable auto-renewal, specify <see cref="Timeout.InfiniteTimeSpan"/>
122+
///
123+
/// Defaults to 1/3 of the specified lease duration (may be infinite).
124+
/// </summary>
125+
public EtcdLeaseOptionsBuilder RenewalCadence(TimeSpan renewalCadence)
126+
{
127+
this._renewalCadence = new TimeoutValue(renewalCadence, nameof(renewalCadence));
128+
return this;
129+
}
130+
131+
/// <summary>
132+
/// Waiting to acquire a lease requires a busy wait that alternates acquire attempts and sleeps.
133+
/// This determines how much time is spent sleeping between attempts. Lower values will raise the
134+
/// volume of acquire requests under contention but will also raise the responsiveness (how long
135+
/// it takes a waiter to notice that a contended the lease has become available).
136+
///
137+
/// Specifying a range of values allows the implementation to select an actual value in the range
138+
/// at random for each sleep. This helps avoid the case where two clients become "synchronized"
139+
/// in such a way that results in one client monopolizing the lease.
140+
///
141+
/// The default is [250ms, 1s]
142+
/// </summary>
143+
public EtcdLeaseOptionsBuilder BusyWaitSleepTime(TimeSpan min, TimeSpan max)
144+
{
145+
var minTimeoutValue = new TimeoutValue(min, nameof(min));
146+
var maxTimeoutValue = new TimeoutValue(max, nameof(max));
147+
148+
if (minTimeoutValue.IsInfinite) { throw new ArgumentOutOfRangeException(nameof(min), "may not be infinite"); }
149+
150+
if (maxTimeoutValue.IsInfinite || maxTimeoutValue.CompareTo(min) < 0)
151+
{
152+
throw new ArgumentOutOfRangeException(nameof(max), max,
153+
"must be non-infinite and greater than " + nameof(min));
154+
}
155+
156+
this._minBusyWaitSleepTime = minTimeoutValue;
157+
this._maxBusyWaitSleepTime = maxTimeoutValue;
158+
return this;
159+
}
160+
161+
internal static (TimeoutValue duration, TimeoutValue renewalCadence, TimeoutValue minBusyWaitSleepTime, TimeoutValue
162+
maxBusyWaitSleepTime) GetOptions(Action<EtcdLeaseOptionsBuilder>? optionsBuilder)
163+
{
164+
EtcdLeaseOptionsBuilder? options;
165+
if (optionsBuilder != null)
166+
{
167+
options = new EtcdLeaseOptionsBuilder();
168+
optionsBuilder(options);
169+
170+
if (options._renewalCadence is { } renewalCadence && !renewalCadence.IsInfinite)
171+
{
172+
var duration = options._duration ?? DefaultLeaseDuration;
173+
if (renewalCadence.CompareTo(duration) >= 0)
174+
{
175+
throw new ArgumentOutOfRangeException(
176+
nameof(renewalCadence),
177+
renewalCadence.TimeSpan,
178+
$"{nameof(renewalCadence)} must not be larger than {nameof(duration)} ({duration}). To disable auto-renewal, specify {nameof(Timeout)}.{nameof(Timeout.InfiniteTimeSpan)}"
179+
);
180+
}
181+
}
182+
}
183+
else
184+
{
185+
options = null;
186+
}
187+
188+
var durationToUse = options?._duration ?? DefaultLeaseDuration;
189+
return (
190+
duration: durationToUse,
191+
renewalCadence: options?._renewalCadence ?? (durationToUse.IsInfinite
192+
? Timeout.InfiniteTimeSpan
193+
: TimeSpan.FromMilliseconds(durationToUse.InMilliseconds / 3.0)),
194+
minBusyWaitSleepTime: options?._minBusyWaitSleepTime ?? TimeSpan.FromMilliseconds(250),
195+
maxBusyWaitSleepTime: options?._maxBusyWaitSleepTime ?? TimeSpan.FromSeconds(1)
196+
);
197+
}
68198
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using dotnet_etcd.interfaces;
2+
3+
namespace Medallion.Threading.Etcd;
4+
5+
/// <summary>
6+
/// Implements <see cref="IDistributedLockProvider"/> for <see cref="AzureBlobLeaseDistributedLock"/>
7+
/// </summary>
8+
public sealed class EtcdLeaseDistributedLockProvider : IDistributedLockProvider
9+
{
10+
private readonly IEtcdClient _blobContainerClient;
11+
private readonly Action<EtcdLeaseOptionsBuilder>? _options;
12+
13+
/// <summary>
14+
/// Constructs a provider that scopes blobs within the provided <paramref name="blobContainerClient"/> and uses the provided <paramref name="options"/>.
15+
/// </summary>
16+
public EtcdLeaseDistributedLockProvider(IEtcdClient blobContainerClient,
17+
Action<EtcdLeaseOptionsBuilder>? options = null)
18+
{
19+
this._blobContainerClient = blobContainerClient ?? throw new ArgumentNullException(nameof(blobContainerClient));
20+
this._options = options;
21+
}
22+
23+
/// <summary>
24+
/// Constructs an <see cref="AzureBlobLeaseDistributedLock"/> with the given <paramref name="name"/>.
25+
/// </summary>
26+
public EtcdLeaseDistributedLock CreateLock(string name) => new(this._blobContainerClient, name, this._options);
27+
28+
IDistributedLock IDistributedLockProvider.CreateLock(string name) => this.CreateLock(name);
29+
}

src/DistributedLock.Oracle/packages.lock.json

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@
88
"resolved": "3.3.4",
99
"contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
1010
},
11+
"Microsoft.NETFramework.ReferenceAssemblies": {
12+
"type": "Direct",
13+
"requested": "[1.0.3, )",
14+
"resolved": "1.0.3",
15+
"contentHash": "vUc9Npcs14QsyOD01tnv/m8sQUnGTGOw1BCmKcv77LBJY7OxhJ+zJF7UD/sCL3lYNFuqmQEVlkfS4Quif6FyYg==",
16+
"dependencies": {
17+
"Microsoft.NETFramework.ReferenceAssemblies.net472": "1.0.3"
18+
}
19+
},
1120
"Microsoft.SourceLink.GitHub": {
1221
"type": "Direct",
1322
"requested": "[8.0.0, )",
@@ -43,6 +52,11 @@
4352
"resolved": "8.0.0",
4453
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
4554
},
55+
"Microsoft.NETFramework.ReferenceAssemblies.net472": {
56+
"type": "Transitive",
57+
"resolved": "1.0.3",
58+
"contentHash": "0E7evZXHXaDYYiLRfpyXvCh+yzM2rNTyuZDI+ZO7UUqSc6GfjePiXTdqJGtgIKUwdI81tzQKmaWprnUiPj9hAw=="
59+
},
4660
"Microsoft.SourceLink.Common": {
4761
"type": "Transitive",
4862
"resolved": "8.0.0",

src/DistributedLock.Postgres/packages.lock.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -518,9 +518,9 @@
518518
},
519519
"Microsoft.NET.ILLink.Tasks": {
520520
"type": "Direct",
521-
"requested": "[8.0.4, )",
522-
"resolved": "8.0.4",
523-
"contentHash": "PZb5nfQ+U19nhnmnR9T1jw+LTmozhuG2eeuzuW5A7DqxD/UXW2ucjmNJqnqOuh8rdPzM3MQXoF8AfFCedJdCUw=="
521+
"requested": "[8.0.17, )",
522+
"resolved": "8.0.17",
523+
"contentHash": "x5/y4l8AtshpBOrCZdlE4txw8K3e3s9meBFeZeR3l8hbbku2V7kK6ojhXvrbjg1rk3G+JqL1BI26gtgc1ZrdUw=="
524524
},
525525
"Microsoft.SourceLink.GitHub": {
526526
"type": "Direct",

src/DistributedLock.Tests/Tests/CombinatorialTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@ namespace Medallion.Threading.Tests.Azure
66
public class Core_AzureBlobLease_AzureBlobLeaseSynchronizationStrategyTest : DistributedLockCoreTestCases<TestingAzureBlobLeaseDistributedLockProvider, TestingAzureBlobLeaseSynchronizationStrategy> { }
77
}
88

9+
namespace Medallion.Threading.Tests.Etcd
10+
{
11+
public class Core_EtcdLease_EtcdLeaseSynchronizationStrategyTest : DistributedLockCoreTestCases<TestingEtcdLeaseDistributedLockProvider, TestingEtcdLeaseSynchronizationStrategy> { }
12+
}
13+
914
namespace Medallion.Threading.Tests.FileSystem
1015
{
1116
[Category("CI")] public class Core_File_FileSynchronizationStrategyTest : DistributedLockCoreTestCases<TestingFileDistributedLockProvider, TestingLockFileSynchronizationStrategy> { }

0 commit comments

Comments
 (0)