Skip to content

feat: add RetryStrategy class and retryMiddleware implementation #389

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

Merged
merged 2 commits into from
Oct 15, 2019
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
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