Skip to content
Merged
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
2 changes: 1 addition & 1 deletion Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project>
<PropertyGroup>
<VersionPrefix>1.1.1</VersionPrefix>
<VersionPrefix>1.1.2</VersionPrefix>
<PackageLicenseExpression>MIT</PackageLicenseExpression>

<!-- Other useful metadata -->
Expand Down
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,22 @@ Or bind from configuration:
services.AddDynamoDbDistributedLock(configuration);
```

If you need to customize the `IAmazonDynamoDB` client, you can pass in AWSOptions, or the configuration section name:

```csharp
services.AddDynamoDbDistributedLock(configuration, awsOptionsSectionName: "DynamoDb");
// or configure AWSOptions manually
var awsOptions = configuration.GetAWSOptions("DynamoDb");
awsOptions.DefaultClientConfig.ServiceURL = "http://localhost:4566"; // use localstack for testing
services.AddDynamoDbDistributedLock(options =>
{
options.TableName = "my-lock-table";
options.LockTimeoutSeconds = 30;
options.PartitionKeyAttribute = "pk";
options.SortKeyAttribute = "sk";
}, awsOptions);
```

### appsettings.json
```json
{
Expand Down
4 changes: 4 additions & 0 deletions src/DynamoDb.DistributedLock/DynamoDbLockOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ namespace DynamoDb.DistributedLock;
/// </summary>
public sealed class DynamoDbLockOptions
{
/// <summary>
/// The default name of the configuration section for DynamoDB lock settings.
/// </summary>
public const string DynamoDbLockSettings = "DynamoDbLock";

/// <summary>
/// The name of the DynamoDB table to use.
/// </summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using Amazon.DynamoDBv2;
using Amazon.Extensions.NETCore.Setup;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

Expand All @@ -16,25 +17,30 @@ public static class ServiceCollectionExtensions
/// <param name="services">The service collection to register with.</param>
/// <param name="configuration">The configuration source.</param>
/// <param name="sectionName">The name of the configuration section to bind to <see cref="DynamoDbLockOptions"/>.</param>
/// <param name="awsOptionsSectionName">The name of the configuration section to be read with GetAWSOptions</param>
/// <returns>The updated <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddDynamoDbDistributedLock(this IServiceCollection services,
IConfiguration configuration,
string sectionName = DynamoDbLockOptions.DynamoDbLockSettings)
string sectionName = DynamoDbLockOptions.DynamoDbLockSettings, string? awsOptionsSectionName = null)
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding these optional parameters breaks binary compatibility, but because this has an existing optional param of the same type, I couldn't think of a way to do the trick of manually making another set of methods, because they are ambiguous and one gets hidden.

I think this is probably OK, since in my experience binary breaks are not as big of a deal in modern dotnet + nuget, and if you did hit this by copying some old dll, you would see it immediately in startup.

{
return services.AddDynamoDbDistributedLock(options => configuration.GetSection(sectionName).Bind(options));
var awsOptions = awsOptionsSectionName is not null
? configuration.GetAWSOptions(awsOptionsSectionName)
: null;
return services.AddDynamoDbDistributedLock(options => configuration.GetSection(sectionName).Bind(options), awsOptions);
}

/// <summary>
/// Registers the DynamoDB distributed lock using a delegate to configure <see cref="DynamoDbLockOptions"/>.
/// </summary>
/// <param name="services">The service collection to register with.</param>
/// <param name="configure">The delegate to configure <see cref="DynamoDbLockOptions"/>.</param>
/// <param name="awsOptions">The AWSOptions to be used in configuring the dynamodb service client</param>
/// <returns>The updated <see cref="IServiceCollection"/>.</returns>
public static IServiceCollection AddDynamoDbDistributedLock(this IServiceCollection services,
Action<DynamoDbLockOptions> configure)
Action<DynamoDbLockOptions> configure, AWSOptions? awsOptions = null)
{
services.Configure(configure);
services.AddAWSService<IAmazonDynamoDB>();
services.AddAWSService<IAmazonDynamoDB>(awsOptions);
services.AddSingleton<IDynamoDbDistributedLock, DynamoDbDistributedLock>();
return services;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
using Amazon.DynamoDBv2;
using Amazon.Extensions.NETCore.Setup;
using Amazon.Runtime;
using DynamoDb.DistributedLock.Extensions;
using DynamoDb.DistributedLock.Tests.TestKit.Attributes;
using AwesomeAssertions;
Expand Down Expand Up @@ -120,4 +122,39 @@ public void AddDynamoDbDistributedLock_WithConfiguration_BindsCustomKeyAttribute
options.PartitionKeyAttribute.Should().Be(partitionKey);
options.SortKeyAttribute.Should().Be(sortKey);
}

