Skip to content

Commit

Permalink
feat: add RetryStrategy class and retryMiddleware implementation (#389)
Browse files Browse the repository at this point in the history
* feat: add RetryStrategy class and change retryMiddleware interface
  • Loading branch information
AllanZhengYP authored and trivikr committed Jan 3, 2020
1 parent 03d120f commit ff70fac
Show file tree
Hide file tree
Showing 5 changed files with 105 additions and 56 deletions.
82 changes: 71 additions & 11 deletions packages/retry-middleware/src/defaultStrategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,79 @@ import {
import { defaultDelayDecider } from "./delayDecider";
import { defaultRetryDecider } from "./retryDecider";
import { isThrottlingError } from "@aws-sdk/service-error-classification";
import { RetryStrategy, SdkError } from "@aws-sdk/types";
import {
SdkError,
FinalizeHandler,
MetadataBearer,
FinalizeHandlerArguments,
RetryStrategy
} from "@aws-sdk/types";

/**
* Determines whether an error is retryable based on the number of retries
* already attempted, the HTTP status code, and the error received (if any).
*
* @param error The error encountered.
*/
export interface RetryDecider {
(error: SdkError): boolean;
}

/**
* Determines the number of milliseconds to wait before retrying an action.
*
* @param delayBase The base delay (in milliseconds).
* @param attempts The number of times the action has already been tried.
*/
export interface DelayDecider {
(delayBase: number, attempts: number): number;
}

export class ExponentialBackOffStrategy implements RetryStrategy {
constructor(public readonly maxRetries: number) {}
shouldRetry(error: SdkError, retryAttempted: number) {
return retryAttempted < this.maxRetries && defaultRetryDecider(error);
constructor(
public readonly maxRetries: number,
private retryDecider: RetryDecider = defaultRetryDecider,
private delayDecider: DelayDecider = defaultDelayDecider
) {}
private shouldRetry(error: SdkError, retryAttempted: number) {
return retryAttempted < this.maxRetries && this.retryDecider(error);
}
computeDelayBeforeNextRetry(error: SdkError, retryAttempted: number): number {
return defaultDelayDecider(
isThrottlingError(error)
? THROTTLING_RETRY_DELAY_BASE
: DEFAULT_RETRY_DELAY_BASE,
retryAttempted
);

async retry<Input extends object, Ouput extends MetadataBearer>(
next: FinalizeHandler<Input, Ouput>,
args: FinalizeHandlerArguments<Input>
) {
let retries = 0;
let totalDelay = 0;
while (true) {
try {
const { response, output } = await next(args);
output.$metadata.retries = retries;
output.$metadata.totalRetryDelay = totalDelay;

return { response, output };
} catch (err) {
if (this.shouldRetry(err as SdkError, retries)) {
const delay = this.delayDecider(
isThrottlingError(err)
? THROTTLING_RETRY_DELAY_BASE
: DEFAULT_RETRY_DELAY_BASE,
retries++
);
totalDelay += delay;

await new Promise(resolve => setTimeout(resolve, delay));
continue;
}

if (!err.$metadata) {
err.$metadata = {};
}

err.$metadata.retries = retries;
err.$metadata.totalRetryDelay = totalDelay;
throw err;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@ import {
import { retryMiddleware } from "./retryMiddleware";
import { RetryConfig } from "./configurations";
import * as delayDeciderModule from "./delayDecider";
import { ExponentialBackOffStrategy } from "./defaultStrategy";
import { ExponentialBackOffStrategy, RetryDecider } from "./defaultStrategy";
import { HttpRequest } from "@aws-sdk/protocol-http";
import { SdkError } from '@aws-sdk/types';

describe("retryMiddleware", () => {
it("should not retry when the handler completes successfully", async () => {
Expand Down Expand Up @@ -73,8 +74,8 @@ describe("retryMiddleware", () => {
delayDeciderModule,
"defaultDelayDecider"
);
const strategy = new ExponentialBackOffStrategy(maxRetries);
strategy.shouldRetry = () => true;
const retryDecider: RetryDecider = (error: SdkError) => true;
const strategy = new ExponentialBackOffStrategy(maxRetries, retryDecider);
const retryHandler = retryMiddleware({
maxRetries,
retryStrategy: strategy
Expand Down
2 changes: 2 additions & 0 deletions packages/retry-middleware/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
export * from "./retryMiddleware";
export * from "./defaultStrategy";
export * from "./configurations";
export * from "./delayDecider";
export * from "./retryDecider";
42 changes: 5 additions & 37 deletions packages/retry-middleware/src/retryMiddleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,50 +3,18 @@ import {
FinalizeHandlerArguments,
MetadataBearer,
FinalizeHandlerOutput,
SdkError,
InjectableMiddleware
} from "@aws-sdk/types";
import { RetryConfig } from "./configurations";

export function retryMiddleware(options: RetryConfig.Resolved) {
return <Output extends MetadataBearer = MetadataBearer>(
next: FinalizeHandler<any, Output>
): FinalizeHandler<any, Output> =>
async function retry(
args: FinalizeHandlerArguments<any>
): Promise<FinalizeHandlerOutput<Output>> {
let retries = 0;
let totalDelay = 0;
while (true) {
try {
const { response, output } = await next(args);
output.$metadata.retries = retries;
output.$metadata.totalRetryDelay = totalDelay;

return { response, output };
} catch (err) {
if (options.retryStrategy.shouldRetry(err as SdkError, retries)) {
const delay = options.retryStrategy.computeDelayBeforeNextRetry(
err,
retries
);
retries++;
totalDelay += delay;

await new Promise(resolve => setTimeout(resolve, delay));
continue;
}

if (!err.$metadata) {
err.$metadata = {};
}

err.$metadata.retries = retries;
err.$metadata.totalRetryDelay = totalDelay;
throw err;
}
}
};
): FinalizeHandler<any, Output> => async (
args: FinalizeHandlerArguments<any>
): Promise<FinalizeHandlerOutput<Output>> => {
return options.retryStrategy.retry(next, args);
};
}

export function retryPlugin<
Expand Down
28 changes: 23 additions & 5 deletions packages/types/src/util.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
import { HttpEndpoint } from "./http";
import {
FinalizeHandler,
FinalizeHandlerArguments,
FinalizeHandlerOutput
} from "./middleware";
import { MetadataBearer } from "./response";

/**
* A function that, given a TypedArray of bytes, can produce a string
Expand Down Expand Up @@ -50,12 +56,24 @@ export interface BodyLengthCalculator {
// TODO Unify with the types created for the error parsers
export type SdkError = Error & { connectionError?: boolean };

/**
* Interface that specifies the retry behavior
*/
export interface RetryStrategy {
shouldRetry: (error: SdkError, retryAttempted: number) => boolean;
computeDelayBeforeNextRetry: (
error: SdkError,
retryAttempted: number
) => number;
/**
* the maximum number of times requests that encounter potentially
* transient failures should be retried
*/
maxRetries: number;
/**
* the retry behavior the will invoke the next handler and handle the retry accordingly.
* This function should also update the $metadata from the response accordingly.
* @see {@link ResponseMetadata}
*/
retry: <Input extends object, Output extends MetadataBearer>(
next: FinalizeHandler<Input, Output>,
args: FinalizeHandlerArguments<Input>
) => Promise<FinalizeHandlerOutput<Output>>;
}

/**
Expand Down

0 comments on commit ff70fac

Please sign in to comment.