Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(appmesh): add route retry policies #13353

Merged
merged 15 commits into from
Mar 11, 2021
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
44 changes: 44 additions & 0 deletions packages/@aws-cdk/aws-appmesh/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,50 @@ router.addRoute('route-http', {
});
```

Add an http2 route with retries:

```ts
router.addRoute('route-http2-retry', {
routeSpec: appmesh.RouteSpec.http2({
weightedTargets: [{ virtualNode: node }],
retryPolicy: {
// Retry if the connection failed
tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR],
skinny85 marked this conversation as resolved.
Show resolved Hide resolved
// Retry if HTTP responds with a gateway error (502, 503, 504)
httpRetryEvents: [appmesh.HttpRetryEvent.GATEWAY_ERROR],
// Retry five times
retryAttempts: 5,
// Use a 1 second timeout per retry
retryTimeout: cdk.Duration.seconds(1),
},
}),
});
```

Add a gRPC route with retries:

```ts
router.addRoute('route-grpc-retry', {
routeSpec: appmesh.RouteSpec.grpc({
weightedTargets: [{ virtualNode: node }],
match: { serviceName: 'servicename' },
retryPolicy: {
tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR],
httpRetryEvents: [appmesh.HttpRetryEvent.GATEWAY_ERROR],
// Retry if gRPC responds that the request was cancelled, a resource
// was exhausted, or if the service is unavailable
grpcRetryEvents: [
appmesh.GrpcRetryEvent.CANCELLED,
appmesh.GrpcRetryEvent.RESOURCE_EXHAUSTED,
appmesh.GrpcRetryEvent.UNAVAILABLE,
],
retryAttempts: 5,
retryTimeout: cdk.Duration.seconds(1),
},
}),
});
```

The _RouteSpec_ class provides an easy interface for defining new protocol specific route specs.
The `tcp()`, `http()` and `http2()` methods provide the spec necessary to define a protocol specific spec.

Expand Down
198 changes: 198 additions & 0 deletions packages/@aws-cdk/aws-appmesh/lib/route-spec.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as cdk from '@aws-cdk/core';
import { CfnRoute } from './appmesh.generated';
import { Protocol, HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces';
import { IVirtualNode } from './virtual-node';
Expand Down Expand Up @@ -68,6 +69,81 @@ export interface HttpRouteSpecOptions {
* @default - None
*/
readonly timeout?: HttpTimeout;

/**
* The retry policy
*
* @default - no retry policy
*/
readonly retryPolicy?: HttpRetryPolicy;
}

/**
* HTTP retry policy
*/
export interface HttpRetryPolicy {
/**
* Specify HTTP events on which to retry. You must specify at least one value
* for at least one types of retry events.
*
* @default - no retries for http events
Copy link
Contributor

Choose a reason for hiding this comment

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

The fact that either this, or tcpRetryEvents is required, needs to be stated in the documentation of this property.

*/
readonly httpRetryEvents?: HttpRetryEvent[];

/**
* The maximum number of retry attempts
*/
readonly retryAttempts: number;

/**
* The timeout for each retry attempt
*/
readonly retryTimeout: cdk.Duration;

/**
* TCP events on which to retry. The event occurs before any processing of a
* request has started and is encountered when the upstream is temporarily or
* permanently unavailable. You must specify at least one value for at least
* one types of retry events.
*
* @default - no retries for tcp events
*/
readonly tcpRetryEvents?: TcpRetryEvent[];
Copy link
Contributor

Choose a reason for hiding this comment

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

The fact that either this, or httpRetryEvents is required, needs to be stated in the documentation of this property.

}

/**
* HTTP events on which to retry.
*/
export enum HttpRetryEvent {
/**
* HTTP status codes 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, and 511
*/
SERVER_ERROR = 'server-error',

/**
* HTTP status codes 502, 503, and 504
*/
GATEWAY_ERROR = 'gateway-error',

/**
* HTTP status code 409
*/
CLIENT_ERROR = 'client-error',

/**
* Retry on refused stream
*/
STREAM_ERROR = 'stream-error',
}

/**
* TCP events on which you may retry
*/
export enum TcpRetryEvent {
/**
* A connection error
*/
CONNECTION_ERROR = 'connection-error',
}

/**
Expand Down Expand Up @@ -107,6 +183,64 @@ export interface GrpcRouteSpecOptions {
* List of targets that traffic is routed to when a request matches the route
*/
readonly weightedTargets: WeightedTarget[];

/**
* The retry policy
*
* @default - no retry policy
*/
readonly retryPolicy?: GrpcRetryPolicy;
}

/** gRPC retry policy */
export interface GrpcRetryPolicy extends HttpRetryPolicy {
/**
* gRPC events on which to retry. You must specify at least one value
* for at least one types of retry events.
*
* @default - no retries for gRPC events
*/
readonly grpcRetryEvents?: GrpcRetryEvent[];
Copy link
Contributor

Choose a reason for hiding this comment

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

The fact that one of this, tcpRetryEvents or httpRetryEvents is required needs to be stated in the documentation of this property.

}

/**
* gRPC events
*/
export enum GrpcRetryEvent {
/**
* Request was cancelled
*
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
*/
CANCELLED = 'cancelled',

/**
* The deadline was exceeded
*
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
*/
DEADLINE_EXCEEDED = 'deadline-exceeded',

/**
* Internal error
*
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
*/
INTERNAL_ERROR = 'internal',

/**
* A resource was exhausted
*
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
*/
RESOURCE_EXHAUSTED = 'resource-exhausted',

/**
* The service is unavailable
*
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
*/
UNAVAILABLE = 'unavailable',
}

/**
Expand Down Expand Up @@ -203,19 +337,40 @@ class HttpRouteSpec extends RouteSpec {
*/
public readonly weightedTargets: WeightedTarget[];

/**
* The retry policy
*/
public readonly retryPolicy?: HttpRetryPolicy;

constructor(props: HttpRouteSpecOptions, protocol: Protocol) {
super();
this.protocol = protocol;
this.match = props.match;
this.weightedTargets = props.weightedTargets;
this.timeout = props.timeout;

if (props.retryPolicy) {
const httpRetryEvents = props.retryPolicy.httpRetryEvents ?? [];
const tcpRetryEvents = props.retryPolicy.tcpRetryEvents ?? [];

if (httpRetryEvents.length + tcpRetryEvents.length === 0) {
throw new Error('You must specify one value for at least one of `httpRetryEvents` or `tcpRetryEvents`');
}

this.retryPolicy = {
...props.retryPolicy,
httpRetryEvents: httpRetryEvents.length > 0 ? httpRetryEvents : undefined,
tcpRetryEvents: tcpRetryEvents.length > 0 ? tcpRetryEvents : undefined,
};
}
}

public bind(_scope: Construct): RouteSpecConfig {
const prefixPath = this.match ? this.match.prefixPath : '/';
if (prefixPath[0] != '/') {
throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`);
}

