diff --git a/backend/src/api/bitcoin/esplora-api.ts b/backend/src/api/bitcoin/esplora-api.ts index ee7fa4765..ff6219587 100644 --- a/backend/src/api/bitcoin/esplora-api.ts +++ b/backend/src/api/bitcoin/esplora-api.ts @@ -3,102 +3,68 @@ import axios, { AxiosRequestConfig } from 'axios'; import http from 'http'; import { AbstractBitcoinApi } from './bitcoin-api-abstract-factory'; import { IEsploraApi } from './esplora-api.interface'; -import logger from '../../logger'; const axiosConnection = axios.create({ httpAgent: new http.Agent({ keepAlive: true, }) }); class ElectrsApi implements AbstractBitcoinApi { - private axiosConfigWithUnixSocket: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? { + axiosConfig: AxiosRequestConfig = config.ESPLORA.UNIX_SOCKET_PATH ? { socketPath: config.ESPLORA.UNIX_SOCKET_PATH, timeout: 10000, } : { timeout: 10000, }; - private axiosConfigTcpSocketOnly: AxiosRequestConfig = { - timeout: 10000, - }; - unixSocketRetryTimeout; - activeAxiosConfig; - - constructor() { - this.activeAxiosConfig = this.axiosConfigWithUnixSocket; - } - - fallbackToTcpSocket() { - if (!this.unixSocketRetryTimeout) { - logger.err(`Unable to connect to esplora unix socket. Falling back to tcp socket. Retrying unix socket in ${config.ESPLORA.RETRY_UNIX_SOCKET_AFTER / 1000} seconds`); - // Retry the unix socket after a few seconds - this.unixSocketRetryTimeout = setTimeout(() => { - logger.info(`Retrying to use unix socket for esplora now (applied for the next query)`); - this.activeAxiosConfig = this.axiosConfigWithUnixSocket; - this.unixSocketRetryTimeout = undefined; - }, config.ESPLORA.RETRY_UNIX_SOCKET_AFTER); - } - - // Use the TCP socket (reach a different esplora instance through nginx) - this.activeAxiosConfig = this.axiosConfigTcpSocketOnly; - } - - $queryWrapper(url, responseType = 'json'): Promise { - return axiosConnection.get(url, { ...this.activeAxiosConfig, responseType: responseType }) - .then((response) => response.data) - .catch((e) => { - if (e?.code === 'ECONNREFUSED') { - this.fallbackToTcpSocket(); - // Retry immediately - return axiosConnection.get(url, this.activeAxiosConfig) - .then((response) => response.data) - .catch((e) => { - logger.warn(`Cannot query esplora through the unix socket nor the tcp socket. Exception ${e}`); - throw e; - }); - } else { - throw e; - } - }); - } + constructor() { } $getRawMempool(): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/mempool/txids'); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/mempool/txids', this.axiosConfig) + .then((response) => response.data); } $getRawTransaction(txId: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/tx/' + txId, this.axiosConfig) + .then((response) => response.data); } $getTransactionHex(txId: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex'); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/hex', this.axiosConfig) + .then((response) => response.data); } $getBlockHeightTip(): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/blocks/tip/height'); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/blocks/tip/height', this.axiosConfig) + .then((response) => response.data); } $getBlockHashTip(): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/blocks/tip/hash'); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/blocks/tip/hash', this.axiosConfig) + .then((response) => response.data); } $getTxIdsForBlock(hash: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids'); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/block/' + hash + '/txids', this.axiosConfig) + .then((response) => response.data); } $getBlockHash(height: number): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block-height/' + height); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/block-height/' + height, this.axiosConfig) + .then((response) => response.data); } $getBlockHeader(hash: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header'); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/block/' + hash + '/header', this.axiosConfig) + .then((response) => response.data); } $getBlock(hash: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/block/' + hash, this.axiosConfig) + .then((response) => response.data); } $getRawBlock(hash: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", 'arraybuffer') + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/block/' + hash + "/raw", { ...this.axiosConfig, responseType: 'arraybuffer' }) .then((response) => { return Buffer.from(response.data); }); } @@ -119,11 +85,13 @@ class ElectrsApi implements AbstractBitcoinApi { } $getOutspend(txId: string, vout: number): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspend/' + vout, this.axiosConfig) + .then((response) => response.data); } $getOutspends(txId: string): Promise { - return this.$queryWrapper(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends'); + return axiosConnection.get(config.ESPLORA.REST_API_URL + '/tx/' + txId + '/outspends', this.axiosConfig) + .then((response) => response.data); } async $getBatchedOutspends(txId: string[]): Promise { diff --git a/backend/src/index.ts b/backend/src/index.ts index abaec9cef..a7f805313 100644 --- a/backend/src/index.ts +++ b/backend/src/index.ts @@ -45,8 +45,7 @@ class Server { private wss: WebSocket.Server | undefined; private server: http.Server | undefined; private app: Application; - private currentBackendRetryInterval = 1; - private backendRetryCount = 0; + private currentBackendRetryInterval = 5; private maxHeapSize: number = 0; private heapLogInterval: number = 60; @@ -185,17 +184,17 @@ class Server { indexer.$run(); setTimeout(this.runMainUpdateLoop.bind(this), config.MEMPOOL.POLL_RATE_MS); - this.backendRetryCount = 0; + this.currentBackendRetryInterval = 5; } catch (e: any) { - this.backendRetryCount++; - let loggerMsg = `Exception in runMainUpdateLoop() (count: ${this.backendRetryCount}). Retrying in ${this.currentBackendRetryInterval} sec.`; + let loggerMsg = `Exception in runMainUpdateLoop(). Retrying in ${this.currentBackendRetryInterval} sec.`; loggerMsg += ` Reason: ${(e instanceof Error ? e.message : e)}.`; if (e?.stack) { loggerMsg += ` Stack trace: ${e.stack}`; } // When we get a first Exception, only `logger.debug` it and retry after 5 seconds // From the second Exception, `logger.warn` the Exception and increase the retry delay - if (this.backendRetryCount >= 5) { + // Maximum retry delay is 60 seconds + if (this.currentBackendRetryInterval > 5) { logger.warn(loggerMsg); mempool.setOutOfSync(); } else { @@ -205,6 +204,8 @@ class Server { logger.debug(`AxiosError: ${e?.message}`); } setTimeout(this.runMainUpdateLoop.bind(this), 1000 * this.currentBackendRetryInterval); + this.currentBackendRetryInterval *= 2; + this.currentBackendRetryInterval = Math.min(this.currentBackendRetryInterval, 60); } }