Skip to content

Commit

Permalink
feat: add retry functionality in the api for builder publish
Browse files Browse the repository at this point in the history
  • Loading branch information
g11tech committed Feb 7, 2024
1 parent b6890ad commit f457a55
Show file tree
Hide file tree
Showing 6 changed files with 57 additions and 16 deletions.
2 changes: 1 addition & 1 deletion packages/api/src/beacon/client/beacon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function getClient(config: ChainForkConfig, httpClient: IHttpClient): Api
const reqSerializers = getReqSerializers(config);
const returnTypes = getReturnTypes();
// Some routes return JSON, use a client auto-generator
const client = generateGenericJsonClient<Api, ReqTypes>(routesData, reqSerializers, returnTypes, httpClient);
const client = generateGenericJsonClient<Api, ReqTypes>(routesData, reqSerializers, returnTypes, httpClient) as Api;
const fetchOptsSerializer = getFetchOptsSerializers<Api, ReqTypes>(routesData, reqSerializers);

return {
Expand Down
4 changes: 2 additions & 2 deletions packages/api/src/builder/client.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import {ChainForkConfig} from "@lodestar/config";
import {IHttpClient, generateGenericJsonClient} from "../utils/client/index.js";
import {IHttpClient, generateGenericJsonClient, ApiWithExtraOpts} from "../utils/client/index.js";
import {Api, ReqTypes, routesData, getReqSerializers, getReturnTypes} from "./routes.js";

/**
* REST HTTP client for builder routes
*/
export function getClient(config: ChainForkConfig, httpClient: IHttpClient): Api {
export function getClient(config: ChainForkConfig, httpClient: IHttpClient): ApiWithExtraOpts<Api> {
const reqSerializers = getReqSerializers(config);
const returnTypes = getReturnTypes();
// All routes return JSON, use a client auto-generator
Expand Down
13 changes: 9 additions & 4 deletions packages/api/src/builder/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
import {ChainForkConfig} from "@lodestar/config";
import {HttpClient, HttpClientModules, HttpClientOptions, IHttpClient} from "../utils/client/httpClient.js";
import {Api} from "./routes.js";
import {
HttpClient,
HttpClientModules,
HttpClientOptions,
IHttpClient,
ApiWithExtraOpts,
} from "../utils/client/index.js";
import {Api as BuilderApi} from "../builder/routes.js";
import * as builder from "./client.js";

// NOTE: Don't export server here so it's not bundled to all consumers

export type {Api};

// Note: build API does not have namespaces as routes are declared at the "root" namespace

export type Api = ApiWithExtraOpts<BuilderApi>;
type ClientModules = HttpClientModules & {
config: ChainForkConfig;
httpClient?: IHttpClient;
Expand Down
37 changes: 30 additions & 7 deletions packages/api/src/utils/client/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,17 @@ import {APIClientHandler} from "../../interfaces.js";
import {FetchOpts, HttpError, IHttpClient} from "./httpClient.js";
import {HttpStatusCode} from "./httpStatusCode.js";

// See /packages/api/src/routes/index.ts for reasoning

/* eslint-disable @typescript-eslint/no-explicit-any */

type ExtraOpts = {retryAttempts?: number};
type ParamatersWithOptionalExtaOpts<T extends (...args: any) => any> = [...Parameters<T>, ExtraOpts] | Parameters<T>;

export type ApiWithExtraOpts<T extends Record<string, APIClientHandler>> = {
[K in keyof T]: (...args: ParamatersWithOptionalExtaOpts<T[K]>) => ReturnType<T[K]>;
};

// See /packages/api/src/routes/index.ts for reasoning

/**
* Format FetchFn opts from Fn arguments given a route definition and request serializer.
* For routes that return only JSOn use @see getGenericJsonClient
Expand Down Expand Up @@ -58,22 +65,38 @@ export function generateGenericJsonClient<
reqSerializers: ReqSerializers<Api, ReqTypes>,
returnTypes: ReturnTypes<Api>,
fetchFn: IHttpClient
): Api {
): ApiWithExtraOpts<Api> {
return mapValues(routesData, (routeDef, routeId) => {
const fetchOptsSerializer = getFetchOptsSerializer(routeDef, reqSerializers[routeId], routeId as string);
const returnType = returnTypes[routeId as keyof ReturnTypes<Api>] as TypeJson<any> | null;

return async function request(...args: Parameters<Api[keyof Api]>): Promise<ReturnType<Api[keyof Api]>> {
return async function request(
...args: ParamatersWithOptionalExtaOpts<Api[keyof Api]>
): Promise<ReturnType<Api[keyof Api]>> {
try {
// extract the extraOpts if provided
//
const argLen = (args as any[])?.length ?? 0;
const lastArg = (args as any[])[argLen] as ExtraOpts | undefined;
const retryAttempts = lastArg?.retryAttempts;
const extraOpts = {retryAttempts};

if (returnType) {
const res = await fetchFn.json<unknown>(fetchOptsSerializer(...args));
// open extraOpts first if some serializer wants to add some overriding param
const res = await fetchFn.json<unknown>({
...extraOpts,
...fetchOptsSerializer(...(args as Parameters<Api[keyof Api]>)),
});
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/return-await
return {ok: true, response: returnType.fromJson(res.body), status: res.status} as ReturnType<Api[keyof Api]>;
} else {
// We need to avoid parsing the response as the servers might just
// response status 200 and close the request instead of writing an
// empty json response. We return the status code.
const res = await fetchFn.request(fetchOptsSerializer(...args));
const res = await fetchFn.request({
...extraOpts,
...fetchOptsSerializer(...(args as Parameters<Api[keyof Api]>)),
});

// eslint-disable-next-line @typescript-eslint/return-await
return {ok: true, response: undefined, status: res.status} as ReturnType<Api[keyof Api]>;
Expand All @@ -98,5 +121,5 @@ export function generateGenericJsonClient<
throw err;
}
};
}) as unknown as Api;
}) as unknown as ApiWithExtraOpts<Api>;
}
15 changes: 14 additions & 1 deletion packages/api/src/utils/client/httpClient.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {ErrorAborted, Logger, TimeoutError, isValidHttpUrl, toBase64} from "@lodestar/utils";
import {ErrorAborted, Logger, TimeoutError, isValidHttpUrl, toBase64, retry} from "@lodestar/utils";
import {ReqGeneric, RouteDef} from "../index.js";
import {ApiClientResponse, ApiClientSuccessResponse} from "../../interfaces.js";
import {fetch, isFetchError} from "./fetch.js";
Expand Down Expand Up @@ -70,6 +70,7 @@ export type FetchOpts = {
/** Optional, for metrics */
routeId?: string;
timeoutMs?: number;
retryAttempts?: number;
};

export interface IHttpClient {
Expand Down Expand Up @@ -181,6 +182,18 @@ export class HttpClient implements IHttpClient {
private async requestWithBodyWithRetries<T>(
opts: FetchOpts,
getBody: (res: Response) => Promise<T>
): Promise<{status: HttpStatusCode; body: T}> {
return retry(
async (_attempt) => {
return this.requestWithBodyWithFallbacks<T>(opts, getBody);
},
{retries: opts?.retryAttempts ?? 1, retryDelay: 10}
);
}

private async requestWithBodyWithFallbacks<T>(
opts: FetchOpts,
getBody: (res: Response) => Promise<T>
): Promise<{status: HttpStatusCode; body: T}> {
// Early return when no fallback URLs are setup
if (this.urlsOpts.length === 1) {
Expand Down
2 changes: 1 addition & 1 deletion packages/beacon-node/src/execution/builder/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class ExecutionBuilderHttp implements IExecutionBuilder {
async submitBlindedBlock(
signedBlindedBlock: allForks.SignedBlindedBeaconBlock
): Promise<allForks.SignedBeaconBlockOrContents> {
const res = await this.api.submitBlindedBlock(signedBlindedBlock);
const res = await this.api.submitBlindedBlock(signedBlindedBlock, {retryAttempts: 3});
ApiError.assert(res, "execution.builder.submitBlindedBlock");
const {data} = res.response;

Expand Down

0 comments on commit f457a55

Please sign in to comment.