fix(loadtest): bound parallel nonce fetch by --concurrency - #998
Merged
Conversation
FetchNoncesInParallel spawned one goroutine per account and issued every eth_getTransactionCount at once, so --concurrency did not apply to the heaviest read burst the tool produces. With a 10,000-account --sending-accounts-file the endpoint received ~10k concurrent requests in the first seconds of a run, which load-balanced and managed endpoints answer with 429s / 5xx, aborting the run before anything is sent. Acquire a semaphore slot before spawning each fetch so both in-flight requests and live goroutines stay capped at min(--concurrency, N); values <= 0 clamp to 1. Each fetch now retries up to 5 times with exponential backoff (250ms -> 4s) so transient load shedding no longer fails the run, and both the acquire loop and the backoff select on ctx.Done() so Ctrl+C returns promptly. --sequential-nonce-fetch is unchanged; its help text now describes what the two paths actually do. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
praetoriansentry
previously approved these changes
Aug 26, 2026
Satisfies errcheck. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
minhd-vu
enabled auto-merge (squash)
August 26, 2026 20:50
praetoriansentry
approved these changes
Aug 26, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
FetchNoncesInParallelspawned one goroutine per account and fired everyeth_getTransactionCountsimultaneously, so--concurrency(and--rate-limit) did not apply to the heaviest read burst the tool produces. With a 10,000-account--sending-accounts-file, the RPC endpoint receives ~10k concurrent requests in the first seconds of a run.Observed against a load-balanced Amoy gateway: the sweep drew HTTP 500s from the GCP frontend and the run aborted before sending anything. A local bor node absorbs the same sweep fine, so this mainly breaks runs pointed at load-balanced/managed endpoints — exactly the ones a
--send-rpc-urlsplit is meant to enable. The existing workaround,--sequential-nonce-fetch, is one-at-a-time and impractical for 10k accounts over a WAN.Change
min(--concurrency, len(accountsToFetch)). Values<= 0clamp to 1.fetchNonceWithRetry: up to 5 attempts with exponential backoff (250ms → 4s), so transient 429/5xx load shedding no longer aborts the run.ctx.Done(), and the backoff usestime.NewTimer+Stop()rather thantime.After, so Ctrl+C returns promptly without leaking timers.--sequential-nonce-fetchbehavior is unchanged; its help text now describes what the two paths actually do ("one at a time through the rate limiter" vs "in parallel bounded by--concurrency").Testing
New
loadtest/account_test.gouses an httptest JSON-RPC server that records peak concurrent nonce calls and can inject failures. It covers: concurrency bound respected, 0/negative clamping to 1, retry-then-succeed, failure after max attempts, cancellation during backoff, and the no-work case issuing zero RPCs.go test -racepasses;go vet,gofmt, andgolangci-lintare clean.Manually verified against a public Amoy endpoint with a counting HTTP proxy in between to measure peak concurrent
eth_getTransactionCount(2,000-account file):--concurrencymainPeak matches
--concurrencyexactly.main's 200 is a floor rather than its real ceiling — the proxy could not accept faster, so the remainder sat in the socket backlog uncounted.Direct against the endpoint, no proxy:
-c 200: sweep completed in 4s, zero errors, zero retries.mainunbounded on the same file: 3s — so the bound costs roughly 1s at 10k scale.-c 100: 2s.-c 1on 50 accounts: 2s, log confirmsconcurrency: 1.--sequential-nonce-fetchstill reaches "All accounts are ready" as before.The retry path also exercised itself for real: while proxied, the proxy intermittently 500'd under its own load and the sweep absorbed it — 2,101 nonce calls for 2,000 accounts at
-c 100(101 retries) and 2,027 at-c 25, both finishing successfully.Caveat: I could not reproduce the original failure on the public endpoint I tested — it absorbed the full unbounded 10k sweep without a single error, so
mainnever broke there. The reported 500s came from a private gateway's GCP frontend; confirming the fix against that specific endpoint is still worth doing.🤖 Generated with Claude Code