const httpConfig: CfnRoute.HttpRouteProperty = {
action: {
weightedTargets: renderWeightedTargets(this.weightedTargets),
Expand All @@ -224,6 +379,7 @@ class HttpRouteSpec extends RouteSpec {
prefix: prefixPath,
},
timeout: renderTimeout(this.timeout),
retryPolicy: this.retryPolicy ? renderHttpRetryPolicy(this.retryPolicy) : undefined,
};
return {
httpRouteSpec: this.protocol === Protocol.HTTP ? httpConfig : undefined,
Expand Down Expand Up @@ -266,11 +422,33 @@ class GrpcRouteSpec extends RouteSpec {
public readonly match: GrpcRouteMatch;
public readonly timeout?: GrpcTimeout;

/**
* The retry policy.
*/
public readonly retryPolicy?: GrpcRetryPolicy;

constructor(props: GrpcRouteSpecOptions) {
super();
this.weightedTargets = props.weightedTargets;
this.match = props.match;
this.timeout = props.timeout;

if (props.retryPolicy) {
const grpcRetryEvents = props.retryPolicy.grpcRetryEvents ?? [];
const httpRetryEvents = props.retryPolicy.httpRetryEvents ?? [];
const tcpRetryEvents = props.retryPolicy.tcpRetryEvents ?? [];

if (grpcRetryEvents.length + httpRetryEvents.length + tcpRetryEvents.length === 0) {
throw new Error('You must specify one value for at least one of `grpcRetryEvents`, `httpRetryEvents` or `tcpRetryEvents`');
}

this.retryPolicy = {
...props.retryPolicy,
grpcRetryEvents: grpcRetryEvents.length > 0 ? grpcRetryEvents : undefined,
httpRetryEvents: httpRetryEvents.length > 0 ? httpRetryEvents : undefined,
tcpRetryEvents: tcpRetryEvents.length > 0 ? tcpRetryEvents : undefined,
};
}
}

public bind(_scope: Construct): RouteSpecConfig {
Expand All @@ -283,6 +461,7 @@ class GrpcRouteSpec extends RouteSpec {
serviceName: this.match.serviceName,
},
timeout: renderTimeout(this.timeout),
retryPolicy: this.retryPolicy ? renderGrpcRetryPolicy(this.retryPolicy) : undefined,
},
};
}
Expand Down Expand Up @@ -323,3 +502,22 @@ function renderTimeout(timeout?: HttpTimeout): CfnRoute.HttpTimeoutProperty | un
}
: undefined;
}

function renderHttpRetryPolicy(retryPolicy: HttpRetryPolicy): CfnRoute.HttpRetryPolicyProperty {
return {
maxRetries: retryPolicy.retryAttempts,
perRetryTimeout: {
unit: 'ms',
value: retryPolicy.retryTimeout.toMilliseconds(),
},
httpRetryEvents: retryPolicy.httpRetryEvents,
tcpRetryEvents: retryPolicy.tcpRetryEvents,
};
}

function renderGrpcRetryPolicy(retryPolicy: GrpcRetryPolicy): CfnRoute.GrpcRetryPolicyProperty {
return {
...renderHttpRetryPolicy(retryPolicy),
grpcRetryEvents: retryPolicy.grpcRetryEvents,
};
}
Loading