From 70ec86cbdaed4dceebc6e3ba5941c67ecd581e95 Mon Sep 17 00:00:00 2001 From: Santiago Palladino Date: Thu, 17 Aug 2023 11:33:47 -0300 Subject: [PATCH] feat(cli): retry on http errors --- yarn-project/aztec-cli/src/client.ts | 14 +++++++++ yarn-project/aztec-cli/src/index.ts | 30 +++++++++---------- .../foundation/src/json-rpc/client/index.ts | 1 + .../src/json-rpc/client/json_rpc_client.ts | 22 ++++++++++++-- yarn-project/foundation/src/retry/index.ts | 11 +++++++ 5 files changed, 61 insertions(+), 17 deletions(-) create mode 100644 yarn-project/aztec-cli/src/client.ts diff --git a/yarn-project/aztec-cli/src/client.ts b/yarn-project/aztec-cli/src/client.ts new file mode 100644 index 00000000000..1eb6ac3ccb3 --- /dev/null +++ b/yarn-project/aztec-cli/src/client.ts @@ -0,0 +1,14 @@ +import { createAztecRpcClient } from '@aztec/aztec.js'; +import { makeFetch } from '@aztec/foundation/json-rpc/client'; + +const retries = [1, 1, 2]; + +/** + * Creates an Aztec RPC client with a given set of retries on non-server errors. + * @param rpcUrl - URL of the RPC server. + * @returns An RPC client. + */ +export function createClient(rpcUrl: string) { + const fetch = makeFetch(retries, true); + return createAztecRpcClient(rpcUrl, fetch); +} diff --git a/yarn-project/aztec-cli/src/index.ts b/yarn-project/aztec-cli/src/index.ts index 6e64073d0bb..10e8bc37dda 100644 --- a/yarn-project/aztec-cli/src/index.ts +++ b/yarn-project/aztec-cli/src/index.ts @@ -4,7 +4,6 @@ import { ContractDeployer, Fr, Point, - createAztecRpcClient, generatePublicKey, getAccountWallets, getSchnorrAccount, @@ -23,6 +22,7 @@ import { dirname, resolve } from 'path'; import { fileURLToPath } from 'url'; import { mnemonicToAccount } from 'viem/accounts'; +import { createClient } from './client.js'; import { encodeArgs, parseStructString } from './encoding.js'; import { deployAztecContracts, @@ -127,7 +127,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { ) .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async options => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const privateKey = options.privateKey ? new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex')) : PrivateKey.random(); @@ -161,7 +161,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { const contractAbi = await getContractAbi(options.contractAbi, log); const constructorAbi = contractAbi.functions.find(({ name }) => name === 'constructor'); - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const publicKey = options.publicKey ? Point.fromString(options.publicKey) : undefined; const salt = options.salt ? Fr.fromBuffer(Buffer.from(stripLeadingHex(options.salt), 'hex')) : undefined; const deployer = new ContractDeployer(contractAbi, client, publicKey); @@ -189,7 +189,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .requiredOption('-ca, --contract-address
', 'An Aztec address to check if contract has been deployed to.') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async options => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const address = AztecAddress.fromString(options.contractAddress); const isDeployed = await isContractDeployed(client, address); if (isDeployed) log(`\nContract found at ${address.toString()}\n`); @@ -202,7 +202,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets the receipt for the specified transaction hash.') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (_txHash, options) => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const txHash = TxHash.fromString(_txHash); const receipt = await client.getTxReceipt(txHash); if (!receipt) { @@ -219,7 +219,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .option('-b, --include-bytecode ', "Include the contract's public function bytecode, if any.", false) .action(async (contractAddress, options) => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const address = AztecAddress.fromString(contractAddress); const contractDataWithOrWithoutBytecode = options.includeBytecode ? await client.getContractDataAndBytecode(address) @@ -255,7 +255,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { const fromBlock = from ? parseInt(from) : 1; const limitCount = limit ? parseInt(limit) : 100; - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const logs = await client.getUnencryptedLogs(fromBlock, limitCount); if (!logs.length) { log(`No logs found in blocks ${fromBlock} to ${fromBlock + limitCount}`); @@ -273,7 +273,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .requiredOption('-pa, --partial-address ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async options => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const address = AztecAddress.fromString(options.address); const publicKey = Point.fromString(options.publicKey); const partialAddress = Fr.fromString(options.partialAddress); @@ -287,7 +287,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets all the Aztec accounts stored in the Aztec RPC.') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (options: any) => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const accounts = await client.getAccounts(); if (!accounts.length) { log('No accounts found.'); @@ -305,7 +305,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .argument('
', 'The Aztec address to get account for') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (_address, options) => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const address = AztecAddress.fromString(_address); const account = await client.getAccount(address); @@ -321,7 +321,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets all the recipients stored in the Aztec RPC.') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (options: any) => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const recipients = await client.getRecipients(); if (!recipients.length) { log('No recipients found.'); @@ -339,7 +339,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .argument('
', 'The Aztec address to get recipient for') .option('-u, --rpc-url ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (_address, options) => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const address = AztecAddress.fromString(_address); const recipient = await client.getRecipient(address); @@ -381,7 +381,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { const privateKey = new PrivateKey(Buffer.from(stripLeadingHex(options.privateKey), 'hex')); - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const wallet = await getAccountWallets( client, SchnorrAccountContractAbi, @@ -428,7 +428,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { `Invalid number of args passed. Expected ${fnAbi.parameters.length}; Received: ${options.args.length}`, ); } - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const from = await getTxSender(client, options.from); const result = await client.viewTx(functionName, functionArgs, contractAddress, from); log('\nView result: ', JsonStringify(result, true), '\n'); @@ -463,7 +463,7 @@ export function getProgram(log: LogFn, debugLogger: DebugLogger): Command { .description('Gets the current Aztec L2 block number.') .option('-u, --rpcUrl ', 'URL of the Aztec RPC', AZTEC_RPC_HOST || 'http://localhost:8080') .action(async (options: any) => { - const client = createAztecRpcClient(options.rpcUrl); + const client = createClient(options.rpcUrl); const num = await client.getBlockNum(); log(`${num}\n`); }); diff --git a/yarn-project/foundation/src/json-rpc/client/index.ts b/yarn-project/foundation/src/json-rpc/client/index.ts index 786e07ee69e..6de7b7cd552 100644 --- a/yarn-project/foundation/src/json-rpc/client/index.ts +++ b/yarn-project/foundation/src/json-rpc/client/index.ts @@ -3,4 +3,5 @@ export { mustSucceedFetch, mustSucceedFetchUnlessNoRetry, defaultFetch, + makeFetch, } from './json_rpc_client.js'; diff --git a/yarn-project/foundation/src/json-rpc/client/json_rpc_client.ts b/yarn-project/foundation/src/json-rpc/client/json_rpc_client.ts index 9163396a482..a53799f7840 100644 --- a/yarn-project/foundation/src/json-rpc/client/json_rpc_client.ts +++ b/yarn-project/foundation/src/json-rpc/client/json_rpc_client.ts @@ -4,8 +4,8 @@ // while avoiding Promise of Promise. import { RemoteObject } from 'comlink'; -import { createDebugLogger } from '../../log/index.js'; -import { NoRetryError, retry } from '../../retry/index.js'; +import { DebugLogger, createDebugLogger } from '../../log/index.js'; +import { NoRetryError, makeBackoff, retry } from '../../retry/index.js'; import { ClassConverter, JsonClassConverterInput, StringClassConverterInput } from '../class_converter.js'; import { JsonStringify, convertFromJsonObj, convertToJsonObj } from '../convert.js'; @@ -72,6 +72,24 @@ export async function mustSucceedFetch(host: string, rpcMethod: string, body: an return await retry(() => defaultFetch(host, rpcMethod, body, useApiEndpoints), 'JsonRpcClient request'); } +/** + * Makes a fetch function that retries based on the given attempts. + * @param retries - Sequence of intervals (in seconds) to retry. + * @param noRetry - Whether to stop retries on server errors. + * @param log - Optional logger for logging attempts. + * @returns A fetch function. + */ +export function makeFetch(retries: number[], noRetry: boolean, log?: DebugLogger) { + return async (host: string, rpcMethod: string, body: any, useApiEndpoints: boolean) => { + return await retry( + () => defaultFetch(host, rpcMethod, body, useApiEndpoints, noRetry), + 'JsonRpcClient request', + makeBackoff(retries), + log, + ); + }; +} + /** * A fetch function with retries unless the error is a NoRetryError. */ diff --git a/yarn-project/foundation/src/retry/index.ts b/yarn-project/foundation/src/retry/index.ts index 4287678bffa..37200bef6d9 100644 --- a/yarn-project/foundation/src/retry/index.ts +++ b/yarn-project/foundation/src/retry/index.ts @@ -21,6 +21,17 @@ export function* backoffGenerator() { } } +/** + * Generates a backoff sequence based on the array of retry intervals to use with the `retry` function. + * @param retries - Intervals to retry (in seconds). + * @returns A generator sequence. + */ +export function* makeBackoff(retries: number[]) { + for (const retry of retries) { + yield retry; + } +} + /** * Retry a given asynchronous function with a specific backoff strategy, until it succeeds or backoff generator ends. * It logs the error and retry interval in case an error is caught. The function can be named for better log output.