Skip to content

Commit fad0d36

Browse files
authored
[leader-election] Update (#423)
* Update * Small changes
1 parent 11c13ac commit fad0d36

6 files changed

Lines changed: 60 additions & 64 deletions

File tree

leader-election/DistributedMutex/BlobDistributedMutex.cs

Lines changed: 28 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,22 @@ namespace DistributedMutex
77
using System.Threading;
88
using System.Threading.Tasks;
99

10-
public class BlobDistributedMutex
10+
public class BlobDistributedMutex(BlobSettings blobSettings, Func<CancellationToken, Task> taskToRunWhenLeaseAcquired, Action? onLeaseTimeoutRetry = null)
1111
{
1212
private static readonly TimeSpan RenewInterval = TimeSpan.FromSeconds(10);
1313
private static readonly TimeSpan AcquireAttemptInterval = TimeSpan.FromSeconds(20);
14-
private readonly BlobSettings blobSettings;
15-
private readonly Func<CancellationToken, Task> taskToRunWhenLeaseAcquired;
16-
private readonly Action? onLeaseTimeoutRetry;
14+
private readonly BlobSettings blobSettings = blobSettings;
15+
private readonly Func<CancellationToken, Task> taskToRunWhenLeaseAcquired = taskToRunWhenLeaseAcquired;
16+
private readonly Action? onLeaseTimeoutRetry = onLeaseTimeoutRetry;
1717

18-
public BlobDistributedMutex(BlobSettings blobSettings, Func<CancellationToken, Task> taskToRunWhenLeaseAcquired, Action? onLeaseTimeoutRetry = null)
19-
{
20-
this.blobSettings = blobSettings;
21-
this.taskToRunWhenLeaseAcquired = taskToRunWhenLeaseAcquired;
22-
this.onLeaseTimeoutRetry = onLeaseTimeoutRetry;
23-
}
24-
25-
public async Task RunTaskWhenMutexAcquired(CancellationToken token)
18+
public async Task RunTaskWhenMutexAcquiredAsync(CancellationToken token)
2619
{
2720
var leaseManager = new BlobLeaseManager(blobSettings);
2821

29-
await RunTaskWhenBlobLeaseAcquired(leaseManager, token);
22+
await RunTaskWhenBlobLeaseAcquiredAsync(leaseManager, token);
3023
}
3124

32-
private static async Task CancelAllWhenAnyCompletes(Task leaderTask, Task renewLeaseTask, CancellationTokenSource cts)
25+
private static async Task CancelAllWhenAnyCompletesAsync(Task leaderTask, Task renewLeaseTask, CancellationTokenSource cts)
3326
{
3427
await Task.WhenAny(leaderTask, renewLeaseTask);
3528

@@ -43,53 +36,48 @@ private static async Task CancelAllWhenAnyCompletes(Task leaderTask, Task renewL
4336
}
4437
catch (Exception)
4538
{
46-
if (allTasks.Exception != null)
47-
{
48-
allTasks.Exception.Handle(ex =>
39+
allTasks.Exception?.Handle(ex =>
4940
{
50-
if (!(ex is OperationCanceledException))
41+
if (ex is not OperationCanceledException)
5142
{
5243
Trace.TraceError(ex.Message);
5344
}
5445

5546
return true;
5647
});
57-
}
5848
}
5949
}
6050

61-
private async Task RunTaskWhenBlobLeaseAcquired(BlobLeaseManager leaseManager, CancellationToken token)
51+
private async Task RunTaskWhenBlobLeaseAcquiredAsync(BlobLeaseManager leaseManager, CancellationToken token)
6252
{
6353
while (!token.IsCancellationRequested)
6454
{
6555
// Try to acquire the blob lease, otherwise wait for some time before we can try again.
66-
string? leaseId = await TryAcquireLeaseOrWait(leaseManager, token);
56+
string? leaseId = await TryAcquireLeaseOrWaitAsync(leaseManager, token);
6757

6858
if (!string.IsNullOrEmpty(leaseId))
6959
{
7060
// Create a new linked cancellation token source, so if either the
7161
// original token is canceled or the lease cannot be renewed,
7262
// then the leader task can be canceled.
73-
using (var leaseCts =
74-
CancellationTokenSource.CreateLinkedTokenSource([token]))
75-
{
76-
// Run the leader task.
77-
var leaderTask = taskToRunWhenLeaseAcquired.Invoke(leaseCts.Token);
78-
79-
// Keeps renewing the lease in regular intervals.
80-
// If the lease cannot be renewed, then the task completes.
81-
var renewLeaseTask =
82-
KeepRenewingLease(leaseManager, leaseId, leaseCts.Token);
83-
84-
// When any task completes (either the leader task or when it could
85-
// not renew the lease) then cancel the other task.
86-
await CancelAllWhenAnyCompletes(leaderTask, renewLeaseTask, leaseCts);
87-
}
63+
using var leaseCts =
64+
CancellationTokenSource.CreateLinkedTokenSource([token]);
65+
// Run the leader task.
66+
var leaderTask = taskToRunWhenLeaseAcquired.Invoke(leaseCts.Token);
67+
68+
// Keeps renewing the lease in regular intervals.
69+
// If the lease cannot be renewed, then the task completes.
70+
var renewLeaseTask =
71+
KeepRenewingLeaseAsync(leaseManager, leaseId, leaseCts.Token);
72+
73+
// When any task completes (either the leader task or when it could
74+
// not renew the lease) then cancel the other task.
75+
await CancelAllWhenAnyCompletesAsync(leaderTask, renewLeaseTask, leaseCts);
8876
}
8977
}
9078
}
9179

92-
private async Task<string?> TryAcquireLeaseOrWait(BlobLeaseManager leaseManager, CancellationToken token)
80+
private async Task<string?> TryAcquireLeaseOrWaitAsync(BlobLeaseManager leaseManager, CancellationToken token)
9381
{
9482
try
9583
{
@@ -98,10 +86,7 @@ private async Task RunTaskWhenBlobLeaseAcquired(BlobLeaseManager leaseManager, C
9886
{
9987
return leaseId;
10088
}
101-
if (onLeaseTimeoutRetry != null)
102-
{
103-
onLeaseTimeoutRetry();
104-
}
89+
onLeaseTimeoutRetry?.Invoke();
10590
await Task.Delay(AcquireAttemptInterval, token);
10691
return null;
10792
}
@@ -111,7 +96,7 @@ private async Task RunTaskWhenBlobLeaseAcquired(BlobLeaseManager leaseManager, C
11196
}
11297
}
11398

114-
private async Task KeepRenewingLease(BlobLeaseManager leaseManager, string leaseId, CancellationToken token)
99+
private async Task KeepRenewingLeaseAsync(BlobLeaseManager leaseManager, string leaseId, CancellationToken token)
115100
{
116101
var renewOffset = new Stopwatch();
117102

@@ -136,7 +121,7 @@ private async Task KeepRenewingLease(BlobLeaseManager leaseManager, string lease
136121
// If the adjusted interval is greater than zero wait for that long
137122
if (renewIntervalAdjusted > TimeSpan.Zero)
138123
{
139-
await Task.Delay(RenewInterval - renewOffset.Elapsed, token);
124+
await Task.Delay(renewIntervalAdjusted, token);
140125
}
141126
}
142127
catch (OperationCanceledException)

leader-election/DistributedMutex/BlobLeaseManager.cs

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ internal class BlobLeaseManager
3838
private readonly BlobContainerClient leaseContainerClient;
3939
private readonly PageBlobClient leaseBlobClient;
4040

41+
private const int LeaseAcquireTimeoutSeconds = 15;
42+
private const int LeaseAlreadyPresentStatusCode = 412;
43+
4144
public BlobLeaseManager(BlobSettings settings)
4245
: this(settings.BlobServiceClient, settings.Container, settings.BlobName)
4346
{
@@ -48,7 +51,15 @@ public BlobLeaseManager(BlobServiceClient blobServiceClient, string leaseContain
4851
leaseContainerClient = blobServiceClient.GetBlobContainerClient(leaseContainerName);
4952
leaseBlobClient = leaseContainerClient.GetPageBlobClient(leaseBlobName);
5053
leaseContainerClient.CreateIfNotExists();
51-
leaseBlobClient.CreateIfNotExists(512);
54+
try
55+
{
56+
leaseBlobClient.CreateIfNotExists(512);
57+
}
58+
catch (RequestFailedException leaseAlreadyPresentException)
59+
{
60+
// There is currently a lease on the blob and no lease ID was specified in the request. Status=412. It throws an Exception if the lease exists and It is already taken.
61+
if (leaseAlreadyPresentException.Status != LeaseAlreadyPresentStatusCode) throw;
62+
}
5263
}
5364

5465
public void ReleaseLease(string leaseId)
@@ -71,15 +82,15 @@ public void ReleaseLease(string leaseId)
7182
try
7283
{
7384
var leaseClient = leaseBlobClient.GetBlobLeaseClient();
74-
var lease = await leaseClient.AcquireAsync(TimeSpan.FromSeconds(15), null, token);
85+
var lease = await leaseClient.AcquireAsync(TimeSpan.FromSeconds(LeaseAcquireTimeoutSeconds), null, token);
7586
return lease.Value.LeaseId;
7687
}
7788
catch (RequestFailedException storageException)
7889
{
7990
Trace.TraceError(storageException.ErrorCode);
8091

8192
var status = storageException.Status;
82-
if (status == (int) HttpStatusCode.NotFound)
93+
if (status == (int)HttpStatusCode.NotFound)
8394
{
8495
blobNotFound = true;
8596
}

leader-election/DistributedMutex/DistributedMutex.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net10.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77
</PropertyGroup>

leader-election/LeaderElectionConsoleWorker/LeaderElectionConsoleWorker.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
<PropertyGroup>
44
<OutputType>Exe</OutputType>
5-
<TargetFramework>net8.0</TargetFramework>
5+
<TargetFramework>net10.0</TargetFramework>
66
<ImplicitUsings>enable</ImplicitUsings>
77
<Nullable>enable</Nullable>
88
</PropertyGroup>
99
<ItemGroup>
1010
<PackageReference Include="Azure.Identity" Version="1.17.1" />
1111
<PackageReference Include="Azure.Storage.Blobs" Version="12.26.0" />
12-
<PackageReference Include="System.Configuration.ConfigurationManager" Version="9.0.10" />
12+
<PackageReference Include="System.Configuration.ConfigurationManager" Version="10.0.1" />
1313
</ItemGroup>
1414
<ItemGroup>
1515
<ProjectReference Include="..\DistributedMutex\DistributedMutex.csproj">

leader-election/LeaderElectionConsoleWorker/Program.cs

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class Program
1414
static async Task Main(string[] args)
1515
{
1616
// Create a new shared cancellation token source
17-
CancellationTokenSource source = new CancellationTokenSource();
17+
CancellationTokenSource source = new();
1818
CancellationToken token = source.Token;
1919

2020
// Get the connection string from app settings
@@ -25,13 +25,13 @@ static async Task Main(string[] args)
2525
return;
2626
}
2727
// Create a BlobSettings object with the connection string and the name of the blob to use for the lease
28-
BlobSettings blobSettings = new BlobSettings(
28+
BlobSettings blobSettings = new(
2929
storageUri,
3030
"leases",
3131
"leader");
3232

3333
// Get the current process ID for output
34-
var pid = System.Diagnostics.Process.GetCurrentProcess().Id;
34+
var pid = Environment.ProcessId;
3535

3636
// Start an async task that will wait for a keypress and cancel the token when a key is pressed
3737
var uiTask = Task.Run(async () =>
@@ -41,32 +41,32 @@ static async Task Main(string[] args)
4141
if (Console.KeyAvailable)
4242
{
4343
Console.ReadKey(true);
44-
Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss")}] Requesting shutdown.");
44+
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] Requesting shutdown.");
4545
source.Cancel();
4646
}
4747
await Task.Delay(500);
4848
}
49-
Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss")}] This process ({pid}) is shutting down.");
49+
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] This process ({pid}) is shutting down.");
5050

5151
});
5252

5353
// Create a new BlobDistributedMutex object with the BlobSettings object and a task
5454
// to run when the lease is acquired, and an action to run when the lease is not acquired.
55-
BlobDistributedMutex distributedMutex = new BlobDistributedMutex(
55+
BlobDistributedMutex distributedMutex = new(
5656
blobSettings,
57-
async (CancellationToken token) =>
57+
async token =>
5858
{
5959
while (!token.IsCancellationRequested)
6060
{
61-
Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss")}] This process ({pid}) is currently the leader. Press any key to exit.");
62-
await Task.Delay(15000);
61+
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] This process ({pid}) is currently the leader. Press any key to exit.");
62+
await Task.Delay(15000, token);
6363
}
64-
}, () => {
65-
Console.WriteLine($"[{DateTime.Now.ToString("HH:mm:ss")}] This process ({pid}) could not acquire lease. Retrying in 20 seconds. Press any key to exit.");
64+
}, async () => {
65+
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] This process ({pid}) could not acquire lease. Retrying in 20 seconds. Press any key to exit.");
6666
});
6767

