Skip to content

Commit 66f7053

Browse files
authored
feat(appmesh): add route retry policies (#13353)
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*
1 parent cc608d0 commit 66f7053

File tree

5 files changed

+678
-13
lines changed

5 files changed

+678
-13
lines changed

packages/@aws-cdk/aws-appmesh/README.md

+44
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,50 @@ router.addRoute('route-http', {
320320
});
321321
```
322322

323+
Add an http2 route with retries:
324+
325+
```ts
326+
router.addRoute('route-http2-retry', {
327+
routeSpec: appmesh.RouteSpec.http2({
328+
weightedTargets: [{ virtualNode: node }],
329+
retryPolicy: {
330+
// Retry if the connection failed
331+
tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR],
332+
// Retry if HTTP responds with a gateway error (502, 503, 504)
333+
httpRetryEvents: [appmesh.HttpRetryEvent.GATEWAY_ERROR],
334+
// Retry five times
335+
retryAttempts: 5,
336+
// Use a 1 second timeout per retry
337+
retryTimeout: cdk.Duration.seconds(1),
338+
},
339+
}),
340+
});
341+
```
342+
343+
Add a gRPC route with retries:
344+
345+
```ts
346+
router.addRoute('route-grpc-retry', {
347+
routeSpec: appmesh.RouteSpec.grpc({
348+
weightedTargets: [{ virtualNode: node }],
349+
match: { serviceName: 'servicename' },
350+
retryPolicy: {
351+
tcpRetryEvents: [appmesh.TcpRetryEvent.CONNECTION_ERROR],
352+
httpRetryEvents: [appmesh.HttpRetryEvent.GATEWAY_ERROR],
353+
// Retry if gRPC responds that the request was cancelled, a resource
354+
// was exhausted, or if the service is unavailable
355+
grpcRetryEvents: [
356+
appmesh.GrpcRetryEvent.CANCELLED,
357+
appmesh.GrpcRetryEvent.RESOURCE_EXHAUSTED,
358+
appmesh.GrpcRetryEvent.UNAVAILABLE,
359+
],
360+
retryAttempts: 5,
361+
retryTimeout: cdk.Duration.seconds(1),
362+
},
363+
}),
364+
});
365+
```
366+
323367
The _RouteSpec_ class provides an easy interface for defining new protocol specific route specs.
324368
The `tcp()`, `http()` and `http2()` methods provide the spec necessary to define a protocol specific spec.
325369

packages/@aws-cdk/aws-appmesh/lib/route-spec.ts

+198
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import * as cdk from '@aws-cdk/core';
12
import { CfnRoute } from './appmesh.generated';
23
import { Protocol, HttpTimeout, GrpcTimeout, TcpTimeout } from './shared-interfaces';
34
import { IVirtualNode } from './virtual-node';
@@ -68,6 +69,81 @@ export interface HttpRouteSpecOptions {
6869
* @default - None
6970
*/
7071
readonly timeout?: HttpTimeout;
72+
73+
/**
74+
* The retry policy
75+
*
76+
* @default - no retry policy
77+
*/
78+
readonly retryPolicy?: HttpRetryPolicy;
79+
}
80+
81+
/**
82+
* HTTP retry policy
83+
*/
84+
export interface HttpRetryPolicy {
85+
/**
86+
* Specify HTTP events on which to retry. You must specify at least one value
87+
* for at least one types of retry events.
88+
*
89+
* @default - no retries for http events
90+
*/
91+
readonly httpRetryEvents?: HttpRetryEvent[];
92+
93+
/**
94+
* The maximum number of retry attempts
95+
*/
96+
readonly retryAttempts: number;
97+
98+
/**
99+
* The timeout for each retry attempt
100+
*/
101+
readonly retryTimeout: cdk.Duration;
102+
103+
/**
104+
* TCP events on which to retry. The event occurs before any processing of a
105+
* request has started and is encountered when the upstream is temporarily or
106+
* permanently unavailable. You must specify at least one value for at least
107+
* one types of retry events.
108+
*
109+
* @default - no retries for tcp events
110+
*/
111+
readonly tcpRetryEvents?: TcpRetryEvent[];
112+
}
113+
114+
/**
115+
* HTTP events on which to retry.
116+
*/
117+
export enum HttpRetryEvent {
118+
/**
119+
* HTTP status codes 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, and 511
120+
*/
121+
SERVER_ERROR = 'server-error',
122+
123+
/**
124+
* HTTP status codes 502, 503, and 504
125+
*/
126+
GATEWAY_ERROR = 'gateway-error',
127+
128+
/**
129+
* HTTP status code 409
130+
*/
131+
CLIENT_ERROR = 'client-error',
132+
133+
/**
134+
* Retry on refused stream
135+
*/
136+
STREAM_ERROR = 'stream-error',
137+
}
138+
139+
/**
140+
* TCP events on which you may retry
141+
*/
142+
export enum TcpRetryEvent {
143+
/**
144+
* A connection error
145+
*/
146+
CONNECTION_ERROR = 'connection-error',
71147
}
72148

73149
/**
@@ -107,6 +183,64 @@ export interface GrpcRouteSpecOptions {
107183
* List of targets that traffic is routed to when a request matches the route
108184
*/
109185
readonly weightedTargets: WeightedTarget[];
186+
187+
/**
188+
* The retry policy
189+
*
190+
* @default - no retry policy
191+
*/
192+
readonly retryPolicy?: GrpcRetryPolicy;
193+
}
194+
195+
/** gRPC retry policy */
196+
export interface GrpcRetryPolicy extends HttpRetryPolicy {
197+
/**
198+
* gRPC events on which to retry. You must specify at least one value
199+
* for at least one types of retry events.
200+
*
201+
* @default - no retries for gRPC events
202+
*/
203+
readonly grpcRetryEvents?: GrpcRetryEvent[];
204+
}
205+
206+
/**
207+
* gRPC events
208+
*/
209+
export enum GrpcRetryEvent {
210+
/**
211+
* Request was cancelled
212+
*
213+
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
214+
*/
215+
CANCELLED = 'cancelled',
216+
217+
/**
218+
* The deadline was exceeded
219+
*
220+
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
221+
*/
222+
DEADLINE_EXCEEDED = 'deadline-exceeded',
223+
224+
/**
225+
* Internal error
226+
*
227+
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
228+
*/
229+
INTERNAL_ERROR = 'internal',
230+
231+
/**
232+
* A resource was exhausted
233+
*
234+
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
235+
*/
236+
RESOURCE_EXHAUSTED = 'resource-exhausted',
237+
238+
/**
239+
* The service is unavailable
240+
*
241+
* @see https://grpc.github.io/grpc/core/md_doc_statuscodes.html
242+
*/
243+
UNAVAILABLE = 'unavailable',
110244
}
111245

112246
/**
@@ -203,19 +337,40 @@ class HttpRouteSpec extends RouteSpec {
203337
*/
204338
public readonly weightedTargets: WeightedTarget[];
205339

340+
/**
341+
* The retry policy
342+
*/
343+
public readonly retryPolicy?: HttpRetryPolicy;
344+
206345
constructor(props: HttpRouteSpecOptions, protocol: Protocol) {
207346
super();
208347
this.protocol = protocol;
209348
this.match = props.match;
210349
this.weightedTargets = props.weightedTargets;
211350
this.timeout = props.timeout;
351+
352+
if (props.retryPolicy) {
353+
const httpRetryEvents = props.retryPolicy.httpRetryEvents ?? [];
354+
const tcpRetryEvents = props.retryPolicy.tcpRetryEvents ?? [];
355+
356+
if (httpRetryEvents.length + tcpRetryEvents.length === 0) {
357+
throw new Error('You must specify one value for at least one of `httpRetryEvents` or `tcpRetryEvents`');
358+
}
359+
360+
this.retryPolicy = {
361+
...props.retryPolicy,
362+
httpRetryEvents: httpRetryEvents.length > 0 ? httpRetryEvents : undefined,
363+
tcpRetryEvents: tcpRetryEvents.length > 0 ? tcpRetryEvents : undefined,
364+
};
365+
}
212366
}
213367

214368
public bind(_scope: Construct): RouteSpecConfig {
215369
const prefixPath = this.match ? this.match.prefixPath : '/';
216370
if (prefixPath[0] != '/') {
217371
throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`);
218372
}
373+
219374
const httpConfig: CfnRoute.HttpRouteProperty = {
220375
action: {
221376
weightedTargets: renderWeightedTargets(this.weightedTargets),
@@ -224,6 +379,7 @@ class HttpRouteSpec extends RouteSpec {
224379
prefix: prefixPath,
225380
},
226381
timeout: renderTimeout(this.timeout),
382+
retryPolicy: this.retryPolicy ? renderHttpRetryPolicy(this.retryPolicy) : undefined,
227383
};
228384
return {
229385
httpRouteSpec: this.protocol === Protocol.HTTP ? httpConfig : undefined,
@@ -266,11 +422,33 @@ class GrpcRouteSpec extends RouteSpec {
266422
public readonly match: GrpcRouteMatch;
267423
public readonly timeout?: GrpcTimeout;
268424

425+
/**
426+
* The retry policy.
427+
*/
428+
public readonly retryPolicy?: GrpcRetryPolicy;
429+
269430
constructor(props: GrpcRouteSpecOptions) {
270431
super();
271432
this.weightedTargets = props.weightedTargets;
272433
this.match = props.match;
273434
this.timeout = props.timeout;
435+
436+
if (props.retryPolicy) {
437+
const grpcRetryEvents = props.retryPolicy.grpcRetryEvents ?? [];
438+
const httpRetryEvents = props.retryPolicy.httpRetryEvents ?? [];
439+
const tcpRetryEvents = props.retryPolicy.tcpRetryEvents ?? [];
440+
441+
if (grpcRetryEvents.length + httpRetryEvents.length + tcpRetryEvents.length === 0) {
442+
throw new Error('You must specify one value for at least one of `grpcRetryEvents`, `httpRetryEvents` or `tcpRetryEvents`');
443+
}
444+
445+
this.retryPolicy = {
446+
...props.retryPolicy,
447+
grpcRetryEvents: grpcRetryEvents.length > 0 ? grpcRetryEvents : undefined,
448+
httpRetryEvents: httpRetryEvents.length > 0 ? httpRetryEvents : undefined,
449+
tcpRetryEvents: tcpRetryEvents.length > 0 ? tcpRetryEvents : undefined,
450+
};
451+
}
274452
}
275453

276454
public bind(_scope: Construct): RouteSpecConfig {
@@ -283,6 +461,7 @@ class GrpcRouteSpec extends RouteSpec {
283461
serviceName: this.match.serviceName,
284462
},
285463
timeout: renderTimeout(this.timeout),
464+
retryPolicy: this.retryPolicy ? renderGrpcRetryPolicy(this.retryPolicy) : undefined,
286465
},
287466
};
288467
}
@@ -323,3 +502,22 @@ function renderTimeout(timeout?: HttpTimeout): CfnRoute.HttpTimeoutProperty | un
323502
}
324503
: undefined;
325504
}
505+
506+
function renderHttpRetryPolicy(retryPolicy: HttpRetryPolicy): CfnRoute.HttpRetryPolicyProperty {
507+
return {
508+
maxRetries: retryPolicy.retryAttempts,
509+
perRetryTimeout: {
510+
unit: 'ms',
511+
value: retryPolicy.retryTimeout.toMilliseconds(),
512+
},
513+
httpRetryEvents: retryPolicy.httpRetryEvents,
514+
tcpRetryEvents: retryPolicy.tcpRetryEvents,
515+
};
516+
}
517+
518+
function renderGrpcRetryPolicy(retryPolicy: GrpcRetryPolicy): CfnRoute.GrpcRetryPolicyProperty {
519+
return {
520+
...renderHttpRetryPolicy(retryPolicy),
521+
grpcRetryEvents: retryPolicy.grpcRetryEvents,
522+
};
523+
}

0 commit comments

Comments
 (0)