From 37b718af9e1e37dad4570d7932a25242319e506e Mon Sep 17 00:00:00 2001 From: Jakub Date: Thu, 28 Nov 2024 00:38:51 +0100 Subject: [PATCH 1/2] Add retry logic to tBTC API calls We observed that the tBTC API calls are failing intermittently, which may be related to unstable Cloudflare workers infrastructure. This PR adds retry logic to the tBTC API calls to handle these failures. Requests to the tBTC API will be retried up to 5 times with an exponential backoff strategy. --- sdk/src/lib/api/HttpApi.ts | 39 +++++++++++++++++++++++++++++++++++++- sdk/src/lib/api/TbtcApi.ts | 32 +++++++++++++++---------------- 2 files changed, 53 insertions(+), 18 deletions(-) diff --git a/sdk/src/lib/api/HttpApi.ts b/sdk/src/lib/api/HttpApi.ts index d1759d6d2..696156715 100644 --- a/sdk/src/lib/api/HttpApi.ts +++ b/sdk/src/lib/api/HttpApi.ts @@ -1,11 +1,48 @@ +import { backoffRetrier, RetryOptions } from "../utils" + /** * Represents an abstract HTTP API. */ export default abstract class HttpApi { #apiUrl: string - constructor(apiUrl: string) { + /** + * Retry options for API requests. + */ + #retryOptions: RetryOptions + + constructor( + apiUrl: string, + retryOptions: RetryOptions = { + retries: 5, + backoffStepMs: 1000, + }, + ) { this.#apiUrl = apiUrl + this.#retryOptions = retryOptions + } + + /** + * Makes an HTTP request with retry logic. + * @param requestFn Function that returns a Promise of the HTTP response. + * @returns The HTTP response. + * @throws Error if the request fails after all retries. + */ + async requestWithRetry( + requestFn: () => Promise, + ): Promise { + return backoffRetrier( + this.#retryOptions.retries, + this.#retryOptions.backoffStepMs, + )(async () => { + const response = await requestFn() + + if (!response.ok) { + throw new Error(`Request failed: ${await response.text()}`) + } + + return response + }) } /** diff --git a/sdk/src/lib/api/TbtcApi.ts b/sdk/src/lib/api/TbtcApi.ts index 931e36601..a1baf46c4 100644 --- a/sdk/src/lib/api/TbtcApi.ts +++ b/sdk/src/lib/api/TbtcApi.ts @@ -38,12 +38,11 @@ export default class TbtcApi extends HttpApi { * otherwise. */ async saveReveal(revealData: SaveRevealRequest): Promise { - const response = await this.postRequest("reveals", revealData) - - if (!response.ok) - throw new Error( - `Reveal not saved properly in the database, response: ${response.status}`, - ) + const response = await this.requestWithRetry(async () => + this.postRequest("reveals", revealData), + ).catch((error) => { + throw new Error(`Failed to save reveal: ${error}`) + }) const { success } = (await response.json()) as { success: boolean } @@ -60,11 +59,11 @@ export default class TbtcApi extends HttpApi { depositStatus: DepositStatus fundingOutpoint: BitcoinTxOutpoint }> { - const response = await this.postRequest("deposits", depositData) - if (!response.ok) - throw new Error( - `Bitcoin deposit creation failed, response: ${response.status}`, - ) + const response = await this.requestWithRetry(async () => + this.postRequest("deposits", depositData), + ).catch((error) => { + throw new Error(`Failed to create deposit: ${error}`) + }) const responseData = (await response.json()) as CreateDepositResponse @@ -83,12 +82,11 @@ export default class TbtcApi extends HttpApi { * @returns All owner deposits, including queued deposits. */ async getDepositsByOwner(depositOwner: ChainIdentifier): Promise { - const response = await this.getRequest( - `deposits/${depositOwner.identifierHex}`, - ) - - if (!response.ok) - throw new Error(`Failed to fetch deposits: ${response.status}`) + const response = await this.requestWithRetry(async () => + this.getRequest(`deposits/${depositOwner.identifierHex}`), + ).catch((error) => { + throw new Error(`Failed to fetch deposits: ${error}`) + }) const responseData = (await response.json()) as Deposit[] From 2196fe39c1555a809a1668e9ca15813821f18528 Mon Sep 17 00:00:00 2001 From: Jakub Nowakowski Date: Fri, 6 Dec 2024 10:49:38 +0100 Subject: [PATCH 2/2] Enable retries for all HTTP requests in SDK Previously we were only retrying HTTP requests in the SDK for tBTC API calls. We can generalize this solution and apply it to all HTTP requests. --- sdk/src/lib/api/HttpApi.ts | 28 ++++++++++++++++------------ sdk/src/lib/api/TbtcApi.ts | 24 ++++++++++++------------ 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/sdk/src/lib/api/HttpApi.ts b/sdk/src/lib/api/HttpApi.ts index 696156715..eb9be45ca 100644 --- a/sdk/src/lib/api/HttpApi.ts +++ b/sdk/src/lib/api/HttpApi.ts @@ -28,7 +28,7 @@ export default abstract class HttpApi { * @returns The HTTP response. * @throws Error if the request fails after all retries. */ - async requestWithRetry( + async #requestWithRetry( requestFn: () => Promise, ): Promise { return backoffRetrier( @@ -55,10 +55,12 @@ export default abstract class HttpApi { endpoint: string, requestInit?: RequestInit, ): Promise { - return fetch(new URL(endpoint, this.#apiUrl), { - credentials: "include", - ...requestInit, - }) + return this.#requestWithRetry(async () => + fetch(new URL(endpoint, this.#apiUrl), { + credentials: "include", + ...requestInit, + }), + ) } /** @@ -73,12 +75,14 @@ export default abstract class HttpApi { body: unknown, requestInit?: RequestInit, ): Promise { - return fetch(new URL(endpoint, this.#apiUrl), { - method: "POST", - body: JSON.stringify(body), - credentials: "include", - headers: { "Content-Type": "application/json" }, - ...requestInit, - }) + return this.#requestWithRetry(async () => + fetch(new URL(endpoint, this.#apiUrl), { + method: "POST", + body: JSON.stringify(body), + credentials: "include", + headers: { "Content-Type": "application/json" }, + ...requestInit, + }), + ) } } diff --git a/sdk/src/lib/api/TbtcApi.ts b/sdk/src/lib/api/TbtcApi.ts index a1baf46c4..c4ce503c4 100644 --- a/sdk/src/lib/api/TbtcApi.ts +++ b/sdk/src/lib/api/TbtcApi.ts @@ -38,11 +38,11 @@ export default class TbtcApi extends HttpApi { * otherwise. */ async saveReveal(revealData: SaveRevealRequest): Promise { - const response = await this.requestWithRetry(async () => - this.postRequest("reveals", revealData), - ).catch((error) => { - throw new Error(`Failed to save reveal: ${error}`) - }) + const response = await this.postRequest("reveals", revealData).catch( + (error) => { + throw new Error(`Failed to save reveal: ${error}`) + }, + ) const { success } = (await response.json()) as { success: boolean } @@ -59,11 +59,11 @@ export default class TbtcApi extends HttpApi { depositStatus: DepositStatus fundingOutpoint: BitcoinTxOutpoint }> { - const response = await this.requestWithRetry(async () => - this.postRequest("deposits", depositData), - ).catch((error) => { - throw new Error(`Failed to create deposit: ${error}`) - }) + const response = await this.postRequest("deposits", depositData).catch( + (error) => { + throw new Error(`Failed to create deposit: ${error}`) + }, + ) const responseData = (await response.json()) as CreateDepositResponse @@ -82,8 +82,8 @@ export default class TbtcApi extends HttpApi { * @returns All owner deposits, including queued deposits. */ async getDepositsByOwner(depositOwner: ChainIdentifier): Promise { - const response = await this.requestWithRetry(async () => - this.getRequest(`deposits/${depositOwner.identifierHex}`), + const response = await this.getRequest( + `deposits/${depositOwner.identifierHex}`, ).catch((error) => { throw new Error(`Failed to fetch deposits: ${error}`) })