From c4f4303c857b03c315102ae562a476b89b1ca6ec Mon Sep 17 00:00:00 2001 From: Dominic Hickie Date: Thu, 18 Jul 2024 13:52:29 +0100 Subject: [PATCH 1/4] Add DynamoDB distributed lock implementation --- Brighter.sln | 2 + Directory.Packages.props | 1 + .../DynamoDbLockingProvider.cs | 139 ++++++++++++++++++ .../DynamoDbLockingProviderOptions.cs | 47 ++++++ .../LockItem.cs | 47 ++++++ .../Paramore.Brighter.Locking.DynamoDB.csproj | 20 +++ .../Locking/DynamoDBLockingBaseTest.cs | 59 ++++++++ .../Locking/When_releasing_a_lock.cs | 79 ++++++++++ .../Locking/When_there_is_an_existing_lock.cs | 44 ++++++ .../Locking/When_there_is_no_existing_lock.cs | 75 ++++++++++ .../Paramore.Brighter.DynamoDB.Tests.csproj | 4 +- 11 files changed, 516 insertions(+), 1 deletion(-) create mode 100644 src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProvider.cs create mode 100644 src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProviderOptions.cs create mode 100644 src/Paramore.Brighter.Locking.DynamoDB/LockItem.cs create mode 100644 src/Paramore.Brighter.Locking.DynamoDB/Paramore.Brighter.Locking.DynamoDB.csproj create mode 100644 tests/Paramore.Brighter.DynamoDB.Tests/Locking/DynamoDBLockingBaseTest.cs create mode 100644 tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_releasing_a_lock.cs create mode 100644 tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_there_is_an_existing_lock.cs create mode 100644 tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_there_is_no_existing_lock.cs diff --git a/Brighter.sln b/Brighter.sln index be56a53c4..1ebb392dd 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -347,6 +347,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.ServiceAc EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Locking.Azure", "Paramore.Brighter.Locking.Azure\Paramore.Brighter.Locking.Azure.csproj", "{021F3B51-A640-4C0D-9B47-FB4E32DF6715}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.Locking.DynamoDB", "src\Paramore.Brighter.Locking.DynamoDB\Paramore.Brighter.Locking.DynamoDB.csproj", "{CBF99394-E332-439B-8632-ABDE06F6E343}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/Directory.Packages.props b/Directory.Packages.props index d8b532dd7..faf59ba2e 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -43,6 +43,7 @@ + diff --git a/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProvider.cs b/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProvider.cs new file mode 100644 index 000000000..c2f85299d --- /dev/null +++ b/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProvider.cs @@ -0,0 +1,139 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Dominic Hickie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ +#endregion +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.Model; + +namespace Paramore.Brighter.Locking.DynamoDb +{ + public class DynamoDbLockingProvider : IDistributedLock + { + private readonly IAmazonDynamoDB _dynamoDb; + private readonly DynamoDbLockingProviderOptions _options; + private readonly TimeProvider _timeProvider; + + public DynamoDbLockingProvider(IAmazonDynamoDB dynamoDb, DynamoDbLockingProviderOptions options) + :this(dynamoDb, options, TimeProvider.System) + { + } + + public DynamoDbLockingProvider(IAmazonDynamoDB dynamoDb, DynamoDbLockingProviderOptions options, TimeProvider timeProvider) + { + _dynamoDb = dynamoDb; + _options = options; + _timeProvider = timeProvider; + } + + /// + /// Attempt to obtain a lock on a resource + /// + /// The name of the resource to Lock + /// The Cancellation Token + /// The id of the lock that has been acquired or null if no lock was able to be acquired + public async Task ObtainLockAsync(string resource, CancellationToken cancellationToken = default) + { + var lockId = Guid.NewGuid().ToString(); + + try + { + await _dynamoDb.PutItemAsync(BuildLockRequest(resource, lockId), cancellationToken); + } + catch (ConditionalCheckFailedException) + { + return null; + } + + return lockId; + } + + /// + /// Release a lock + /// + /// The name of the resource to Lock + /// The lock Id that was provided when the lock was obtained + /// + /// Awaitable Task + public async Task ReleaseLockAsync(string resource, string lockId, CancellationToken cancellationToken = default) + { + if (_options.ManuallyReleaseLock) + { + try + { + await _dynamoDb.DeleteItemAsync(BuildReleaseRequest(resource, lockId), cancellationToken); + } + catch (ConditionalCheckFailedException) + { + // Log that the lease is no longer valid + } + } + } + + private PutItemRequest BuildLockRequest(string resource, string lockId) + { + var now = _timeProvider.GetUtcNow(); + var leaseExpiry = now.Add(_options.LeaseValidity); + + return new PutItemRequest + { + TableName = _options.LockTableName, + Item = new Dictionary + { + {"ResourceId", new AttributeValue{ S = $"{_options.LeaseholderGroupId}_{resource}"} }, + {"LeaseExpiry", new AttributeValue{ N = leaseExpiry.ToUnixTimeMilliseconds().ToString()} }, + {"LockId", new AttributeValue{S = lockId} } + }, + ConditionExpression = "attribute_not_exists(#r) OR (attribute_exists(#r) AND #e <= :t)", + ExpressionAttributeNames = new Dictionary + { + {"#r", "LockId"}, + {"#e", "LeaseExpiry"} + }, + ExpressionAttributeValues = new Dictionary + { + {":t", new AttributeValue {N = now.ToUnixTimeMilliseconds().ToString()} } + } + }; + } + + private DeleteItemRequest BuildReleaseRequest(string resource, string leaseId) + { + return new DeleteItemRequest + { + TableName = _options.LockTableName, + Key = new Dictionary + { + {"ResourceId", new AttributeValue{S = $"{_options.LeaseholderGroupId}_{resource}"} } + }, + ConditionExpression = "attribute_exists(#r) AND #l = :l", + ExpressionAttributeNames = new Dictionary + { + {"#r", "ResourceId"}, + {"#l", "LockId"} + }, + ExpressionAttributeValues = new Dictionary + { + {":l", new AttributeValue{S = leaseId} } + } + }; + } + } +} diff --git a/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProviderOptions.cs b/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProviderOptions.cs new file mode 100644 index 000000000..0e8d43fad --- /dev/null +++ b/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProviderOptions.cs @@ -0,0 +1,47 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Dominic Hickie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ +#endregion +namespace Paramore.Brighter.Locking.DynamoDb +{ + public class DynamoDbLockingProviderOptions(string lockTableName, string leaseholderGroupId) + { + /// + /// The name of the dynamo DB table containing locks + /// + public string LockTableName { get; init; } = lockTableName; + + /// + /// The ID of the group of potential leaseholders that share the lock + /// + public string LeaseholderGroupId { get; init; } = leaseholderGroupId; + + /// + /// The amount of time before the lease automatically expires + /// + public TimeSpan LeaseValidity { get; set; } = TimeSpan.FromMinutes(1); + + /// + /// Whether the lock provider should manually release the lock on completion or simply wait for expiry + /// + public bool ManuallyReleaseLock { get; set; } = false; + } +} diff --git a/src/Paramore.Brighter.Locking.DynamoDB/LockItem.cs b/src/Paramore.Brighter.Locking.DynamoDB/LockItem.cs new file mode 100644 index 000000000..e0b8a4001 --- /dev/null +++ b/src/Paramore.Brighter.Locking.DynamoDB/LockItem.cs @@ -0,0 +1,47 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Dominic Hickie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ +#endregion +using Amazon.DynamoDBv2.DataModel; + +namespace Paramore.Brighter.Locking.DynamoDB +{ + [DynamoDBTable("brighter_distributed_lock")] + public class LockItem + { + /// + /// The ID of the resource being locked + /// + [DynamoDBHashKey] + [DynamoDBProperty] + public string? ResourceId { get; init; } + + /// + /// The time at which the lease for the lock expires, as a unix timestamp in milliseconds + /// + public long LeaseExpiry { get; init; } + + /// + /// The ID of the lock that has been obtained + /// + public string? LockId { get; init; } + } +} diff --git a/src/Paramore.Brighter.Locking.DynamoDB/Paramore.Brighter.Locking.DynamoDB.csproj b/src/Paramore.Brighter.Locking.DynamoDB/Paramore.Brighter.Locking.DynamoDB.csproj new file mode 100644 index 000000000..457c527ad --- /dev/null +++ b/src/Paramore.Brighter.Locking.DynamoDB/Paramore.Brighter.Locking.DynamoDB.csproj @@ -0,0 +1,20 @@ + + + + net8.0 + enable + enable + This is the Dynamo DB Distributed Locking Provider. + Dominic Hickie + true + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Locking/DynamoDBLockingBaseTest.cs b/tests/Paramore.Brighter.DynamoDB.Tests/Locking/DynamoDBLockingBaseTest.cs new file mode 100644 index 000000000..fd0a67a22 --- /dev/null +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Locking/DynamoDBLockingBaseTest.cs @@ -0,0 +1,59 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Amazon.DynamoDBv2; +using Amazon.DynamoDBv2.DataModel; +using Amazon.DynamoDBv2.Model; +using Amazon.Runtime; +using Paramore.Brighter.DynamoDb; +using Paramore.Brighter.Locking.DynamoDB; +using Paramore.Brighter.Outbox.DynamoDB; + +namespace Paramore.Brighter.DynamoDB.Tests.Locking +{ + public class DynamoDBLockingBaseTest + { + protected DynamoDbTableBuilder DbTableBuilder { get; } + protected string LockTableName { get; } + protected AWSCredentials Credentials { get; set; } + protected IAmazonDynamoDB Client { get; } + private DynamoDBContext _dynamoDBContext; + + protected DynamoDBLockingBaseTest() + { + Client = CreateClient(); + _dynamoDBContext = new DynamoDBContext(Client); + DbTableBuilder = new DynamoDbTableBuilder(Client); + + // Create the lock table + var createTableRequest = new DynamoDbTableFactory().GenerateCreateTableRequest( + new DynamoDbCreateProvisionedThroughput( + new ProvisionedThroughput { ReadCapacityUnits = 10, WriteCapacityUnits = 10 } + )); + LockTableName = createTableRequest.TableName; + + (bool exist, IEnumerable _) hasTables = DbTableBuilder.HasTables([LockTableName]).GetAwaiter().GetResult(); + if (!hasTables.exist) + { + DbTableBuilder.Build(createTableRequest).GetAwaiter().GetResult(); + DbTableBuilder.EnsureTablesReady([createTableRequest.TableName], TableStatus.ACTIVE).GetAwaiter().GetResult(); + } + } + + protected async Task GetLockItem(string resourceId) + { + return await _dynamoDBContext.LoadAsync(resourceId); + } + + private IAmazonDynamoDB CreateClient() + { + Credentials = new BasicAWSCredentials("FakeAccessKey", "FakeSecretKey"); + + var clientConfig = new AmazonDynamoDBConfig + { + ServiceURL = "http://localhost:8000" + }; + + return new AmazonDynamoDBClient(Credentials, clientConfig); + } + } +} diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_releasing_a_lock.cs b/tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_releasing_a_lock.cs new file mode 100644 index 000000000..cb8a01ecc --- /dev/null +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_releasing_a_lock.cs @@ -0,0 +1,79 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Locking.DynamoDb; +using Xunit; + +namespace Paramore.Brighter.DynamoDB.Tests.Locking +{ + [Trait("Category", "DynamoDB")] + public class DynamoDbReleasingLockTests : DynamoDBLockingBaseTest + { + private readonly DynamoDbLockingProviderOptions _options; + private readonly FakeTimeProvider _timeProvider; + + public DynamoDbReleasingLockTests() + { + _options = new DynamoDbLockingProviderOptions("brighter_distributed_lock", Guid.NewGuid().ToString()); + _timeProvider = new FakeTimeProvider(); + } + + [Fact] + public async Task When_manual_lock_release_is_enabled() + { + _options.ManuallyReleaseLock = true; + var provider = new DynamoDbLockingProvider(Client, _options, _timeProvider); + + var resource = Guid.NewGuid().ToString(); + var result = await provider.ObtainLockAsync(resource); + result.Should().NotBeNull(); + + var lockItem = await GetLockItem($"{_options.LeaseholderGroupId}_{resource}"); + lockItem.Should().NotBeNull(); + + await provider.ReleaseLockAsync(resource, result); + lockItem = await GetLockItem($"{_options.LeaseholderGroupId}_{resource}"); + lockItem.Should().BeNull(); + } + + [Fact] + public async Task When_manual_lock_release_is_disabled() + { + _options.ManuallyReleaseLock = false; + var provider = new DynamoDbLockingProvider(Client, _options, _timeProvider); + + var resource = Guid.NewGuid().ToString(); + var result = await provider.ObtainLockAsync(resource); + result.Should().NotBeNull(); + + var lockItem = await GetLockItem($"{_options.LeaseholderGroupId}_{resource}"); + lockItem.Should().NotBeNull(); + + await provider.ReleaseLockAsync(resource, result); + lockItem = await GetLockItem($"{_options.LeaseholderGroupId}_{resource}"); + lockItem.Should().NotBeNull(); + } + + [Fact] + public async Task When_one_of_multiple_locks_is_released() + { + _options.ManuallyReleaseLock = true; + var provider = new DynamoDbLockingProvider(Client, _options, _timeProvider); + + var resourceA = Guid.NewGuid().ToString(); + var resultA = await provider.ObtainLockAsync(resourceA); + resultA.Should().NotBeNull(); + + var resourceB = Guid.NewGuid().ToString(); + var resultB = await provider.ObtainLockAsync(resourceB); + resultB.Should().NotBeNull(); + + await provider.ReleaseLockAsync(resourceA, resultA); + var lockItemA = await GetLockItem($"{_options.LeaseholderGroupId}_{resourceA}"); + var lockItemB = await GetLockItem($"{_options.LeaseholderGroupId}_{resourceB}"); + lockItemA.Should().BeNull(); + lockItemB.Should().NotBeNull(); + } + } +} diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_there_is_an_existing_lock.cs b/tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_there_is_an_existing_lock.cs new file mode 100644 index 000000000..09e3f3833 --- /dev/null +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_there_is_an_existing_lock.cs @@ -0,0 +1,44 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Locking.DynamoDb; +using Xunit; + +namespace Paramore.Brighter.DynamoDB.Tests.Locking +{ + [Trait("Category", "DynamoDB")] + public class DynamoDbExistingLockTests : DynamoDBLockingBaseTest + { + private readonly DynamoDbLockingProvider _provider; + private readonly DynamoDbLockingProviderOptions _options; + private readonly FakeTimeProvider _timeProvider; + + public DynamoDbExistingLockTests() + { + _options = new DynamoDbLockingProviderOptions("brighter_distributed_lock", Guid.NewGuid().ToString()); + _timeProvider = new FakeTimeProvider(); + _provider = new DynamoDbLockingProvider(Client, _options, _timeProvider); + } + + [Fact] + public async Task When_there_is_an_existing_lock_for_resource() + { + var startTime = _timeProvider.GetUtcNow(); + var resource = Guid.NewGuid().ToString(); + var resultA = await _provider.ObtainLockAsync(resource); + resultA.Should().NotBeNull(); + + _timeProvider.Advance(TimeSpan.FromSeconds(30)); + + var resultB = await _provider.ObtainLockAsync(resource); + resultB.Should().BeNull(); + + var lockItem = await GetLockItem($"{_options.LeaseholderGroupId}_{resource}"); + + lockItem.Should().NotBeNull(); + lockItem.LockId.Should().Be(resultA); + lockItem.LeaseExpiry.Should().Be(startTime.Add(_options.LeaseValidity).ToUnixTimeMilliseconds()); + } + } +} diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_there_is_no_existing_lock.cs b/tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_there_is_no_existing_lock.cs new file mode 100644 index 000000000..2d69d6651 --- /dev/null +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Locking/When_there_is_no_existing_lock.cs @@ -0,0 +1,75 @@ +using System; +using System.Threading.Tasks; +using FluentAssertions; +using Microsoft.Extensions.Time.Testing; +using Paramore.Brighter.Locking.DynamoDb; +using Xunit; + +namespace Paramore.Brighter.DynamoDB.Tests.Locking +{ + [Trait("Category", "DynamoDB")] + public class DynamoDbNoExistingLockTests : DynamoDBLockingBaseTest + { + private readonly DynamoDbLockingProvider _provider; + private readonly DynamoDbLockingProviderOptions _options; + private readonly FakeTimeProvider _timeProvider; + + public DynamoDbNoExistingLockTests() + { + _options = new DynamoDbLockingProviderOptions("brighter_distributed_lock", Guid.NewGuid().ToString()); + _timeProvider = new FakeTimeProvider(); + _provider = new DynamoDbLockingProvider(Client, _options, _timeProvider); + } + + [Fact] + public async Task When_there_is_no_existing_lock_for_resource() + { + var resource = Guid.NewGuid().ToString(); + var result = await _provider.ObtainLockAsync(resource); + + result.Should().NotBeNull(); + + var lockItem = await GetLockItem($"{_options.LeaseholderGroupId}_{resource}"); + + lockItem.Should().NotBeNull(); + lockItem.LockId.Should().Be(result); + lockItem.LeaseExpiry.Should().Be(_timeProvider.GetUtcNow().Add(_options.LeaseValidity).ToUnixTimeMilliseconds()); + } + + [Fact] + public async Task When_there_is_existing_expired_lock_for_resource() + { + var resource = Guid.NewGuid().ToString(); + var result = await _provider.ObtainLockAsync(resource); + result.Should().NotBeNull(); + + _timeProvider.Advance(TimeSpan.FromMinutes(5)); + result = await _provider.ObtainLockAsync(resource); + result.Should().NotBeNull(); + + var lockItem = await GetLockItem($"{_options.LeaseholderGroupId}_{resource}"); + + lockItem.Should().NotBeNull(); + lockItem.LockId.Should().Be(result); + lockItem.LeaseExpiry.Should().Be(_timeProvider.GetUtcNow().Add(_options.LeaseValidity).ToUnixTimeMilliseconds()); + } + + [Fact] + public async Task When_there_is_existing_lock_for_different_resource() + { + var resourceA = Guid.NewGuid().ToString(); + var result = await _provider.ObtainLockAsync(resourceA); + result.Should().NotBeNull(); + + var resourceB = Guid.NewGuid().ToString(); + result = await _provider.ObtainLockAsync(resourceB); + result.Should().NotBeNull(); + + var lockItem = await GetLockItem($"{_options.LeaseholderGroupId}_{resourceB}"); + + lockItem.Should().NotBeNull(); + lockItem.LockId.Should().Be(result); + lockItem.LeaseExpiry.Should().Be(_timeProvider.GetUtcNow().Add(_options.LeaseValidity).ToUnixTimeMilliseconds()); + } + } +} diff --git a/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj b/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj index 3fb8905d9..791e96aa8 100644 --- a/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj +++ b/tests/Paramore.Brighter.DynamoDB.Tests/Paramore.Brighter.DynamoDB.Tests.csproj @@ -1,4 +1,4 @@ - + net8.0 @@ -12,6 +12,7 @@ runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -22,6 +23,7 @@ + From 48d8de95d54d76d6547022cf2effcf2150410a4c Mon Sep 17 00:00:00 2001 From: Dominic Hickie Date: Fri, 19 Jul 2024 09:28:28 +0100 Subject: [PATCH 2/4] Keeping visual studio happy --- Brighter.sln | 84 +++++++++++++++++++--------------------------------- 1 file changed, 30 insertions(+), 54 deletions(-) diff --git a/Brighter.sln b/Brighter.sln index 1ebb392dd..ccddc7534 100644 --- a/Brighter.sln +++ b/Brighter.sln @@ -335,17 +335,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GreetingsSender", "samples\ EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GreetingsReceiverConsole", "samples\AWSTransfomers\Compression\GreetingsReceiverConsole\GreetingsReceiverConsole.csproj", "{18742337-075A-40D6-B67F-91F5894A50C3}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Transformers.Azure", "src\Paramore.Brighter.Transformers.Azure\Paramore.Brighter.Transformers.Azure.csproj", "{29FAAF3E-504D-472F-91F6-14A41B897912}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.Transformers.Azure", "src\Paramore.Brighter.Transformers.Azure\Paramore.Brighter.Transformers.Azure.csproj", "{29FAAF3E-504D-472F-91F6-14A41B897912}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Azure.Tests", "tests\Paramore.Brighter.Azure.Tests\Paramore.Brighter.Azure.Tests.csproj", "{AA2AA086-9B8A-4910-A793-E92B1E352351}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.Azure.Tests", "tests\Paramore.Brighter.Azure.Tests\Paramore.Brighter.Azure.Tests.csproj", "{AA2AA086-9B8A-4910-A793-E92B1E352351}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Archive.Azure", "src\Paramore.Brighter.Archive.Azure\Paramore.Brighter.Archive.Azure.csproj", "{F329B6C6-40C2-45BA-A2A8-276ACAFA1867}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.Archive.Azure", "src\Paramore.Brighter.Archive.Azure\Paramore.Brighter.Archive.Azure.csproj", "{F329B6C6-40C2-45BA-A2A8-276ACAFA1867}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.ServiceActivator.Control", "src\Paramore.Brighter.ServiceActivator.Control\Paramore.Brighter.ServiceActivator.Control.csproj", "{4ACA8480-16A0-4BC8-8401-4A27E5AEC1BE}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.ServiceActivator.Control", "src\Paramore.Brighter.ServiceActivator.Control\Paramore.Brighter.ServiceActivator.Control.csproj", "{4ACA8480-16A0-4BC8-8401-4A27E5AEC1BE}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.ServiceActivator.Control.Api", "src\Paramore.Brighter.ServiceActivator.Control.Api\Paramore.Brighter.ServiceActivator.Control.Api.csproj", "{397F8496-6916-43EF-AEB2-5D84048DE357}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.ServiceActivator.Control.Api", "src\Paramore.Brighter.ServiceActivator.Control.Api\Paramore.Brighter.ServiceActivator.Control.Api.csproj", "{397F8496-6916-43EF-AEB2-5D84048DE357}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Paramore.Brighter.Locking.Azure", "Paramore.Brighter.Locking.Azure\Paramore.Brighter.Locking.Azure.csproj", "{021F3B51-A640-4C0D-9B47-FB4E32DF6715}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.Locking.Azure", "Paramore.Brighter.Locking.Azure\Paramore.Brighter.Locking.Azure.csproj", "{021F3B51-A640-4C0D-9B47-FB4E32DF6715}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Paramore.Brighter.Locking.DynamoDB", "src\Paramore.Brighter.Locking.DynamoDB\Paramore.Brighter.Locking.DynamoDB.csproj", "{CBF99394-E332-439B-8632-ABDE06F6E343}" EndProject @@ -1907,30 +1907,6 @@ Global {18742337-075A-40D6-B67F-91F5894A50C3}.Release|Mixed Platforms.Build.0 = Release|Any CPU {18742337-075A-40D6-B67F-91F5894A50C3}.Release|x86.ActiveCfg = Release|Any CPU {18742337-075A-40D6-B67F-91F5894A50C3}.Release|x86.Build.0 = Release|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|Any CPU.Build.0 = Debug|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|x86.ActiveCfg = Debug|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|x86.Build.0 = Debug|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|Any CPU.ActiveCfg = Release|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|Any CPU.Build.0 = Release|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|x86.ActiveCfg = Release|Any CPU - {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|x86.Build.0 = Release|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Debug|x86.ActiveCfg = Debug|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Debug|x86.Build.0 = Debug|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Release|Any CPU.Build.0 = Release|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Release|x86.ActiveCfg = Release|Any CPU - {3BCE9BD6-B5A7-4962-8A95-7B83B458458A}.Release|x86.Build.0 = Release|Any CPU {29FAAF3E-504D-472F-91F6-14A41B897912}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {29FAAF3E-504D-472F-91F6-14A41B897912}.Debug|Any CPU.Build.0 = Debug|Any CPU {29FAAF3E-504D-472F-91F6-14A41B897912}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -1955,18 +1931,18 @@ Global {AA2AA086-9B8A-4910-A793-E92B1E352351}.Release|Mixed Platforms.Build.0 = Release|Any CPU {AA2AA086-9B8A-4910-A793-E92B1E352351}.Release|x86.ActiveCfg = Release|Any CPU {AA2AA086-9B8A-4910-A793-E92B1E352351}.Release|x86.Build.0 = Release|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Debug|Any CPU.Build.0 = Debug|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Debug|x86.ActiveCfg = Debug|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Debug|x86.Build.0 = Debug|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Release|Any CPU.ActiveCfg = Release|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Release|Any CPU.Build.0 = Release|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Release|x86.ActiveCfg = Release|Any CPU - {EC046F36-F93F-447A-86EA-F60585232867}.Release|x86.Build.0 = Release|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|x86.ActiveCfg = Debug|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Debug|x86.Build.0 = Debug|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|Any CPU.Build.0 = Release|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|x86.ActiveCfg = Release|Any CPU + {F329B6C6-40C2-45BA-A2A8-276ACAFA1867}.Release|x86.Build.0 = Release|Any CPU {4ACA8480-16A0-4BC8-8401-4A27E5AEC1BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4ACA8480-16A0-4BC8-8401-4A27E5AEC1BE}.Debug|Any CPU.Build.0 = Debug|Any CPU {4ACA8480-16A0-4BC8-8401-4A27E5AEC1BE}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -1991,18 +1967,6 @@ Global {397F8496-6916-43EF-AEB2-5D84048DE357}.Release|Mixed Platforms.Build.0 = Release|Any CPU {397F8496-6916-43EF-AEB2-5D84048DE357}.Release|x86.ActiveCfg = Release|Any CPU {397F8496-6916-43EF-AEB2-5D84048DE357}.Release|x86.Build.0 = Release|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Debug|x86.ActiveCfg = Debug|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Debug|x86.Build.0 = Debug|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Release|Any CPU.Build.0 = Release|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Release|Mixed Platforms.Build.0 = Release|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Release|x86.ActiveCfg = Release|Any CPU - {3384FBF0-5DCB-452D-8288-FAD1D0023089}.Release|x86.Build.0 = Release|Any CPU {021F3B51-A640-4C0D-9B47-FB4E32DF6715}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {021F3B51-A640-4C0D-9B47-FB4E32DF6715}.Debug|Any CPU.Build.0 = Debug|Any CPU {021F3B51-A640-4C0D-9B47-FB4E32DF6715}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU @@ -2015,6 +1979,18 @@ Global {021F3B51-A640-4C0D-9B47-FB4E32DF6715}.Release|Mixed Platforms.Build.0 = Release|Any CPU {021F3B51-A640-4C0D-9B47-FB4E32DF6715}.Release|x86.ActiveCfg = Release|Any CPU {021F3B51-A640-4C0D-9B47-FB4E32DF6715}.Release|x86.Build.0 = Release|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Debug|Any CPU.Build.0 = Debug|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Debug|Mixed Platforms.ActiveCfg = Debug|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Debug|Mixed Platforms.Build.0 = Debug|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Debug|x86.ActiveCfg = Debug|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Debug|x86.Build.0 = Debug|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Release|Any CPU.ActiveCfg = Release|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Release|Any CPU.Build.0 = Release|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Release|Mixed Platforms.ActiveCfg = Release|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Release|Mixed Platforms.Build.0 = Release|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Release|x86.ActiveCfg = Release|Any CPU + {CBF99394-E332-439B-8632-ABDE06F6E343}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE From b89be2d8f1fea3409ea762cf45a2546eb52eca41 Mon Sep 17 00:00:00 2001 From: Dominic Hickie Date: Thu, 18 Jul 2024 16:06:14 +0100 Subject: [PATCH 3/4] Add logging to locking provider (cherry picked from commit cd30ac98058dd209de9a764cd6a00686c7a0e032) --- .../DynamoDbLockingProvider.cs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProvider.cs b/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProvider.cs index c2f85299d..4c9c0b21c 100644 --- a/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProvider.cs +++ b/src/Paramore.Brighter.Locking.DynamoDB/DynamoDbLockingProvider.cs @@ -22,6 +22,8 @@ THE SOFTWARE. */ #endregion using Amazon.DynamoDBv2; using Amazon.DynamoDBv2.Model; +using Microsoft.Extensions.Logging; +using Paramore.Brighter.Logging; namespace Paramore.Brighter.Locking.DynamoDb { @@ -31,6 +33,8 @@ public class DynamoDbLockingProvider : IDistributedLock private readonly DynamoDbLockingProviderOptions _options; private readonly TimeProvider _timeProvider; + private static readonly ILogger s_logger = ApplicationLogging.CreateLogger(); + public DynamoDbLockingProvider(IAmazonDynamoDB dynamoDb, DynamoDbLockingProviderOptions options) :this(dynamoDb, options, TimeProvider.System) { @@ -59,9 +63,11 @@ public DynamoDbLockingProvider(IAmazonDynamoDB dynamoDb, DynamoDbLockingProvider } catch (ConditionalCheckFailedException) { + s_logger.LogInformation("Unable to obtain lock for resource {resource}, an existing lock is in place", resource); return null; } + s_logger.LogInformation("Obtained lock {lockId} for resource {resource}", lockId, resource); return lockId; } @@ -82,7 +88,7 @@ public async Task ReleaseLockAsync(string resource, string lockId, CancellationT } catch (ConditionalCheckFailedException) { - // Log that the lease is no longer valid + s_logger.LogInformation("Unable to release lock {lockId} for resource {resourceId} - lock has expired", lockId, resource); } } } From 00d883adf6780bc08b8ebe9d67d3c55bf5232692 Mon Sep 17 00:00:00 2001 From: Dominic Hickie Date: Fri, 19 Jul 2024 10:43:22 +0100 Subject: [PATCH 4/4] Add extension method for registering the dynamo DB locking provider --- .../Paramore.Brighter.Locking.DynamoDB.csproj | 1 + .../ServiceCollectionExtensions.cs | 74 +++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 src/Paramore.Brighter.Locking.DynamoDB/ServiceCollectionExtensions.cs diff --git a/src/Paramore.Brighter.Locking.DynamoDB/Paramore.Brighter.Locking.DynamoDB.csproj b/src/Paramore.Brighter.Locking.DynamoDB/Paramore.Brighter.Locking.DynamoDB.csproj index 457c527ad..74953c368 100644 --- a/src/Paramore.Brighter.Locking.DynamoDB/Paramore.Brighter.Locking.DynamoDB.csproj +++ b/src/Paramore.Brighter.Locking.DynamoDB/Paramore.Brighter.Locking.DynamoDB.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Paramore.Brighter.Locking.DynamoDB/ServiceCollectionExtensions.cs b/src/Paramore.Brighter.Locking.DynamoDB/ServiceCollectionExtensions.cs new file mode 100644 index 000000000..7b3d83f17 --- /dev/null +++ b/src/Paramore.Brighter.Locking.DynamoDB/ServiceCollectionExtensions.cs @@ -0,0 +1,74 @@ +#region Licence +/* The MIT License (MIT) +Copyright © 2024 Dominic Hickie + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the “Software”), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. */ +#endregion +using Amazon.DynamoDBv2; +using Microsoft.Extensions.DependencyInjection; +using Paramore.Brighter.Extensions.DependencyInjection; +using Paramore.Brighter.Locking.DynamoDb; + +namespace Paramore.Brighter.Locking.DynamoDB +{ + public static class ServiceCollectionExtensions + { + /// + /// Registers a DynamoDb distributed locking provider. This helper registers + /// - IDistributedLock + /// + /// You will need to register IAmazonDynamoDb BEFORE calling this extension + /// We do not register this, as we assume you will need to register them for your code's access to DynamoDb + /// So we assume that prerequisite has taken place beforehand + /// + /// An action for customising the lock config before registering the provider + /// The lifetime of the locking provider + /// + public static IBrighterBuilder UseDynamoDbDistributedLock(this IBrighterBuilder builder, + string lockTableName, + string leaseholderGroupId, + Action? configAction = null, + ServiceLifetime serviceLifetime = ServiceLifetime.Singleton) + { + var config = new DynamoDbLockingProviderOptions(lockTableName, leaseholderGroupId); + + if (configAction != null) + { + configAction(config); + } + + builder.Services.AddSingleton(config); + builder.Services.Add(new ServiceDescriptor(typeof(IDistributedLock), BuildLockingProvider, serviceLifetime)); + + return builder; + } + + private static DynamoDbLockingProvider BuildLockingProvider(IServiceProvider serviceProvider) + { + var config = serviceProvider.GetService(); + if (config == null) + throw new InvalidOperationException("No service of type DynamoDbLockingProviderOptions could be found, please register before calling this method"); + var dynamoDb = serviceProvider.GetService(); + if (dynamoDb == null) + throw new InvalidOperationException("No service of type IAmazonDynamoDb was found. Please register before calling this method"); + + return new DynamoDbLockingProvider(dynamoDb, config); + } + } +}