[Fact]
public void AddDynamoDbDistributedLock_WithActionAndAwsConfig_SetsUpServiceAndOptions()
{
// Arrange
var services = new ServiceCollection();
var awsOptions = new AWSOptions
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The unit tests only cover the overload that passes in the object, not the config section, because in the other tests you override the dynamodb service in order to bypass credential resolution. I couldn't figure out a good way to make all that work outside of setting fake aws env vars, and if I did the same thing of registering a substitute, it wouldn't actually test anything.

So to increase coverage you could set aws access keys and secret env vars, but that makes me feel gross in tests so I didn't.

{
DefaultClientConfig = { ServiceURL = "http://localhost/" },
// to allow the service client to be resolved without actual AWS credentials
Credentials = new AnonymousAWSCredentials(),
};

// Act
services.AddDynamoDbDistributedLock(options =>
{
options.TableName = "locks";
options.LockTimeoutSeconds = 45;
}, awsOptions);

var provider = services.BuildServiceProvider();

// Assert
var lockService = provider.GetService<IDynamoDbDistributedLock>();
lockService.Should().NotBeNull();

var options = provider.GetRequiredService<IOptions<DynamoDbLockOptions>>().Value;
options.TableName.Should().Be("locks");
options.LockTimeoutSeconds.Should().Be(45);
options.PartitionKeyAttribute.Should().Be("pk");
options.SortKeyAttribute.Should().Be("sk");

var dynamoDbClient = provider.GetRequiredService<IAmazonDynamoDB>();
dynamoDbClient.Config.ServiceURL.Should().Be("http://localhost/");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -50,13 +50,11 @@ public async Task ExecuteAsync_WhenOperationSucceedsOnFirstAttempt_ShouldReturnR
var sut = new ExponentialBackoffRetryPolicy(options);
var operationCalled = 0;

var result = await sut.ExecuteAsync(
_ =>
var result = await sut.ExecuteAsync(_ =>
{
operationCalled++;
return Task.FromResult(expectedResult);
},
_ => true);
}, _ => true, TestContext.Current.CancellationToken);
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

xunit analyzer was warning about not using a cancellation token here, and recommended this one, so that if you cancel tests that hang they will have a better chance of failing fast and gracefully. This does slightly change the code paths being hit, but seems worth it.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

πŸ”₯Good catch!


result.Should().Be(expectedResult);
operationCalled.Should().Be(1);
Expand Down Expand Up @@ -120,15 +118,13 @@ public async Task ExecuteAsync_WhenOperationSucceedsAfterRetries_ShouldReturnRes
var sut = new ExponentialBackoffRetryPolicy(options);
var operationCalled = 0;

var result = await sut.ExecuteAsync(
_ =>
var result = await sut.ExecuteAsync(_ =>
{
operationCalled++;
if (operationCalled < 3)
throw new InvalidOperationException("Retry me");
return Task.FromResult(expectedResult);
},
_ => true);
}, _ => true, TestContext.Current.CancellationToken);

result.Should().Be(expectedResult);
operationCalled.Should().Be(3);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public async Task AcquireLockAsync_WhenRetryDisabled_ShouldNotRetryOnFailure(
var sut = new DynamoDbDistributedLock(dynamo, options);

// Act
var result = await sut.AcquireLockAsync(resourceId, ownerId);
var result = await sut.AcquireLockAsync(resourceId, ownerId, TestContext.Current.CancellationToken);

// Assert
result.Should().BeFalse();
Expand Down Expand Up @@ -64,7 +64,7 @@ public async Task AcquireLockAsync_WhenRetryEnabledAndEventuallySucceeds_ShouldR
var sut = new DynamoDbDistributedLock(dynamo, options);

// Act
var result = await sut.AcquireLockAsync(resourceId, ownerId);
var result = await sut.AcquireLockAsync(resourceId, ownerId, TestContext.Current.CancellationToken);

// Assert
result.Should().BeTrue();
Expand All @@ -90,7 +90,7 @@ public async Task AcquireLockAsync_WhenRetryEnabledButMaxAttemptsReached_ShouldR
var sut = new DynamoDbDistributedLock(dynamo, options);

// Act
var result = await sut.AcquireLockAsync(resourceId, ownerId);
var result = await sut.AcquireLockAsync(resourceId, ownerId, TestContext.Current.CancellationToken);

// Assert
result.Should().BeFalse();
Expand Down Expand Up @@ -123,7 +123,7 @@ public async Task AcquireLockAsync_WhenRetryEnabledWithThrottling_ShouldRetryOnP
var sut = new DynamoDbDistributedLock(dynamo, options);

// Act
var result = await sut.AcquireLockAsync(resourceId, ownerId);
var result = await sut.AcquireLockAsync(resourceId, ownerId, TestContext.Current.CancellationToken);

// Assert
result.Should().BeTrue();
Expand Down Expand Up @@ -180,7 +180,7 @@ public async Task AcquireLockHandleAsync_WhenRetryEnabledAndSucceeds_ShouldRetur
var sut = new DynamoDbDistributedLock(dynamo, options);

// Act
var result = await sut.AcquireLockHandleAsync(resourceId, ownerId);
var result = await sut.AcquireLockHandleAsync(resourceId, ownerId, TestContext.Current.CancellationToken);

// Assert
result.Should().NotBeNull();
Expand Down Expand Up @@ -209,7 +209,7 @@ public async Task AcquireLockAsync_WhenRetryEnabledAndThrottlingExhaustsRetries_
var sut = new DynamoDbDistributedLock(dynamo, options);

// Act
var result = await sut.AcquireLockAsync(resourceId, ownerId);
var result = await sut.AcquireLockAsync(resourceId, ownerId, TestContext.Current.CancellationToken);

// Assert
result.Should().BeFalse();
Expand Down