6868
// Wait for completion of the DistributedMutex and the UI task before exiting
69-
await distributedMutex.RunTaskWhenMutexAcquired(token);
69+
await distributedMutex.RunTaskWhenMutexAcquiredAsync(token);
7070
await uiTask;
7171
}
7272
}

leader-election/Readme.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ Install the prerequisites and follow the steps to run the example and observe th
1212

1313
### Prerequisites
1414

15-
- [.NET 8 SDK](https://dotnet.microsoft.com/download/dotnet/8.0)
15+
- [.NET 10 SDK](https://dotnet.microsoft.com/download/dotnet/10.0)
1616
- [Azurite emulator for local Azure Storage development](https://learn.microsoft.com/azure/storage/common/storage-use-azurite) or an [Azure Storage Account](https://learn.microsoft.com/azure/storage/common/storage-account-create)
1717

1818
#### Optional
@@ -44,8 +44,8 @@ Install the prerequisites and follow the steps to run the example and observe th
4444
#### Running with Azurite storage emulator
4545

4646
The included `app.config` file is set up to use a local Azure Storage emulator. Open a new terminal window, navigate to an empty working directory for the Azurite data files, and start the emulator with the command `azurite`, or `npx azurite` if you installed via `npm`.
47-
Azure SDKs by DefaultAzureCredencials needs https, and azurite by default is http. Follow the instructions [here](https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage#azure-sdks).
48-
In `LeaderElectionConsoleWorker/app.config` you see `https://127.0.0.1:10000/devstoreaccount1`
47+
Azure SDKs by DefaultAzureCredential needs https, and Azurite by default is http. Follow the instructions [here](https://learn.microsoft.com/azure/storage/common/storage-use-azurite?tabs=visual-studio%2Cblob-storage#azure-sdks).
48+
In `LeaderElectionConsoleWorker/app.config` you see `https://127.0.0.1:10000/devstoreaccount1` (This is the Blob service endpoint Azurite exposes for the devstoreaccount1 account.)
4949

5050
#### Running with Azure Storage account
5151

0 commit comments

Comments
 (0)