Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 76 additions & 73 deletions README.md

Large diffs are not rendered by default.

82 changes: 82 additions & 0 deletions docs/DistributedLock.MongoDB.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
# DistributedLock.MongoDB

[Download the NuGet package](https://www.nuget.org/packages/DistributedLock.MongoDB) [![NuGet Status](http://img.shields.io/nuget/v/DistributedLock.MongoDB.svg?style=flat)](https://www.nuget.org/packages/DistributedLock.MongoDB/)

The DistributedLock.MongoDB package offers distributed locks based on [MongoDB](https://www.mongodb.com/). For example:

```C#
var client = new MongoClient("mongodb://localhost:27017");
var database = client.GetDatabase("myDatabase");
var @lock = new MongoDistributedLock("MyLockName", database);
await using (await @lock.AcquireAsync())
{
// I have the lock
}
```

## APIs

- The `MongoDistributedLock` class implements the `IDistributedLock` interface.
- The `MongoDistributedSynchronizationProvider` class implements the `IDistributedLockProvider` interface.

## Implementation notes

MongoDB-based locks use MongoDB's document upsert and update operations to implement distributed locking. The implementation works as follows:

1. **Acquisition**: Attempts to insert or update a document with the lock key and a unique lock ID.
2. **Extension**: Automatically extends the lock expiry while held to prevent timeout.
3. **Release**: Deletes the lock document when disposed.
4. **Expiry**: Locks automatically expire if not extended, allowing recovery from crashed processes.

MongoDB locks can be constructed with an `IMongoDatabase` and an optional collection name. If no collection name is specified, locks will be stored in a collection named `"DistributedLocks"`. The collection will automatically have an index created on the `expiresAt` field for efficient queries.

When using the provider pattern, you can create multiple locks with different names from the same provider:

```C#
var client = new MongoClient(connectionString);
var database = client.GetDatabase("myDatabase");
var provider = new MongoDistributedSynchronizationProvider(database);

var lock1 = provider.CreateLock("lock1");
var lock2 = provider.CreateLock("lock2");

await using (await lock1.AcquireAsync())
{
// Do work with lock1
}
```

**NOTE**: Lock extension happens automatically in the background while the lock is held. If lock extension fails (for example, due to network issues), the `HandleLostToken` will be signaled to notify you that the lock may have been lost.

## Options

In addition to specifying the name and database, several tuning options are available:

- `Expiry` determines how long the lock will be initially claimed for. Because of automatic extension, locks can be held for longer than this value. Defaults to 30 seconds.
- `ExtensionCadence` determines how frequently the hold on the lock will be renewed to the full `Expiry`. Defaults to 1/3 of `Expiry` (approximately 10 seconds when using the default expiry).
- `BusyWaitSleepTime` specifies a range of times that the implementation will sleep between attempts to acquire a lock that is currently held by someone else. A random time in the range will be chosen for each sleep. If you expect contention, lowering these values may increase responsiveness (how quickly a lock detects that it can now be taken) but will increase the number of calls made to MongoDB. Raising the values will have the reverse effects. Defaults to a range of 10ms to 800ms.

Example of using options:

```C#
var @lock = new MongoDistributedLock(
"MyLockName",
database,
options => options
.Expiry(TimeSpan.FromSeconds(30))
.ExtensionCadence(TimeSpan.FromSeconds(10))
.BusyWaitSleepTime(
min: TimeSpan.FromMilliseconds(10),
max: TimeSpan.FromMilliseconds(800))
);
```

You can also specify a custom collection name:

```C#
var @lock = new MongoDistributedLock("MyLockName", database, "MyCustomLocks");
```

## Stale lock cleanup

Stale locks from crashed processes will automatically expire based on the `Expiry` setting. MongoDB's built-in TTL index support ensures that expired lock documents are cleaned up automatically by the database. This means that if a process crashes while holding a lock, the lock will become available again after the expiry time has elapsed.
1 change: 1 addition & 0 deletions src/Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<PackageVersion Include="Microsoft.SourceLink.GitHub" Version="8.0.0" />
<PackageVersion Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" Version="3.3.4" />
<PackageVersion Include="Azure.Storage.Blobs" Version="12.19.1" />
<PackageVersion Include="MongoDB.Driver" Version="3.5.2" />
<PackageVersion Include="Nullable" Version="1.3.1" Condition="'$(TargetFramework)' != 'netstandard2.1'" />
<PackageVersion Include="MySqlConnector" Version="2.3.5" />
<PackageVersion Include="NUnit.Analyzers" Version="4.1.0" />
Expand Down
1 change: 1 addition & 0 deletions src/DistributedLock.Core/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,5 @@
[assembly: InternalsVisibleTo("DistributedLock.ZooKeeper, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
[assembly: InternalsVisibleTo("DistributedLock.MySql, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
[assembly: InternalsVisibleTo("DistributedLock.Oracle, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
[assembly: InternalsVisibleTo("DistributedLock.MongoDB, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
#endif
30 changes: 3 additions & 27 deletions src/DistributedLock.Core/packages.lock.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,6 @@
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"Microsoft.CodeAnalysis.PublicApiAnalyzers": {
"type": "Direct",
"requested": "[3.3.4, )",
"resolved": "3.3.4",
"contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
},
"Microsoft.NETFramework.ReferenceAssemblies": {
"type": "Direct",
"requested": "[1.0.3, )",
Expand Down Expand Up @@ -81,12 +75,6 @@
"System.Threading.Tasks.Extensions": "4.5.4"
}
},
"Microsoft.CodeAnalysis.PublicApiAnalyzers": {
"type": "Direct",
"requested": "[3.3.4, )",
"resolved": "3.3.4",
"contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[8.0.0, )",
Expand Down Expand Up @@ -136,12 +124,6 @@
}
},
".NETStandard,Version=v2.1": {
"Microsoft.CodeAnalysis.PublicApiAnalyzers": {
"type": "Direct",
"requested": "[3.3.4, )",
"resolved": "3.3.4",
"contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[8.0.0, )",
Expand All @@ -164,17 +146,11 @@
}
},
"net8.0": {
"Microsoft.CodeAnalysis.PublicApiAnalyzers": {
"type": "Direct",
"requested": "[3.3.4, )",
"resolved": "3.3.4",
"contentHash": "kNLTfXtXUWDHVt5iaPkkiPuyHYlMgLI6SOFT4w88bfeI2vqSeGgHunFkdvlaCM8RDfcY0t2+jnesQtidRJJ/DA=="
},
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[8.0.4, )",
"resolved": "8.0.4",
"contentHash": "PZb5nfQ+U19nhnmnR9T1jw+LTmozhuG2eeuzuW5A7DqxD/UXW2ucjmNJqnqOuh8rdPzM3MQXoF8AfFCedJdCUw=="
"requested": "[8.0.22, )",
"resolved": "8.0.22",
"contentHash": "MhcMithKEiyyNkD2ZfbDZPmcOdi0GheGfg8saEIIEfD/fol3iHmcV8TsZkD4ZYz5gdUuoX4YtlVySUU7Sxl9SQ=="
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
Expand Down
5 changes: 5 additions & 0 deletions src/DistributedLock.MongoDB/AssemblyAttributes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
using System.Runtime.CompilerServices;

[assembly:
InternalsVisibleTo(
"DistributedLock.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fd3af56ccc8ed94fffe25bfd651e6a5674f8f20a76d37de800dd0f7380e04f0fde2da6fa200380b14fe398605b6f470c87e5e0a0bf39ae871f07536a4994aa7a0057c4d3bcedc8fef3eecb0c88c2024a1b3289305c2393acd9fb9f9a42d0bd7826738ce864d507575ea3a1fe1746ab19823303269f79379d767949807f494be8")]
62 changes: 62 additions & 0 deletions src/DistributedLock.MongoDB/DistributedLock.MongoDB.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.1;net8.0;net472;</TargetFrameworks>
<RootNamespace>Medallion.Threading.MongoDB</RootNamespace>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<WarningLevel>4</WarningLevel>
<LangVersion>Latest</LangVersion>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<PropertyGroup>
<Version>1.3.0</Version>
<AssemblyVersion>1.0.0.0</AssemblyVersion>
<Authors>Michael Adelson, joesdu</Authors>
<Description>Provides a distributed lock implementation based on MongoDB</Description>
<Copyright>Copyright © 2020 Michael Adelson</Copyright>
<PackageLicenseExpression>MIT</PackageLicenseExpression>
<PackageTags>distributed lock async mongodb</PackageTags>
<PackageProjectUrl>https://github.com/madelson/DistributedLock</PackageProjectUrl>
<RepositoryUrl>https://github.com/madelson/DistributedLock</RepositoryUrl>
<FileVersion>1.0.0.0</FileVersion>
<PackageReleaseNotes>See https://github.com/madelson/DistributedLock#release-notes</PackageReleaseNotes>
<SignAssembly>true</SignAssembly>
<AssemblyOriginatorKeyFile>..\DistributedLock.snk</AssemblyOriginatorKeyFile>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Release'">
<Optimize>True</Optimize>
<GeneratePackageOnBuild>True</GeneratePackageOnBuild>
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<TreatSpecificWarningsAsErrors />
<!-- see https://github.com/dotnet/sdk/issues/2679 -->
<DebugType>embedded</DebugType>
<!-- see https://mitchelsellers.com/blog/article/net-5-deterministic-builds-source-linking -->
<ContinuousIntegrationBuild>true</ContinuousIntegrationBuild>
<EmbedUntrackedSources>true</EmbedUntrackedSources>
</PropertyGroup>

<PropertyGroup Condition="'$(Configuration)' == 'Debug'">
<Optimize>False</Optimize>
<NoWarn>1591</NoWarn>
<DefineConstants>TRACE;DEBUG</DefineConstants>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="MongoDB.Driver" />
<PackageReference Include="Microsoft.SourceLink.GitHub" PrivateAssets="All" />
<PackageReference Include="Microsoft.CodeAnalysis.PublicApiAnalyzers" PrivateAssets="All" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\DistributedLock.Core\DistributedLock.Core.csproj" />
</ItemGroup>

<ItemGroup>
<Using Remove="System.Net.Http" />
</ItemGroup>

<Import Project="..\FixDistributedLockCoreDependencyVersion.targets" />
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
using Medallion.Threading.Internal;

namespace Medallion.Threading.MongoDB;

public partial class MongoDistributedLock
{
// AUTO-GENERATED

IDistributedSynchronizationHandle? IDistributedLock.TryAcquire(TimeSpan timeout, CancellationToken cancellationToken) =>
this.TryAcquire(timeout, cancellationToken);
IDistributedSynchronizationHandle IDistributedLock.Acquire(TimeSpan? timeout, CancellationToken cancellationToken) =>
this.Acquire(timeout, cancellationToken);
ValueTask<IDistributedSynchronizationHandle?> IDistributedLock.TryAcquireAsync(TimeSpan timeout, CancellationToken cancellationToken) =>
this.TryAcquireAsync(timeout, cancellationToken).Convert(To<IDistributedSynchronizationHandle?>.ValueTask);
ValueTask<IDistributedSynchronizationHandle> IDistributedLock.AcquireAsync(TimeSpan? timeout, CancellationToken cancellationToken) =>
this.AcquireAsync(timeout, cancellationToken).Convert(To<IDistributedSynchronizationHandle>.ValueTask);

/// <summary>
/// Attempts to acquire the lock synchronously. Usage:
/// <code>
/// using (var handle = myLock.TryAcquire(...))
/// {
/// if (handle != null) { /* we have the lock! */ }
/// }
/// // dispose releases the lock if we took it
/// </code>
/// </summary>
/// <param name="timeout">How long to wait before giving up on the acquisition attempt. Defaults to 0</param>
/// <param name="cancellationToken">Specifies a token by which the wait can be canceled</param>
/// <returns>A <see cref="MongoDistributedLockHandle"/> which can be used to release the lock or null on failure</returns>
public MongoDistributedLockHandle? TryAcquire(TimeSpan timeout = default, CancellationToken cancellationToken = default) =>
DistributedLockHelpers.TryAcquire(this, timeout, cancellationToken);

/// <summary>
/// Acquires the lock synchronously, failing with <see cref="TimeoutException"/> if the attempt times out. Usage:
/// <code>
/// using (myLock.Acquire(...))
/// {
/// /* we have the lock! */
/// }
/// // dispose releases the lock
/// </code>
/// </summary>
/// <param name="timeout">How long to wait before giving up on the acquisition attempt. Defaults to <see cref="Timeout.InfiniteTimeSpan"/></param>
/// <param name="cancellationToken">Specifies a token by which the wait can be canceled</param>
/// <returns>A <see cref="MongoDistributedLockHandle"/> which can be used to release the lock</returns>
public MongoDistributedLockHandle Acquire(TimeSpan? timeout = null, CancellationToken cancellationToken = default) =>
DistributedLockHelpers.Acquire(this, timeout, cancellationToken);

/// <summary>
/// Attempts to acquire the lock asynchronously. Usage:
/// <code>
/// await using (var handle = await myLock.TryAcquireAsync(...))
/// {
/// if (handle != null) { /* we have the lock! */ }
/// }
/// // dispose releases the lock if we took it
/// </code>
/// </summary>
/// <param name="timeout">How long to wait before giving up on the acquisition attempt. Defaults to 0</param>
/// <param name="cancellationToken">Specifies a token by which the wait can be canceled</param>
/// <returns>A <see cref="MongoDistributedLockHandle"/> which can be used to release the lock or null on failure</returns>
public ValueTask<MongoDistributedLockHandle?> TryAcquireAsync(TimeSpan timeout = default, CancellationToken cancellationToken = default) =>
this.As<IInternalDistributedLock<MongoDistributedLockHandle>>().InternalTryAcquireAsync(timeout, cancellationToken);

/// <summary>
/// Acquires the lock asynchronously, failing with <see cref="TimeoutException"/> if the attempt times out. Usage:
/// <code>
/// await using (await myLock.AcquireAsync(...))
/// {
/// /* we have the lock! */
/// }
/// // dispose releases the lock
/// </code>
/// </summary>
/// <param name="timeout">How long to wait before giving up on the acquisition attempt. Defaults to <see cref="Timeout.InfiniteTimeSpan"/></param>
/// <param name="cancellationToken">Specifies a token by which the wait can be canceled</param>
/// <returns>A <see cref="MongoDistributedLockHandle"/> which can be used to release the lock</returns>
public ValueTask<MongoDistributedLockHandle> AcquireAsync(TimeSpan? timeout = null, CancellationToken cancellationToken = default) =>
DistributedLockHelpers.AcquireAsync(this, timeout, cancellationToken);
}
Loading