Skip to content

Commit 9bfa0b0

Browse files
committed
add Exponential Backoff with Jitter to loadtest --wait-for-receipt
1 parent 073e7d3 commit 9bfa0b0

File tree

5 files changed

+83
-8
lines changed

5 files changed

+83
-8
lines changed

cmd/loadtest/account.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,7 @@ func (ap *AccountPool) FundAccounts(ctx context.Context) error {
445445
Str("txHash", tx.Hash().Hex()).
446446
Msg("transaction to fund account sent")
447447

448-
receipt, err := waitReceipt(ctx, ap.client, tx.Hash(), time.Minute)
448+
receipt, err := waitReceipt(ctx, ap.client, tx.Hash())
449449
if err != nil {
450450
log.Error().
451451
Str("address", tx.To().Hex()).
@@ -670,7 +670,7 @@ func (ap *AccountPool) ReturnFunds(ctx context.Context) error {
670670
Str("txHash", tx.Hash().Hex()).
671671
Msg("transaction to return funds sent")
672672

673-
_, err = waitReceipt(ctx, ap.client, tx.Hash(), time.Minute)
673+
_, err = waitReceiptWithTimeout(ctx, ap.client, tx.Hash(), time.Minute)
674674
if err != nil {
675675
log.Error().
676676
Str("address", tx.To().Hex()).
@@ -852,7 +852,7 @@ func (ap *AccountPool) fund(ctx context.Context, acc Account, forcedNonce *uint6
852852

853853
// Wait for the transaction to be mined
854854
if waitToFund {
855-
receipt, err := waitReceipt(ctx, ap.client, signedTx.Hash(), time.Minute)
855+
receipt, err := waitReceipt(ctx, ap.client, signedTx.Hash())
856856
if err != nil {
857857
log.Error().
858858
Str("address", acc.address.Hex()).

cmd/loadtest/app.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ type (
8989
SendingAddressesFile *string
9090
Proxy *string
9191
WaitForReceipt *bool
92+
ReceiptRetryMax *uint
93+
ReceiptRetryInitialDelayMs *uint
9294

9395
// Computed
9496
CurrentGasPrice *big.Int
@@ -287,6 +289,8 @@ v3, uniswapv3 - Perform UniswapV3 swaps`)
287289
ltp.InscriptionContent = LoadtestCmd.Flags().String("inscription-content", `data:,{"p":"erc-20","op":"mint","tick":"TEST","amt":"1"}`, "The inscription content that will be encoded as calldata. This must be paired up with --mode inscription")
288290
ltp.Proxy = LoadtestCmd.Flags().String("proxy", "", "Use the proxy specified")
289291
ltp.WaitForReceipt = LoadtestCmd.Flags().Bool("wait-for-receipt", false, "If set to true, the load test will wait for the transaction receipt to be mined. If set to false, the load test will not wait for the transaction receipt and will just send the transaction.")
292+
ltp.ReceiptRetryMax = LoadtestCmd.Flags().Uint("receipt-retry-max", 30, "Maximum number of attempts to poll for transaction receipt when --wait-for-receipt is enabled.")
293+
ltp.ReceiptRetryInitialDelayMs = LoadtestCmd.Flags().Uint("receipt-retry-initial-delay-ms", 100, "Initial delay in milliseconds for receipt polling retry. Uses exponential backoff with jitter.")
290294

291295
inputLoadTestParams = *ltp
292296

cmd/loadtest/loadtest.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -900,7 +900,9 @@ func mainLoop(ctx context.Context, c *ethclient.Client, rpc *ethrpc.Client) erro
900900
recordSample(routineID, requestID, tErr, startReq, endReq, sendingTops.Nonce.Uint64())
901901
}
902902
if tErr == nil && *inputLoadTestParams.WaitForReceipt {
903-
_, tErr = waitReceipt(ctx, c, ltTxHash, time.Minute)
903+
receiptMaxRetries := *inputLoadTestParams.ReceiptRetryMax
904+
receiptRetryInitialDelayMs := *inputLoadTestParams.ReceiptRetryInitialDelayMs
905+
_, tErr = waitReceiptWithRetries(ctx, c, ltTxHash, receiptMaxRetries, receiptRetryInitialDelayMs)
904906
}
905907

906908
if tErr != nil {

cmd/loadtest/tx.go

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,83 @@ package loadtest
22

33
import (
44
"context"
5+
"fmt"
6+
"math/rand"
57
"time"
68

7-
"github.com/ethereum/go-ethereum/accounts/abi/bind"
89
"github.com/ethereum/go-ethereum/common"
910
"github.com/ethereum/go-ethereum/core/types"
1011
"github.com/ethereum/go-ethereum/ethclient"
1112
)
1213

13-
func waitReceipt(ctx context.Context, client *ethclient.Client, txHash common.Hash, timeout time.Duration) (*types.Receipt, error) {
14-
ctxTimeout, cancel := context.WithTimeout(ctx, timeout)
14+
// waitReceipt waits for a transaction receipt with default parameters.
15+
func waitReceipt(ctx context.Context, client *ethclient.Client, txHash common.Hash) (*types.Receipt, error) {
16+
return internalWaitReceipt(ctx, client, txHash, 0, 0, 0)
17+
}
18+
19+
// waitReceiptWithRetries waits for a transaction receipt with retries and exponential backoff.
20+
func waitReceiptWithRetries(ctx context.Context, client *ethclient.Client, txHash common.Hash, maxRetries uint, initialDelayMs uint) (*types.Receipt, error) {
21+
return internalWaitReceipt(ctx, client, txHash, maxRetries, initialDelayMs, 0)
22+
}
23+
24+
// waitReceiptWithTimeout waits for a transaction receipt with a specified timeout.
25+
func waitReceiptWithTimeout(ctx context.Context, client *ethclient.Client, txHash common.Hash, timeout time.Duration) (*types.Receipt, error) {
26+
return internalWaitReceipt(ctx, client, txHash, 0, 0, timeout)
27+
}
28+
29+
// waitReceiptWithRetriesAndTimeout waits for a transaction receipt with retries, exponential backoff, and a timeout.
30+
func internalWaitReceipt(ctx context.Context, client *ethclient.Client, txHash common.Hash, maxRetries uint, initialDelayMs uint, timeout time.Duration) (*types.Receipt, error) {
31+
// Set defaults for zero values
32+
effectiveTimeout := timeout
33+
if effectiveTimeout == 0 {
34+
effectiveTimeout = 1 * time.Minute // Default: 1 minute
35+
}
36+
37+
// Create context with timeout
38+
timeoutCtx, cancel := context.WithTimeout(ctx, effectiveTimeout)
1539
defer cancel()
16-
return bind.WaitMinedHash(ctxTimeout, client, txHash)
40+
41+
effectiveInitialDelayMs := initialDelayMs
42+
if effectiveInitialDelayMs == 0 {
43+
effectiveInitialDelayMs = 100 // Default: 100ms
44+
} else if effectiveInitialDelayMs < 10 {
45+
effectiveInitialDelayMs = 10 // Minimum 10ms
46+
}
47+
48+
for attempt := uint(0); ; attempt++ {
49+
receipt, err := client.TransactionReceipt(timeoutCtx, txHash)
50+
if err == nil && receipt != nil {
51+
return receipt, nil
52+
}
53+
54+
// If maxRetries > 0 and we've reached the limit, exit
55+
// Note: effectiveMaxRetries is always > 0 due to default above
56+
if maxRetries > 0 && attempt >= maxRetries-1 {
57+
return nil, fmt.Errorf("failed to get receipt after %d attempts: %w", maxRetries, err)
58+
}
59+
60+
// Calculate delay
61+
baseDelay := time.Duration(effectiveInitialDelayMs) * time.Millisecond
62+
exponentialDelay := baseDelay * time.Duration(1<<attempt)
63+
64+
// Add cap to prevent extremely long delays
65+
maxDelay := 30 * time.Second
66+
if exponentialDelay > maxDelay {
67+
exponentialDelay = maxDelay
68+
}
69+
70+
maxJitter := exponentialDelay / 2
71+
if maxJitter <= 0 {
72+
maxJitter = 1 * time.Millisecond
73+
}
74+
jitter := time.Duration(rand.Int63n(int64(maxJitter)))
75+
totalDelay := exponentialDelay + jitter
76+
77+
select {
78+
case <-timeoutCtx.Done():
79+
return nil, timeoutCtx.Err()
80+
case <-time.After(totalDelay):
81+
// Continue
82+
}
83+
}
1784
}

doc/polycli_loadtest.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -157,6 +157,8 @@ The codebase has a contract that used for load testing. It's written in Solidity
157157
--proxy string Use the proxy specified
158158
--rate-limit float An overall limit to the number of requests per second. Give a number less than zero to remove this limit all together (default 4)
159159
--recall-blocks uint The number of blocks that we'll attempt to fetch for recall (default 50)
160+
--receipt-retry-initial-delay-ms uint Initial delay in milliseconds for receipt polling retry. Uses exponential backoff with jitter. (default 100)
161+
--receipt-retry-max uint Maximum number of attempts to poll for transaction receipt when --wait-for-receipt is enabled. (default 30)
160162
-n, --requests int Number of requests to perform for the benchmarking session. The default is to just perform a single request which usually leads to non-representative benchmarking results. (default 1)
161163
-r, --rpc-url string The RPC endpoint url (default "http://localhost:8545")
162164
--seed int A seed for generating random values and addresses (default 123456)

0 commit comments

Comments
 (0)