Skip to content

Commit

Permalink
feat(appmesh): add route retry policies (#13353)
Browse files Browse the repository at this point in the history
Adds route retry policies for http/http2 and gRPC routes.

Closes #11642

----

*By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license*
  • Loading branch information
misterjoshua committed Mar 11, 2021
1 parent cc608d0 commit 66f7053
Show file tree
Hide file tree
Showing 5 changed files with 678 additions and 13 deletions.
44 changes: 44 additions & 0 deletions packages/@aws-cdk/aws-appmesh/README.md
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],
// 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
@@ -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
*/
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[];
}

/**
* 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[];
}

/**
* 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,
};
}

0 comments on commit 66f7053

Please sign in to comment.