Skip to content

Commit

Permalink
feat(dynamodb): custom timeout for replication operation (#13354)
Browse files Browse the repository at this point in the history
Closes #10249


----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
jogold committed Mar 8, 2021
1 parent 4e03667 commit 6a5a4f2
Show file tree
Hide file tree
Showing 4 changed files with 62 additions and 20 deletions.
11 changes: 11 additions & 0 deletions packages/@aws-cdk/aws-dynamodb/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,17 @@ globalTable.autoScaleWriteCapacity({
}).scaleOnUtilization({ targetUtilizationPercent: 75 });
```

When adding a replica region for a large table, you might want to increase the
timeout for the replication operation:

```ts
const globalTable = new dynamodb.Table(this, 'Table', {
partitionKey: { name: 'id', type: dynamodb.AttributeType.STRING },
replicationRegions: ['us-east-1', 'us-east-2', 'us-west-2'],
replicationTimeout: Duration.hours(2), // defaults to Duration.minutes(30)
});
```

## Encryption

All user data stored in Amazon DynamoDB is fully encrypted at rest. When creating a new table, you can choose to encrypt using the following customer master keys (CMK) to encrypt your table:
Expand Down
19 changes: 16 additions & 3 deletions packages/@aws-cdk/aws-dynamodb/lib/replica-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,26 @@ import { Construct } from 'constructs';
// eslint-disable-next-line no-duplicate-imports, import/order
import { Construct as CoreConstruct } from '@aws-cdk/core';

/**
* Properties for a ReplicaProvider
*/
export interface ReplicaProviderProps {
/**
* The timeout for the replication operation.
*
* @default Duration.minutes(30)
*/
readonly timeout?: Duration;
}

export class ReplicaProvider extends NestedStack {
/**
* Creates a stack-singleton resource provider nested stack.
*/
public static getOrCreate(scope: Construct) {
public static getOrCreate(scope: Construct, props: ReplicaProviderProps = {}) {
const stack = Stack.of(scope);
const uid = '@aws-cdk/aws-dynamodb.ReplicaProvider';
return stack.node.tryFindChild(uid) as ReplicaProvider || new ReplicaProvider(stack, uid);
return stack.node.tryFindChild(uid) as ReplicaProvider ?? new ReplicaProvider(stack, uid, props);
}

/**
Expand All @@ -34,7 +46,7 @@ export class ReplicaProvider extends NestedStack {
*/
public readonly isCompleteHandler: lambda.Function;

private constructor(scope: Construct, id: string) {
private constructor(scope: Construct, id: string, props: ReplicaProviderProps = {}) {
super(scope as CoreConstruct, id);

const code = lambda.Code.fromAsset(path.join(__dirname, 'replica-handler'));
Expand Down Expand Up @@ -80,6 +92,7 @@ export class ReplicaProvider extends NestedStack {
onEventHandler: this.onEventHandler,
isCompleteHandler: this.isCompleteHandler,
queryInterval: Duration.seconds(10),
totalTimeout: props.timeout,
});
}
}
17 changes: 12 additions & 5 deletions packages/@aws-cdk/aws-dynamodb/lib/table.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import {
Aws, CfnCondition, CfnCustomResource, CustomResource, Fn,
IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token,
Aws, CfnCondition, CfnCustomResource, CustomResource, Duration,
Fn, IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token,
} from '@aws-cdk/core';
import { Construct } from 'constructs';
import { DynamoDBMetrics } from './dynamodb-canned-metrics.generated';
Expand Down Expand Up @@ -218,6 +218,13 @@ export interface TableOptions {
* @experimental
*/
readonly replicationRegions?: string[];

/**
* The timeout for a table replication operation in a single region.
*
* @default Duration.minutes(30)
*/
readonly replicationTimeout?: Duration;
}

/**
Expand Down Expand Up @@ -1135,7 +1142,7 @@ export class Table extends TableBase {
}

if (props.replicationRegions && props.replicationRegions.length > 0) {
this.createReplicaTables(props.replicationRegions);
this.createReplicaTables(props.replicationRegions, props.replicationTimeout);
}
}

Expand Down Expand Up @@ -1451,14 +1458,14 @@ export class Table extends TableBase {
*
* @param regions regions where to create tables
*/
private createReplicaTables(regions: string[]) {
private createReplicaTables(regions: string[], timeout?: Duration) {
const stack = Stack.of(this);

if (!Token.isUnresolved(stack.region) && regions.includes(stack.region)) {
throw new Error('`replicationRegions` cannot include the region where this stack is deployed.');
}

const provider = ReplicaProvider.getOrCreate(this);
const provider = ReplicaProvider.getOrCreate(this, { timeout });

// Documentation at https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/V2gt_IAM.html
// is currently incorrect. AWS Support recommends `dynamodb:*` in both source and destination regions
Expand Down
35 changes: 23 additions & 12 deletions packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import * as appscaling from '@aws-cdk/aws-applicationautoscaling';
import * as iam from '@aws-cdk/aws-iam';
import * as kms from '@aws-cdk/aws-kms';
import { App, Aws, CfnDeletionPolicy, ConstructNode, Duration, PhysicalName, RemovalPolicy, Resource, Stack, Tags } from '@aws-cdk/core';
import * as cr from '@aws-cdk/custom-resources';
import { testLegacyBehavior } from 'cdk-build-tools/lib/feature-flag';
import { Construct } from 'constructs';
import {
Expand All @@ -20,6 +21,8 @@ import {
CfnTable,
} from '../lib';

jest.mock('@aws-cdk/custom-resources');

/* eslint-disable quote-props */

// CDK parameters
Expand Down Expand Up @@ -2295,12 +2298,6 @@ describe('global', () => {
// THEN
expect(stack).toHaveResource('Custom::DynamoDBReplica', {
Properties: {
ServiceToken: {
'Fn::GetAtt': [
'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D',
'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn',
],
},
TableName: {
Ref: 'TableCD117FA1',
},
Expand All @@ -2311,12 +2308,6 @@ describe('global', () => {

expect(stack).toHaveResource('Custom::DynamoDBReplica', {
Properties: {
ServiceToken: {
'Fn::GetAtt': [
'awscdkawsdynamodbReplicaProviderNestedStackawscdkawsdynamodbReplicaProviderNestedStackResource18E3F12D',
'Outputs.awscdkawsdynamodbReplicaProviderframeworkonEventF9504691Arn',
],
},
TableName: {
Ref: 'TableCD117FA1',
},
Expand Down Expand Up @@ -2814,6 +2805,26 @@ describe('global', () => {
// THEN
expect(SynthUtils.toCloudFormation(stack).Conditions).toBeUndefined();
});

test('can configure timeout', () => {
// GIVEN
const stack = new Stack();

// WHEN
new Table(stack, 'Table', {
partitionKey: {
name: 'id',
type: AttributeType.STRING,
},
replicationRegions: ['eu-central-1'],
replicationTimeout: Duration.hours(1),
});

// THEN
expect(cr.Provider).toHaveBeenCalledWith(expect.anything(), expect.any(String), expect.objectContaining({
totalTimeout: Duration.hours(1),
}));
});
});

test('L1 inside L2 expects removalpolicy to have been set', () => {
Expand Down

0 comments on commit 6a5a4f2

Please sign in to comment.