diff --git a/apps/entropy-tester/package.json b/apps/entropy-tester/package.json index c5697b75dd..a297001186 100644 --- a/apps/entropy-tester/package.json +++ b/apps/entropy-tester/package.json @@ -1,6 +1,6 @@ { "name": "@pythnetwork/entropy-tester", - "version": "1.1.0", + "version": "1.2.0", "description": "Utility to test entropy provider callbacks", "private": true, "type": "module", diff --git a/apps/entropy-tester/src/index.ts b/apps/entropy-tester/src/index.ts index ecadcf90d8..34fcc6bd75 100644 --- a/apps/entropy-tester/src/index.ts +++ b/apps/entropy-tester/src/index.ts @@ -11,9 +11,12 @@ import yargs from "yargs"; import { hideBin } from "yargs/helpers"; import { z } from "zod"; +const DEFAULT_RETRIES = 3; + type LoadedConfig = { contract: EvmEntropyContract; interval: number; + retries: number; }; function timeToSeconds(timeStr: string): number { @@ -46,6 +49,7 @@ async function loadConfig(configPath: string): Promise { "chain-id": z.string(), interval: z.string(), "rpc-endpoint": z.string().optional(), + retries: z.number().default(DEFAULT_RETRIES), }), ); const configContent = (await import(configPath, { @@ -78,7 +82,7 @@ async function loadConfig(configPath: string): Promise { evmChain.networkId, ); } - return { contract: firstContract, interval }; + return { contract: firstContract, interval, retries: config.retries }; }); return loadedConfigs; } @@ -188,31 +192,63 @@ export const main = function () { privateKeyFileContent.replace("0x", "").trimEnd(), ); logger.info("Running"); - const promises = configs.map(async ({ contract, interval }) => { - const child = logger.child({ chain: contract.chain.getId() }); - // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - while (true) { - try { - await Promise.race([ - testLatency(contract, privateKey, child), - new Promise((_, reject) => - setTimeout(() => { - reject( - new Error( - "Timeout: 120s passed but testLatency function was not resolved", - ), + const promises = configs.map( + async ({ contract, interval, retries }) => { + const child = logger.child({ chain: contract.chain.getId() }); + // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition + while (true) { + let lastError: Error | undefined; + let success = false; + + for (let attempt = 1; attempt <= retries; attempt++) { + try { + await Promise.race([ + testLatency(contract, privateKey, child), + new Promise((_, reject) => + setTimeout(() => { + reject( + new Error( + "Timeout: 120s passed but testLatency function was not resolved", + ), + ); + }, 120_000), + ), + ]); + success = true; + break; + } catch (error) { + lastError = error as Error; + child.warn( + { attempt, maxRetries: retries, error: error }, + `Attempt ${attempt.toString()}/${retries.toString()} failed, ${attempt < retries ? "retrying..." : "all retries exhausted"}`, + ); + + if (attempt < retries) { + // Wait a bit before retrying (exponential backoff, max 10s) + const backoffDelay = Math.min( + 2000 * Math.pow(2, attempt - 1), + 10_000, + ); + await new Promise((resolve) => + setTimeout(resolve, backoffDelay), ); - }, 120_000), - ), - ]); - } catch (error) { - child.error(error, "Error testing latency"); + } + } + } + + if (!success && lastError) { + child.error( + { error: lastError, retriesExhausted: retries }, + "All retries exhausted, callback was not called.", + ); + } + + await new Promise((resolve) => + setTimeout(resolve, interval * 1000), + ); } - await new Promise((resolve) => - setTimeout(resolve, interval * 1000), - ); - } - }); + }, + ); await Promise.all(promises); }, )