Skip to content

Commit 8d2fd2d

Browse files
feat: add DynamoDB transactional storage provider (#9616)
* Initial cut of dynamodb transactional storage * the trnasactional storage load keys and states upon activation * write a transactional storage function * updated * chunk transaction items and wrote unit tests * updated recovery test * updated recovery test * changed * set maximum bytes in state * updated * polish codes * configured storage serializer for options * fixed unit test error * Update to slnx * Fix obsoletion error * Tests * Minor fixups * fix: harden DynamoDB transactional storage Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30769b52-5dfb-4bc6-9cb9-6bf16f79c3f4 * fix: address DynamoDB review feedback Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30769b52-5dfb-4bc6-9cb9-6bf16f79c3f4 * fix: propagate DynamoDB initialization cancellation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30769b52-5dfb-4bc6-9cb9-6bf16f79c3f4 * docs: fix DynamoDB capitalization Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30769b52-5dfb-4bc6-9cb9-6bf16f79c3f4 * docs: document DynamoDB transaction APIs Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30769b52-5dfb-4bc6-9cb9-6bf16f79c3f4 * fix: harden DynamoDB transactional batching Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 30769b52-5dfb-4bc6-9cb9-6bf16f79c3f4 --------- Co-authored-by: Reuben Bond <reuben.bond@gmail.com> Co-authored-by: Reuben Bond <rebond@microsoft.com> Copilot-Session: 30769b52-5dfb-4bc6-9cb9-6bf16f79c3f4
1 parent 07d01be commit 8d2fd2d

36 files changed

Lines changed: 2302 additions & 75 deletions

Orleans.slnx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,7 @@
8282
<Project Path="src/AWS/Orleans.Persistence.DynamoDB/Orleans.Persistence.DynamoDB.csproj" />
8383
<Project Path="src/AWS/Orleans.Reminders.DynamoDB/Orleans.Reminders.DynamoDB.csproj" />
8484
<Project Path="src/AWS/Orleans.Streaming.SQS/Orleans.Streaming.SQS.csproj" />
85+
<Project Path="src/AWS/Orleans.Transactions.DynamoDB/Orleans.Transactions.DynamoDB.csproj" />
8586
</Folder>
8687
<Folder Name="/src/Extensions/Azure/">
8788
<Project Path="src/Azure/Orleans.Clustering.AzureStorage/Orleans.Clustering.AzureStorage.csproj" />
@@ -186,6 +187,7 @@
186187
</Folder>
187188
<Folder Name="/test/Transactions/">
188189
<Project Path="test/Transactions/Orleans.Transactions.Azure.Test/Orleans.Transactions.Azure.Test.csproj" />
190+
<Project Path="test/Transactions/Orleans.Transactions.DynamoDB.Test/Orleans.Transactions.DynamoDB.Test.csproj" />
189191
<Project Path="test/Transactions/Orleans.Transactions.Tests/Orleans.Transactions.Tests.csproj" />
190192
</Folder>
191193
</Solution>

src/AWS/Orleans.Persistence.DynamoDB/Options/DynamoDBStorageOptions.cs

100755100644
File mode changed.

src/AWS/Orleans.Persistence.DynamoDB/Provider/DynamoDBGrainStorage.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,15 @@ await storage.InitializeTable(this.options.TableName,
9595
new AttributeDefinition { AttributeName = GRAIN_TYPE_PROPERTY_NAME, AttributeType = ScalarAttributeType.S }
9696
},
9797
secondaryIndexes: null,
98-
ttlAttributeName: this.options.TimeToLive.HasValue ? GRAIN_TTL_PROPERTY_NAME : null);
98+
ttlAttributeName: this.options.TimeToLive.HasValue ? GRAIN_TTL_PROPERTY_NAME : null,
99+
cancellationToken: ct);
99100
stopWatch.Stop();
100101
LogInformationProviderInitialized(logger, this.name, this.GetType().Name, this.options.InitStage, stopWatch.ElapsedMilliseconds);
101102
}
103+
catch (OperationCanceledException) when (ct.IsCancellationRequested)
104+
{
105+
throw;
106+
}
102107
catch (Exception exc)
103108
{
104109
stopWatch.Stop();
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.DependencyInjection.Extensions;
4+
using Microsoft.Extensions.Options;
5+
using Orleans.Configuration;
6+
using Orleans.Providers;
7+
using Orleans.Runtime;
8+
using Orleans.Storage;
9+
using Orleans.Transactions.Abstractions;
10+
using Orleans.Transactions.DynamoDB.TransactionalState;
11+
12+
namespace Orleans.Hosting;
13+
14+
/// <summary>
15+
/// <see cref="IServiceCollection"/> extensions.
16+
/// </summary>
17+
public static class DynamoDBTransactionServiceCollectionExtensions
18+
{
19+
internal static IServiceCollection AddDynamoDBTransactionalStateStorage(this IServiceCollection services,
20+
string name,
21+
Action<OptionsBuilder<DynamoDBTransactionalStorageOptions>>? configureOptions = null)
22+
{
23+
configureOptions?.Invoke(services.AddOptions<DynamoDBTransactionalStorageOptions>(name));
24+
services.AddTransient<IConfigurationValidator>(sp => new DynamoDBTransactionalStorageOptionsValidator(sp.GetRequiredService<IOptionsMonitor<DynamoDBTransactionalStorageOptions>>().Get(name), name));
25+
services.ConfigureNamedOptionForLogging<DynamoDBTransactionalStorageOptions>(name);
26+
services.AddTransient<IPostConfigureOptions<DynamoDBTransactionalStorageOptions>, DefaultStorageProviderSerializerOptionsConfigurator<DynamoDBTransactionalStorageOptions>>();
27+
28+
services.TryAddSingleton<ITransactionalStateStorageFactory>(sp => sp.GetRequiredKeyedService<ITransactionalStateStorageFactory>(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME));
29+
services.AddKeyedSingleton<ITransactionalStateStorageFactory>(name, (sp, key) =>
30+
DynamoDBTransactionalStateStorageFactory.Create(
31+
sp,
32+
key as string ?? throw new InvalidOperationException("The transactional state storage provider name is required.")));
33+
services.AddSingleton<ILifecycleParticipant<ISiloLifecycle>>(s => (ILifecycleParticipant<ISiloLifecycle>)s.GetRequiredKeyedService<ITransactionalStateStorageFactory>(name));
34+
35+
return services;
36+
}
37+
}
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
using System;
2+
using Microsoft.Extensions.Options;
3+
using Orleans.Configuration;
4+
using Orleans.Providers;
5+
6+
namespace Orleans.Hosting;
7+
8+
/// <summary>
9+
/// Extensions for configuring DynamoDB transactional state storage.
10+
/// </summary>
11+
public static class DynamoDBTransactionSiloBuilderExtensions
12+
{
13+
/// <summary>
14+
/// Configure silo to use DynamoDB storage as the default transactional grain storage.
15+
/// </summary>
16+
/// <param name="builder">The silo builder.</param>
17+
/// <param name="configureOptions">The action used to configure the provider.</param>
18+
/// <returns>The silo builder.</returns>
19+
public static ISiloBuilder AddDynamoDBTransactionalStateStorageAsDefault(this ISiloBuilder builder, Action<DynamoDBTransactionalStorageOptions> configureOptions)
20+
{
21+
return builder.AddDynamoDBTransactionalStateStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions);
22+
}
23+
24+
/// <summary>
25+
/// Configure silo to use DynamoDB storage for transactional grain storage.
26+
/// </summary>
27+
/// <param name="builder">The silo builder.</param>
28+
/// <param name="name">The provider name.</param>
29+
/// <param name="configureOptions">The action used to configure the provider.</param>
30+
/// <returns>The silo builder.</returns>
31+
public static ISiloBuilder AddDynamoDBTransactionalStateStorage(this ISiloBuilder builder, string name, Action<DynamoDBTransactionalStorageOptions> configureOptions)
32+
{
33+
return builder.ConfigureServices(services => services.AddDynamoDBTransactionalStateStorage(name, ob => ob.Configure(configureOptions)));
34+
}
35+
36+
/// <summary>
37+
/// Configure silo to use DynamoDB storage as the default transactional grain storage.
38+
/// </summary>
39+
/// <param name="builder">The silo builder.</param>
40+
/// <param name="configureOptions">The action used to configure the provider.</param>
41+
/// <returns>The silo builder.</returns>
42+
public static ISiloBuilder AddDynamoDBTransactionalStateStorageAsDefault(this ISiloBuilder builder, Action<OptionsBuilder<DynamoDBTransactionalStorageOptions>>? configureOptions = null)
43+
{
44+
return builder.AddDynamoDBTransactionalStateStorage(ProviderConstants.DEFAULT_STORAGE_PROVIDER_NAME, configureOptions);
45+
}
46+
47+
/// <summary>
48+
/// Configure silo to use DynamoDB storage for transactional grain storage.
49+
/// </summary>
50+
/// <param name="builder">The silo builder.</param>
51+
/// <param name="name">The provider name.</param>
52+
/// <param name="configureOptions">The action used to configure the provider.</param>
53+
/// <returns>The silo builder.</returns>
54+
public static ISiloBuilder AddDynamoDBTransactionalStateStorage(this ISiloBuilder builder, string name, Action<OptionsBuilder<DynamoDBTransactionalStorageOptions>>? configureOptions = null)
55+
{
56+
return builder.ConfigureServices(services => services.AddDynamoDBTransactionalStateStorage(name, configureOptions));
57+
}
58+
}
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
using Orleans.Runtime;
2+
using Orleans.Storage;
3+
#if CLUSTERING_DYNAMODB
4+
using Orleans.Clustering.DynamoDB;
5+
#elif PERSISTENCE_DYNAMODB
6+
using Orleans.Persistence.DynamoDB;
7+
#elif REMINDERS_DYNAMODB
8+
using Orleans.Reminders.DynamoDB;
9+
#elif AWSUTILS_TESTS
10+
using Orleans.AWSUtils.Tests;
11+
#elif TRANSACTIONS_DYNAMODB
12+
using Orleans.Transactions.DynamoDB;
13+
#else
14+
#endif
15+
16+
namespace Orleans.Configuration;
17+
18+
/// <summary>
19+
/// Configuration options for DynamoDB transactional state storage.
20+
/// </summary>
21+
public class DynamoDBTransactionalStorageOptions : DynamoDBClientOptions, IStorageProviderSerializerOptions
22+
{
23+
/// <summary>
24+
/// Gets or sets a unique identifier for this service, which should survive deployment and redeployment.
25+
/// </summary>
26+
public string ServiceId { get; set; } = string.Empty;
27+
28+
/// <summary>
29+
/// Use Provisioned Throughput for tables
30+
/// </summary>
31+
public bool UseProvisionedThroughput { get; set; } = true;
32+
33+
/// <summary>
34+
/// Create the table if it doesn't exist
35+
/// </summary>
36+
public bool CreateIfNotExists { get; set; } = true;
37+
38+
/// <summary>
39+
/// Update the table if it exists
40+
/// </summary>
41+
public bool UpdateIfExists { get; set; } = true;
42+
43+
/// <summary>
44+
/// Read capacity unit for DynamoDB storage
45+
/// </summary>
46+
public int ReadCapacityUnits { get; set; } = DynamoDBStorage.DefaultReadCapacityUnits;
47+
48+
/// <summary>
49+
/// Write capacity unit for DynamoDB storage
50+
/// </summary>
51+
public int WriteCapacityUnits { get; set; } = DynamoDBStorage.DefaultWriteCapacityUnits;
52+
53+
/// <summary>
54+
/// DynamoDB table name.
55+
/// Defaults to 'OrleansTransactionalState'.
56+
/// </summary>
57+
public string TableName { get; set; } = "OrleansTransactionalState";
58+
59+
/// <summary>
60+
/// Stage of silo lifecycle where storage should be initialized. Storage must be initialized prior to use.
61+
/// </summary>
62+
public int InitStage { get; set; } = DEFAULT_INIT_STAGE;
63+
64+
/// <summary>
65+
/// The default silo lifecycle stage for initializing transactional state storage.
66+
/// </summary>
67+
public const int DEFAULT_INIT_STAGE = ServiceLifecycleStage.ApplicationServices;
68+
69+
/// <summary>
70+
/// Gets or sets the serializer used to serialize grain state.
71+
/// </summary>
72+
public IGrainStorageSerializer GrainStorageSerializer { get; set; } = null!;
73+
}
74+
75+
/// <summary>
76+
/// Configuration validator for DynamoDBTransactionalStorageOptions
77+
/// </summary>
78+
public class DynamoDBTransactionalStorageOptionsValidator : IConfigurationValidator
79+
{
80+
private readonly DynamoDBTransactionalStorageOptions options;
81+
private readonly string name;
82+
83+
/// <summary>
84+
/// Constructor
85+
/// </summary>
86+
/// <param name="options">The option to be validated.</param>
87+
/// <param name="name">The option name to be validated.</param>
88+
public DynamoDBTransactionalStorageOptionsValidator(DynamoDBTransactionalStorageOptions options, string name)
89+
{
90+
this.options = options;
91+
this.name = name;
92+
}
93+
94+
/// <inheritdoc />
95+
public void ValidateConfiguration()
96+
{
97+
if (string.IsNullOrWhiteSpace(this.options.TableName))
98+
throw new OrleansConfigurationException(
99+
$"Configuration for DynamoDBTransactionalStateStorage {this.name} is invalid. {nameof(this.options.TableName)} is not valid.");
100+
101+
if (this.options.UseProvisionedThroughput)
102+
{
103+
if (this.options.ReadCapacityUnits == 0)
104+
throw new OrleansConfigurationException(
105+
$"Configuration for DynamoDBTransactionalStateStorage {this.name} is invalid. {nameof(this.options.ReadCapacityUnits)} is not valid.");
106+
107+
if (this.options.WriteCapacityUnits == 0)
108+
throw new OrleansConfigurationException(
109+
$"Configuration for DynamoDBTransactionalStateStorage {this.name} is invalid. {nameof(this.options.WriteCapacityUnits)} is not valid.");
110+
}
111+
}
112+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
<PropertyGroup>
3+
<PackageReadmeFile>README.md</PackageReadmeFile>
4+
<PackageId>Microsoft.Orleans.Transactions.DynamoDB</PackageId>
5+
<Title>Microsoft Orleans AWS DynamoDB Transactions Provider</Title>
6+
<Description>Microsoft Orleans transactional providers backed by AWS DynamoDB</Description>
7+
<PackageTags>$(PackageTags) AWS DynamoDB</PackageTags>
8+
<TargetFrameworks>$(DefaultTargetFrameworks)</TargetFrameworks>
9+
<OrleansBuildTimeCodeGen>true</OrleansBuildTimeCodeGen>
10+
</PropertyGroup>
11+
12+
<PropertyGroup>
13+
<AssemblyName>Orleans.Transactions.DynamoDB</AssemblyName>
14+
<RootNamespace>Orleans.Transactions.DynamoDB</RootNamespace>
15+
<DefineConstants>$(DefineConstants);TRANSACTIONS_DYNAMODB</DefineConstants>
16+
</PropertyGroup>
17+
18+
<ItemGroup>
19+
<Compile Include="..\Shared\AWSUtils.cs" Link="AWSUtils.cs" />
20+
<Compile Include="..\Shared\Storage\DynamoDBStorage.cs" Link="Storage\DynamoDBStorage.cs" />
21+
<Compile Include="..\Shared\Storage\DynamoDBClientOptions.cs" Link="Storage\DynamoDBClientOptions.cs" />
22+
</ItemGroup>
23+
24+
<ItemGroup>
25+
<ProjectReference Include="$(SourceRoot)src\Orleans.Runtime\Orleans.Runtime.csproj" />
26+
<ProjectReference Include="..\..\Orleans.Transactions\Orleans.Transactions.csproj" />
27+
<PackageReference Include="AWSSDK.DynamoDBv2" />
28+
</ItemGroup>
29+
30+
<ItemGroup>
31+
<None Include="README.md" Pack="true" PackagePath="\" />
32+
</ItemGroup>
33+
</Project>
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
# Microsoft Orleans Transaction for DynamoDB
2+
3+
## Introduction
4+
Microsoft Orleans Transaction for DynamoDB provides grain transaction for Microsoft Orleans using Amazon's DynamoDB.
5+
This ensures that your grains can perform transactions in a distributed environment using DynamoDB as the underlying storage.
6+
7+
Serialized transactional state and metadata must each fit within DynamoDB's 400 KB item limit. Write batches are split to remain within DynamoDB's limits of 100 actions and 4 MB of affected items per transaction.
8+
9+
## Getting Started
10+
To use this package, install it via NuGet:
11+
12+
```shell
13+
dotnet add package Microsoft.Orleans.Transactions.DynamoDB
14+
```
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace Orleans.Transactions.DynamoDB.TransactionalState;
2+
3+
/// <summary>
4+
/// Constants used by DynamoDB transactional state storage.
5+
/// </summary>
6+
public static class DynamoDBTransactionalStateConstants
7+
{
8+
/// <summary>
9+
/// The DynamoDB partition key attribute name.
10+
/// </summary>
11+
public const string PARTITION_KEY_PROPERTY_NAME = "PartitionKey";
12+
13+
/// <summary>
14+
/// The DynamoDB row key attribute name.
15+
/// </summary>
16+
public const string ROW_KEY_PROPERTY_NAME = "RowKey";
17+
18+
/// <summary>
19+
/// The serialized grain state attribute name.
20+
/// </summary>
21+
public const string BINARY_STATE_PROPERTY_NAME = "GrainState";
22+
23+
/// <summary>
24+
/// The entity tag attribute name.
25+
/// </summary>
26+
public const string ETAG_PROPERTY_NAME = "ETag";
27+
28+
/// <summary>
29+
/// The timestamp attribute name.
30+
/// </summary>
31+
public const string TIMESTAMP_PROPERTY_NAME = "Timestamp";
32+
33+
/// <summary>
34+
/// The expression alias for the current entity tag.
35+
/// </summary>
36+
public const string CURRENT_ETAG_ALIAS = ":currentETag";
37+
}

0 commit comments

Comments
 (0)