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

fix: request retries are abortable #6466

Merged
merged 4 commits into from
Feb 22, 2024
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
1 change: 1 addition & 0 deletions packages/api/src/utils/client/httpClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@ export class HttpClient implements IHttpClient {
{
retries: opts.retries,
retryDelay: 200,
signal: this.getAbortSignal?.(),
onRetry: (e, attempt) => {
this.logger?.debug("Retrying request", {routeId, attempt, lastError: e.message});
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ export class JsonRpcHttpClient implements IJsonRpcHttpClient {
},
{
retries: opts?.retries ?? this.opts?.retries ?? 0,
retryDelay: opts?.retryDelay ?? this.opts?.retryDelay ?? 0,
retryDelay: opts?.retryDelay ?? this.opts?.retryDelay,
shouldRetry: opts?.shouldRetry,
signal: this.opts?.signal,
onRetry: () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,7 @@ describe("eth1 / jsonRpcHttpClient - with retries", function () {
const url = `http://localhost:${port}`;
const payload = {method: "get", params: []};
const retries = 2;
const timeout = 2000;
const timeout = 200;

const controller = new AbortController();
const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
Expand All @@ -287,7 +287,7 @@ describe("eth1 / jsonRpcHttpClient - with retries", function () {
expect(requestCount).toBeWithMessage(retries + 1, "Timeout request should be retried before failing");
});

it("should retry aborted", async function () {
it("should not retry aborted", async function () {
let requestCount = 0;
const server = http.createServer(() => {
requestCount++;
Expand All @@ -308,13 +308,13 @@ describe("eth1 / jsonRpcHttpClient - with retries", function () {
const url = `http://localhost:${port}`;
const payload = {method: "get", params: []};
const retries = 2;
const timeout = 2000;
const timeout = 200;

const controller = new AbortController();
setTimeout(() => controller.abort(), 50);
const eth1JsonRpcClient = new JsonRpcHttpClient([url], {signal: controller.signal});
await expect(eth1JsonRpcClient.fetchWithRetries(payload, {retries, timeout})).rejects.toThrow("Aborted");
expect(requestCount).toBeWithMessage(1, "Aborted request should be retried before failing");
expect(requestCount).toBeWithMessage(1, "Aborted request should not be retried");
});

it("should not retry payload error", async function () {
Expand Down
16 changes: 13 additions & 3 deletions packages/utils/src/retry.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {ErrorAborted} from "./errors.js";
import {sleep} from "./sleep.js";

export type RetryOptions = {
Expand All @@ -21,6 +22,9 @@ export type RetryOptions = {
* Milliseconds to wait before retrying again
*/
retryDelay?: number;
/**
* Abort signal to stop retrying
*/
signal?: AbortSignal;
};

Expand All @@ -39,10 +43,16 @@ export async function retry<A>(fn: (attempt: number) => A | Promise<A>, opts?: R

let lastError: Error = Error("RetryError");
for (let i = 1; i <= maxAttempts; i++) {
try {
// If not the first attempt, invoke right before retrying
if (i > 1) onRetry?.(lastError, i);
// If not the first attempt
if (i > 1) {
if (opts?.signal?.aborted) {
throw new ErrorAborted("retry");
}
// Invoke right before retrying
onRetry?.(lastError, i);
}

try {
return await fn(i);
} catch (e) {
lastError = e as Error;
Expand Down
Loading