Skip to content

feat(loadgen): adding loadgen binary#3335

Merged
chatton merged 20 commits into
mainfrom
cian/benchmarks-binary
Jun 11, 2026
Merged

feat(loadgen): adding loadgen binary#3335
chatton merged 20 commits into
mainfrom
cian/benchmarks-binary

Conversation

@chatton

@chatton chatton commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Overview

Adding a load generating binary that uses spamoor to spam transactions both sustained over a period of time, as well as bursts.

Summary by CodeRabbit

  • New Features

    • Added ev-loadgen CLI with start, run, and burst commands for regular and burst workloads
    • Matrix-driven workload definitions and runtime overrides for flexible scenarios
    • Docker image and compose setup for containerized load generation
  • Documentation

    • Detailed load generator README with architecture, commands, matrix format, and quick start
  • Tests

    • Comprehensive unit tests for CLI flags, config validation, matrix parsing, runner, and API client
  • Chores

    • Build and CI updated to include load generator artifacts
    • Dependency bumps for compression library

@coderabbitai

coderabbitai Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds a new ev-loadgen CLI application: Spamoor client, matrix loader/validator, runner executing matrix entries with metrics polling and sync, Cobra commands (start/run/burst) with scheduler, Docker packaging and compose, just recipes/CI entry, and comprehensive unit tests and documentation.

Changes

ev-loadgen Load Generator

Layer / File(s) Summary
Build infrastructure and dependency updates
.github/workflows/ci.yml, .just/loadgen.just, .just/build.just, justfile, apps/loadgen/go.mod, apps/evm/go.mod, apps/grpc/go.mod, apps/testapp/go.mod, go.mod, .gitignore
CI workflow matrix entry for loadgen Docker build, Just recipes and import, apps/loadgen module files, shared indirect dependency bumps (github.com/klauspost/compress), and new ignore patterns.
Spamoor client and config
apps/loadgen/internal/spamoor/client.go, apps/loadgen/internal/spamoor/config.go, apps/loadgen/internal/spamoor/config_test.go
Client interface and apiClient implementation, URL resolution, GetClients HTTP call, BuildScenarioConfig that translates BENCH_* env into spamoor scenario config, and tests.
Matrix loading and validation
apps/loadgen/internal/matrix/matrix.go, apps/loadgen/internal/matrix/matrix_test.go
Matrix and Entry types with JSON unmarshalling, per-entry scenario whitelisting, env-derived NumSpammers/CountPerSpammer with bounds checking, probability/timeout parsing, Load/Validate functions, and tests.
Runner execution, polling, and sync
apps/loadgen/internal/runner/runner.go, apps/loadgen/internal/runner/runner_test.go
Matrix execution orchestration (with optional overrides), spammer lifecycle management, Prometheus metric polling with baseline subtraction, WaitForSync height-based sync, DeleteAllSpammers, and extensive unit tests using a deterministic fakeClient.
CLI entrypoint and scheduler
apps/loadgen/main.go, apps/loadgen/cmd/root.go, apps/loadgen/cmd/run.go, apps/loadgen/cmd/burst.go, apps/loadgen/cmd/start.go, apps/loadgen/cmd/start_test.go, apps/loadgen/cmd/flags_test.go
Main and Cobra root command, run/start/burst subcommands, persistent --spamoor-url flag resolution, start scheduler implementing regular intervals and randomized daily bursts with graceful shutdown and cleanup, and validation tests.
Docker packaging, matrices, and docs
apps/loadgen/Dockerfile, apps/loadgen/docker-compose.yml, apps/loadgen/matrices/baseline.json, apps/loadgen/matrices/burst.json, apps/loadgen/README.md
Multi-stage Dockerfile, docker-compose wiring for spamoor-daemon + benchmarks, baseline and burst matrix templates, and README documenting architecture, commands, flags/env, scheduling semantics, and quick-start instructions.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

T:testing

Suggested reviewers

  • alpe
  • tac0turtle

Poem

🐇 I built a loadgen to make metrics sing,
Spammers awake and the counters they bring.
With matrix and Docker, the bursts take flight,
Tests hop along through the day and the night.
Cheers from a rabbit — may the benchmarks be bright!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 24.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feat(loadgen): adding loadgen binary' clearly describes the main change - introducing a new loadgen binary application for load generation testing.
Description check ✅ Passed The description provides context about the loadgen binary's purpose (spam transactions using spamoor with sustained and burst patterns) but lacks detailed information about implementation scope, testing, and rationale.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch cian/benchmarks-binary

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@claude

claude Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Claude finished @chatton's task in 2m 58s —— View job


Code Review

  • Gather context and read repository guidelines
  • Read changed files
  • Analyze code quality, architecture, error handling, security, performance, testing, and documentation
  • Post comprehensive review

This is a solid addition — clean separation of concerns, good interface design, proper context/signal handling, and comprehensive tests. Most prior CodeRabbit findings have been addressed. Below are my remaining findings, ordered by severity.


Correctness

1. Unbounded goroutine spawning on tick (no in-flight guard)apps/loadgen/cmd/start.go:114-120

Every tick unconditionally spawns go runWorkload(...). If a workload takes longer than cfg.interval (e.g. 1h interval with a workload that runs for 2h), goroutines accumulate. The wg.Wait() in shutdown guarantees they all finish, but stale runs execute against a chain that has moved on. Consider a simple in-flight guard:

var inFlight int32
// in ticker/burst cases:
if atomic.CompareAndSwapInt32(&inFlight, 0, 1) {
    wg.Add(1)
    go func() {
        defer atomic.StoreInt32(&inFlight, 0)
        runWorkload(...)
    }()
} else {
    log.Printf("skipping tick: previous workload still in flight")
}

Fix this →


2. BENCH_REBROADCAST test documents wrong behaviorapps/loadgen/internal/spamoor/config_test.go:25,35

config_test.go sets BENCH_REBROADCAST: "true" and asserts cfg["rebroadcast"] == "true". Since rebroadcast is a uint64 field in spamoor's scenario config (not a boolean), this test is verifying that a non-numeric env value passes through as a string — which the spamoor API will likely reject or silently ignore. The test should use a numeric string and assert uint64:

"BENCH_REBROADCAST": "120",
// ...
require.Equal(t, uint64(120), cfg["rebroadcast"])

Fix this →


3. uint64 underflow risk in waitForSyncapps/loadgen/internal/runner/runner.go:299-301

if lastHeight > 0 && height > 0 {
    delta := height - lastHeight  // uint64: wraps if height < lastHeight

If height < lastHeight (e.g. a transient metric read returns stale data), delta wraps to ~2^64, which is ≥ syncThreshold, so the loop keeps waiting — this is safe but silent. A guarded subtraction would make the intent explicit and avoid confusion:

var delta uint64
if height >= lastHeight {
    delta = height - lastHeight
} // else delta = 0, keep syncing

Error Handling

4. getSpamoorCountersForPrefix missing error contextapps/loadgen/internal/runner/runner.go:268-276

func getSpamoorCountersForPrefix(api spamoor.Client, namePrefix string) (sent, failed float64, err error) {
    metrics, err := api.GetMetrics()
    if err != nil {
        return 0, 0, err  // no context
    }

The callers runEntry and waitForSpamoorDone will propagate this bare error. Should be fmt.Errorf("get metrics: %w", err) per the project's error handling guideline.

Fix this →

5. envIntOr/envDurationOr silently ignore malformed valuesapps/loadgen/cmd/start.go:168-197

If BENCH_INTERVAL=notaduration, the function falls back to the default with no warning. Logging a warning when a parse fails would help operators catch misconfiguration:

func envDurationOr(key string, fallback time.Duration) time.Duration {
    v := os.Getenv(key)
    if v == "" { return fallback }
    d, err := time.ParseDuration(v)
    if err != nil {
        log.Printf("warning: invalid %s=%q, using default %s", key, v, fallback)
        return fallback
    }
    return d
}

Resilience

6. http.Client 2s timeout may be too shortapps/loadgen/internal/spamoor/client.go:47

The custom GetClients call uses a 2s http.Client timeout. Under high load on the ev-reth node (which is precisely when this tool runs), the spamoor daemon's /api/clients response could be slow. Consider 10s or making it configurable. The other API calls delegate to the tastora spamoorapi.API which likely has no timeout — an inconsistency worth noting.


Minor

7. txPerDay integer truncation silently drops budgetapps/loadgen/cmd/start.go:68-69

runsPerDay := float64(24*time.Hour) / float64(cfg.interval)
regularTxPerRun := int(float64(cfg.txPerDay) / runsPerDay)

With txPerDay=100 and interval=1h (24 runs/day), regularTxPerRun = 4, silently dropping 4 tx/day. For large values this doesn't matter, but for small values the error is proportionally large. A math.Round would be more accurate, and logging the computed value (already done) helps visibility.

8. docker-compose.yml uses spamoor:latestapps/loadgen/docker-compose.yml:5

Non-reproducible image tag. Pin to a specific version tag or digest so load-test runs are comparable across runs.

9. Missing tests for scheduler burst scheduling and DeleteAllSpammers

The burst scheduling logic in runScheduler (nextBurstTimer interaction with resetTimer, burstsRemaining) has no unit tests. This is the most complex stateful logic in the codebase. DeleteAllSpammers also lacks coverage.


Positive Notes

  • Architecture is clean: the spamoor.Client interface is well-scoped and makes the runner fully testable. The fakeClient in runner_test.go is an excellent, deterministic fake.
  • waitForSpamoorDoneWithInterval: the sawRunning || sent >= targetCount exit condition correctly handles both the "spammer finished quickly before first poll" and "metrics lag" cases.
  • Shutdown sequence (wg.Wait()DeleteAllSpammers) is correct — in-flight entries do their per-spammer cleanup via defer, then the global cleanup catches any stragglers.
  • Baseline subtraction for metrics is the right approach for isolating per-run counts from cumulative counters.
  • README is thorough and production-ready.

@github-actions

github-actions Bot commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

The latest Buf updates on your PR. Results from workflow CI / buf-check (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed⏩ skipped✅ passed✅ passedJun 11, 2026, 11:47 AM

@codecov

codecov Bot commented Jun 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 60.71%. Comparing base (29e8542) to head (51ef8b3).

Additional details and impacted files
@@           Coverage Diff           @@
##             main    #3335   +/-   ##
=======================================
  Coverage   60.70%   60.71%           
=======================================
  Files         127      127           
  Lines       13781    13781           
=======================================
+ Hits         8366     8367    +1     
  Misses       4502     4502           
+ Partials      913      912    -1     
Flag Coverage Δ
combined 60.71% <ø> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@chatton chatton marked this pull request as ready for review June 2, 2026 11:51

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (10)
apps/loadgen/cmd/flags_test.go (1)

10-12: 💤 Low value

Redundant (duplicate) start registration.

NewRootCmd() already calls rootCmd.AddCommand(newRunCmd(), newStartCmd(), newCheckCmd()), so root already owns a start subcommand. Adding another startCmd here registers a second, duplicate start command on root. The test still passes because the assertions run against the local startCmd, but root is never used in this test — both root and the extra AddCommand can be dropped.

♻️ Suggested simplification
 func TestStartFlags(t *testing.T) {
-	root := NewRootCmd()
 	startCmd := newStartCmd()
-	root.AddCommand(startCmd)
 
 	err := startCmd.ParseFlags([]string{"--regular-matrix", "custom.json"})
 	require.NoError(t, err)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/flags_test.go` around lines 10 - 12, Remove the redundant
registration of the "start" subcommand: NewRootCmd() already adds newStartCmd(),
so drop the creation of root and the extra root.AddCommand(startCmd) call from
the test; keep the local startCmd used for assertions (created via
newStartCmd()) and remove references to NewRootCmd(), root, and root.AddCommand
to avoid registering a duplicate start command.
apps/loadgen/main.go (1)

17-21: 💤 Low value

Banner side-effect in init() fires on every invocation.

init() runs before main() for all entrypoints, so "ev-loadgen starting" is emitted even for --help, shell completion, and other non-running subcommands, polluting their output. Consider keeping only logger configuration in init() and moving the startup message into main() (or a PersistentPreRun) so it prints only when a workload actually starts.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/main.go` around lines 17 - 21, The init() currently configures
logging and also prints a startup banner ("ev-loadgen starting"), which runs for
every entrypoint; remove the fmt.Fprintln(os.Stderr, "ev-loadgen starting") from
init() so init() only does log.SetFlags and log.SetOutput, and relocate the
banner to main() (or a Cobra PersistentPreRun if using Cobra) so the message is
emitted only when the workload actually starts; update references in main() or
the command's PersistentPreRun to print the same startup message to os.Stderr.
apps/loadgen/internal/client.go (2)

67-81: ⚡ Quick win

Wrap returned errors with context.

GetClients returns bare errors from the HTTP GET and the JSON decode. Wrapping them makes failures actionable in logs/callers.

♻️ Proposed wrapping
 	url := fmt.Sprintf("%s/api/clients", c.api.BaseURL)
 	resp, err := c.client.Get(url)
 	if err != nil {
-		return nil, err
+		return nil, fmt.Errorf("get clients: %w", err)
 	}
 	defer func() { _ = resp.Body.Close() }()
 	if resp.StatusCode < 200 || resp.StatusCode >= 300 {
 		body, _ := io.ReadAll(resp.Body)
 		return nil, fmt.Errorf("get clients failed: %s", string(body))
 	}
 	var raw []clientResponse
 	if err := json.NewDecoder(resp.Body).Decode(&raw); err != nil {
-		return nil, err
+		return nil, fmt.Errorf("decode clients response: %w", err)
 	}

As per coding guidelines: "Wrap errors with context using fmt.Errorf in Go code".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/internal/client.go` around lines 67 - 81, GetClients currently
returns raw errors from the HTTP GET and JSON decode; update
spamoorAPIClient.GetClients to wrap those errors with contextual messages (e.g.,
using fmt.Errorf) so callers see what operation failed. Specifically, wrap the
error returned by c.client.Get(url) with context like "GET /api/clients failed"
and wrap the error from json.NewDecoder(resp.Body).Decode(&raw) with context
like "decode GetClients response failed"; keep existing behavior for non-2xx
handling and closing resp.Body, but replace bare err returns with fmt.Errorf
including the original err.

16-24: ⚖️ Poor tradeoff

Consider propagating context.Context through the client.

The SpamoorClient interface and GetClients rely solely on the 2s http.Client timeout and ignore the caller's context (e.g., cmd.Context() from RunCheck). Threading a context.Context into these methods would honor cancellation and deadlines from upstream callers. This touches the interface surface, so it can be deferred, but it aligns with the guideline.

As per coding guidelines: "Use context.Context for cancellation in Go code".

Also applies to: 67-69

apps/loadgen/docker-compose.yml (1)

5-5: ⚡ Quick win

Pin spamoor-daemon to an immutable version/digest instead of latest.

Using :latest makes load tests non-reproducible and can silently change behavior between runs.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/docker-compose.yml` at line 5, Replace the floating image
reference "image: ethpandaops/spamoor:latest" with an immutable image reference
(either a specific version tag like "ethpandaops/spamoor:vX.Y.Z" or a
content-addressable digest "ethpandaops/spamoor@sha256:...") so the loadgen
service uses a reproducible artifact; update the compose service's image field
where "image: ethpandaops/spamoor:latest" appears and ensure any CI/docs that
trigger builds are updated to pin or update the tag/digest when intentional.
apps/loadgen/internal/matrix.go (2)

104-114: 💤 Low value

envInt silently masks malformed values.

A non-integer value for BENCH_NUM_SPAMMERS/BENCH_COUNT_PER_SPAMMER (e.g. a typo like "2x") silently falls back to the default instead of being rejected, so a misconfigured matrix can run with unintended values. Since these are required, validated inputs, surfacing the parse error would be safer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/internal/matrix.go` around lines 104 - 114, The envInt helper
currently swallows parse errors; change envInt(key) to return (int, error)
instead of silently falling back so callers can fail fast for required inputs
like BENCH_NUM_SPAMMERS and BENCH_COUNT_PER_SPAMMER; specifically, update envInt
to: 1) return fallback value only when the key is absent (or keep current
semantics if fallback is intended) but return a non-nil error when strconv.Atoi
fails, 2) update all callers of envInt (e.g., where BENCH_NUM_SPAMMERS and
BENCH_COUNT_PER_SPAMMER are read) to handle the error and surface it (return
error up the stack or log and exit), and 3) ensure error messages include the
env key and the invalid value to aid debugging.

79-102: ⚡ Quick win

Consider validating Timeout during matrix validation.

Timeout is accepted as a free-form string but never parsed here, so an invalid duration (e.g. "2x") passes validation and only surfaces as an error later when the runner parses it. Validating it up front (when non-empty) gives faster, clearer feedback at load time.

♻️ Proposed early validation
 	if e.Probability != nil && (*e.Probability < 0 || *e.Probability > 1) {
 		return fmt.Errorf("probability must be between 0.0 and 1.0, got %f", *e.Probability)
 	}
 
+	if e.Timeout != "" {
+		if _, err := time.ParseDuration(e.Timeout); err != nil {
+			return fmt.Errorf("invalid timeout %q: %w", e.Timeout, err)
+		}
+	}
+
 	return nil

Add "time" to the imports.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/internal/matrix.go` around lines 79 - 102, The validate method
on Entry should also parse and validate the Timeout string early: when e.Timeout
is non-empty call time.ParseDuration(e.Timeout) (add the "time" import) and
return a clear fmt.Errorf if parsing fails (e.g., "invalid timeout %q: %v"), so
invalid durations like "2x" fail during Entry.validate rather than later in the
runner.
apps/loadgen/cmd/start.go (2)

56-129: ⚖️ Poor tradeoff

Use structured logging instead of log.Printf.

This scheduler (and the runner package) logs exclusively via the standard log package with no levels. Consider a leveled/structured logger so operational output (info vs warning/error such as the workload-error and cleanup-failure cases) is distinguishable.

As per coding guidelines: "Use structured logging with appropriate log levels".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/start.go` around lines 56 - 129, The function runScheduler
uses plain log.Printf; replace it with a leveled/structured logger and use
appropriate levels for events (info for normal lifecycle messages, error for
failures, warn for cleanup warnings). Instantiate or accept a structured logger
(e.g., logger variable or parameter) in runScheduler, then change all log.Printf
calls inside runScheduler and the nested runWorkload closure to
logger.Infof/logger.Warnf/logger.Errorf (or equivalent) and include structured
fields like "label", "txCount", "interval", "regularMatrix", "burstMatrix", and
"spamoor" to make messages machine-readable; also propagate or pass the logger
into any calls that need it (or include it in context) for internal.WaitForSync,
internal.ExecuteMatrixWithOverridesFromFile, and internal.DeleteAllSpammers so
their failures are logged with error level and contextual fields.

79-119: ⚖️ Poor tradeoff

Bursts/regular workloads launched on every tick can queue up unboundedly.

Each ticker.C/burstTimer.C fire spawns a new go runWorkload(...). These serialize on mu, but if a workload runs longer than interval the goroutines pile up and later run stale workloads against a chain that has already moved on. Consider skipping (or coalescing) a tick when a workload is already in flight, e.g. a TryLock/busy flag rather than launching unconditionally.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/start.go` around lines 79 - 119, The tick handlers currently
spawn goroutines unconditionally which can pile up because runWorkload
serializes on mu; add a short-circuit "in-flight" guard (e.g., an int32 atomic
flag or a token channel) to skip launching when a workload is already running:
declare inFlight int32 (or a chan struct{} semaphore) near the function, change
the ticker and burstTimer cases to do an atomic.CompareAndSwapInt32(&inFlight,
0, 1) (or try to receive/send on the token channel) and only call go
runWorkload(...) when the CAS succeeds, and ensure runWorkload clears the flag
on exit (defer atomic.StoreInt32(&inFlight, 0) or release the token) so
subsequent ticks can run; keep existing mu locking inside runWorkload and
preserve cleanup on ctx.Done(). Use the same mechanism for both "regular" and
"burst" launches to coalesce/skips overlapping runs.
apps/loadgen/internal/runner.go (1)

337-353: 💤 Low value

waitForSync reuses a stale lastHeight after a fetch error.

If getSpamoorHeight errors on an iteration, lastHeight is not updated, so the next successful read computes delta = height - lastHeight across a longer wall-clock gap (two+ poll intervals). That inflates the delta and can prevent the delta < syncThreshold exit even when synced. Minor, but resetting lastHeight = 0 on error (forcing a fresh baseline) would make the heuristic more robust.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/internal/runner.go` around lines 337 - 353, In waitForSync's
polling loop, when getSpamoorHeight returns an error the code currently leaves
lastHeight unchanged which can inflate the next delta; update the error branch
in waitForSync to reset lastHeight to 0 (or another sentinel) after logging the
error so the next successful getSpamoorHeight call establishes a fresh baseline
before computing delta against syncThreshold using pollInterval; modify the
block that handles err from getSpamoorHeight to set lastHeight = 0.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@apps/loadgen/Dockerfile`:
- Around line 11-24: The image runs as root; change the Dockerfile to create and
switch to a non-root user before executing CMD: add an unprivileged group/user
(e.g., create group "ev" and user "ev"), change WORKDIR to a non-root directory
(e.g., /home/ev), chown the copied files (ev-loadgen and the JSON matrices) to
that user (references: WORKDIR, COPY lines that place /usr/bin/ev-loadgen and
/root/*.json), and add a USER instruction so the container runs as that
unprivileged user when invoking CMD ["ev-loadgen","start"].

In `@apps/loadgen/go.mod`:
- Line 42: apps/loadgen/go.mod pulls vulnerable transitive modules
(github.com/cometbft/cometbft and github.com/cosmos/cosmos-sdk) via
github.com/celestiaorg/tastora; update the transitive chain by bumping tastora
to v0.20.0 (which brings cometbft to v1.0.1 and cosmos-sdk to v0.54.3), either
by running `go get github.com/celestiaorg/tastora@v0.20.0` or editing
apps/loadgen/go.mod to require the new versions, then run `go mod tidy` and `go
list -m all` to verify the upgraded versions and re-run the vulnerability scan
(e.g., `govulncheck ./...`) to confirm the fixes.

In `@apps/loadgen/internal/config.go`:
- Around line 39-49: The loop that maps environment variables into cfg currently
uses strconv.Atoi and falls back to the original string, which turns
BENCH_REBROADCAST="true" into the JSON string "true"; change the numeric parsing
to use strconv.ParseUint(val, 10, 64) (for example in the same loop that
iterates envMapping) and, on success, assign the parsed uint64 value into
cfg[cfgKey]; on parse failure keep the original string as before. Also update
the corresponding test in config_test.go to expect a numeric value (uint64/JSON
number) for the BENCH_REBROADCAST entry instead of the string "true". Ensure
references include the envMapping loop and the BENCH_REBROADCAST key so the
change is applied to the correct mapping.

In `@apps/loadgen/README.md`:
- Around line 7-12: The README contains fenced code blocks with ASCII diagrams
(e.g., the block starting with "ev-loadgen (this binary)  -->  spamoor-daemon 
-->  ev-reth RPC") that are missing language identifiers and trigger MD040;
update each such triple-backtick fence (including the block at lines referenced
and the other block around lines 19-23) to include an appropriate language tag
like `text` or `bash` (e.g., ```text) so markdown lint passes and the blocks
render correctly.

---

Nitpick comments:
In `@apps/loadgen/cmd/flags_test.go`:
- Around line 10-12: Remove the redundant registration of the "start"
subcommand: NewRootCmd() already adds newStartCmd(), so drop the creation of
root and the extra root.AddCommand(startCmd) call from the test; keep the local
startCmd used for assertions (created via newStartCmd()) and remove references
to NewRootCmd(), root, and root.AddCommand to avoid registering a duplicate
start command.

In `@apps/loadgen/cmd/start.go`:
- Around line 56-129: The function runScheduler uses plain log.Printf; replace
it with a leveled/structured logger and use appropriate levels for events (info
for normal lifecycle messages, error for failures, warn for cleanup warnings).
Instantiate or accept a structured logger (e.g., logger variable or parameter)
in runScheduler, then change all log.Printf calls inside runScheduler and the
nested runWorkload closure to logger.Infof/logger.Warnf/logger.Errorf (or
equivalent) and include structured fields like "label", "txCount", "interval",
"regularMatrix", "burstMatrix", and "spamoor" to make messages machine-readable;
also propagate or pass the logger into any calls that need it (or include it in
context) for internal.WaitForSync, internal.ExecuteMatrixWithOverridesFromFile,
and internal.DeleteAllSpammers so their failures are logged with error level and
contextual fields.
- Around line 79-119: The tick handlers currently spawn goroutines
unconditionally which can pile up because runWorkload serializes on mu; add a
short-circuit "in-flight" guard (e.g., an int32 atomic flag or a token channel)
to skip launching when a workload is already running: declare inFlight int32 (or
a chan struct{} semaphore) near the function, change the ticker and burstTimer
cases to do an atomic.CompareAndSwapInt32(&inFlight, 0, 1) (or try to
receive/send on the token channel) and only call go runWorkload(...) when the
CAS succeeds, and ensure runWorkload clears the flag on exit (defer
atomic.StoreInt32(&inFlight, 0) or release the token) so subsequent ticks can
run; keep existing mu locking inside runWorkload and preserve cleanup on
ctx.Done(). Use the same mechanism for both "regular" and "burst" launches to
coalesce/skips overlapping runs.

In `@apps/loadgen/docker-compose.yml`:
- Line 5: Replace the floating image reference "image:
ethpandaops/spamoor:latest" with an immutable image reference (either a specific
version tag like "ethpandaops/spamoor:vX.Y.Z" or a content-addressable digest
"ethpandaops/spamoor@sha256:...") so the loadgen service uses a reproducible
artifact; update the compose service's image field where "image:
ethpandaops/spamoor:latest" appears and ensure any CI/docs that trigger builds
are updated to pin or update the tag/digest when intentional.

In `@apps/loadgen/internal/client.go`:
- Around line 67-81: GetClients currently returns raw errors from the HTTP GET
and JSON decode; update spamoorAPIClient.GetClients to wrap those errors with
contextual messages (e.g., using fmt.Errorf) so callers see what operation
failed. Specifically, wrap the error returned by c.client.Get(url) with context
like "GET /api/clients failed" and wrap the error from
json.NewDecoder(resp.Body).Decode(&raw) with context like "decode GetClients
response failed"; keep existing behavior for non-2xx handling and closing
resp.Body, but replace bare err returns with fmt.Errorf including the original
err.

In `@apps/loadgen/internal/matrix.go`:
- Around line 104-114: The envInt helper currently swallows parse errors; change
envInt(key) to return (int, error) instead of silently falling back so callers
can fail fast for required inputs like BENCH_NUM_SPAMMERS and
BENCH_COUNT_PER_SPAMMER; specifically, update envInt to: 1) return fallback
value only when the key is absent (or keep current semantics if fallback is
intended) but return a non-nil error when strconv.Atoi fails, 2) update all
callers of envInt (e.g., where BENCH_NUM_SPAMMERS and BENCH_COUNT_PER_SPAMMER
are read) to handle the error and surface it (return error up the stack or log
and exit), and 3) ensure error messages include the env key and the invalid
value to aid debugging.
- Around line 79-102: The validate method on Entry should also parse and
validate the Timeout string early: when e.Timeout is non-empty call
time.ParseDuration(e.Timeout) (add the "time" import) and return a clear
fmt.Errorf if parsing fails (e.g., "invalid timeout %q: %v"), so invalid
durations like "2x" fail during Entry.validate rather than later in the runner.

In `@apps/loadgen/internal/runner.go`:
- Around line 337-353: In waitForSync's polling loop, when getSpamoorHeight
returns an error the code currently leaves lastHeight unchanged which can
inflate the next delta; update the error branch in waitForSync to reset
lastHeight to 0 (or another sentinel) after logging the error so the next
successful getSpamoorHeight call establishes a fresh baseline before computing
delta against syncThreshold using pollInterval; modify the block that handles
err from getSpamoorHeight to set lastHeight = 0.

In `@apps/loadgen/main.go`:
- Around line 17-21: The init() currently configures logging and also prints a
startup banner ("ev-loadgen starting"), which runs for every entrypoint; remove
the fmt.Fprintln(os.Stderr, "ev-loadgen starting") from init() so init() only
does log.SetFlags and log.SetOutput, and relocate the banner to main() (or a
Cobra PersistentPreRun if using Cobra) so the message is emitted only when the
workload actually starts; update references in main() or the command's
PersistentPreRun to print the same startup message to os.Stderr.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 864cc402-06e5-49ca-83c4-83444b6132aa

📥 Commits

Reviewing files that changed from the base of the PR and between 6b2629e and 3e6d05e.

⛔ Files ignored due to path filters (5)
  • apps/evm/go.sum is excluded by !**/*.sum
  • apps/grpc/go.sum is excluded by !**/*.sum
  • apps/loadgen/go.sum is excluded by !**/*.sum
  • apps/testapp/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (29)
  • .github/workflows/ci.yml
  • .gitignore
  • .just/build.just
  • .just/loadgen.just
  • apps/evm/go.mod
  • apps/grpc/go.mod
  • apps/loadgen/Dockerfile
  • apps/loadgen/README.md
  • apps/loadgen/cmd/check.go
  • apps/loadgen/cmd/flags_test.go
  • apps/loadgen/cmd/root.go
  • apps/loadgen/cmd/run.go
  • apps/loadgen/cmd/start.go
  • apps/loadgen/cmd/start_test.go
  • apps/loadgen/docker-compose.yml
  • apps/loadgen/go.mod
  • apps/loadgen/internal/client.go
  • apps/loadgen/internal/config.go
  • apps/loadgen/internal/config_test.go
  • apps/loadgen/internal/matrix.go
  • apps/loadgen/internal/matrix_test.go
  • apps/loadgen/internal/runner.go
  • apps/loadgen/internal/runner_test.go
  • apps/loadgen/main.go
  • apps/loadgen/matrices/baseline.json
  • apps/loadgen/matrices/burst.json
  • apps/testapp/go.mod
  • go.mod
  • justfile

Comment thread apps/loadgen/Dockerfile
Comment thread apps/loadgen/go.mod
Comment thread apps/loadgen/internal/spamoor/config.go
Comment thread apps/loadgen/README.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
apps/loadgen/cmd/start.go (3)

110-120: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reschedule bursts against the remaining day, not a fresh 24h window.

After a burst fires, nextBurstTimer(burstsRemaining, burstWindow) ignores how much time is left before resetTimer fires. If an earlier burst happened late, the next timer is frequently pushed past the pending reset and gets canceled, so burstPerDay is not actually honored. Use time.Until(nextReset) for the follow-up delay window instead of another full burstWindow.

Also applies to: 141-156

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/start.go` around lines 110 - 120, The burst scheduling
currently calls nextBurstTimer(burstsRemaining, burstWindow) after each burst
which resets the follow-up delay to a full burstWindow and ignores the remaining
time until resetTimer fires; update the logic in the run loop where burstTimer
and burstsRemaining are updated (references: burstTimer, resetTimer,
burstsRemaining, nextBurstTimer, burstWindow, resetTimer.Reset) to compute the
remaining window until the next reset (e.g., time.Until(nextReset)) and pass
that reduced window into nextBurstTimer so subsequent bursts are scheduled only
within the remaining day; apply the same change to the other similar block
handling bursts (the later occurrence around where burstsRemaining is
decremented and burstTimer reset) so both places use the time until the pending
reset rather than a fresh burstWindow.

66-67: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Zero txCount here means "use matrix defaults", not "run nothing".

validateStartConfig allows zero values, and regularTxPerRun truncates fractional daily budgets down to zero. Downstream, apps/loadgen/internal/runner.go only applies the BENCH_COUNT_PER_SPAMMER override when totalTxTarget > 0, so --tx-per-day=0, --burst-tx-count=0, or --tx-per-day=1 --interval=1h will still execute the matrix at its baked-in counts. Skip launches when txCount == 0, and either reject or carry fractional budget forward instead of truncating it away. As per coding guidelines, "Validate all inputs from external sources in Go code".

Also applies to: 77-80, 84-85, 110-111, 125-137

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/start.go` around lines 66 - 67, The calculation in start.go
(runsPerDay and regularTxPerRun) currently truncates fractional budgets to zero
and treats zero as "run nothing"; instead preserve fractional budget or reject
ambiguous zeroes and treat zero as the sentinel meaning "use matrix defaults".
Update the logic around regularTxPerRun (the variables runsPerDay,
regularTxPerRun) and validateStartConfig so that: 1) inputs are validated (no
negatives; reject or require explicit flags if the caller intended zero vs
defaults), 2) per-run budget does not silently truncate fractional daily budgets
to zero — either carry the fractional remainder forward using a float
accumulator on the start config (e.g., txBudgetFloat / runsPerDay) and
distribute it across runs, or use rounding (math.Ceil) when appropriate so small
budgets aren’t lost, and 3) explicitly treat txCount==0 as the sentinel to "use
matrix defaults" (so do not skip applying matrix defaults) and ensure
runner.go’s BENCH_COUNT_PER_SPAMMER behavior is preserved by checking this
sentinel instead of relying on totalTxTarget > 0.

77-85: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Wait for workload goroutines to finish before calling internal.DeleteAllSpammers during shutdown.

In apps/loadgen/cmd/start.go, runWorkload is launched as detached goroutines (immediately and on ticker/burst), but when ctx.Done() fires the scheduler immediately calls internal.DeleteAllSpammers(api) and returns without waiting. Because SpamoorClient methods don’t take context.Context, in-flight matrix execution (and per-entry deferred api.DeleteSpammer) can still be running while global cleanup deletes the same spammers, leading to shutdown races, cancellation/timed-out errors, and spurious cleanup warnings. Track launched goroutines with a sync.WaitGroup or errgroup and join them before DeleteAllSpammers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/start.go` around lines 77 - 85, The shutdown races occur
because runWorkload goroutines (launched from start.go, including the immediate
goroutine and those started by the ticker/burst logic) are not awaited before
calling internal.DeleteAllSpammers(api); update the scheduler to track all
launched workers with a sync.WaitGroup or an errgroup tied to ctx, incrementing
the counter inside the code that invokes runWorkload and every goroutine started
from the ticker/burst paths, then on ctx.Done() wait for the group to finish
(wg.Wait() or group.Wait()) before calling internal.DeleteAllSpammers(api) so
in-flight ExecuteMatrixWithOverridesFromFile and per-entry deferred
api.DeleteSpammer calls complete first.
♻️ Duplicate comments (1)
apps/loadgen/internal/config.go (1)

39-48: ⚠️ Potential issue | 🟠 Major

Reject invalid numeric BENCH_* overrides instead of serializing strings.

The else branch still forwards bad external input as the wrong JSON type. With the current mapping, values like BENCH_REBROADCAST=true or BENCH_THROUGHPUT=-1 become strings here and the failure is deferred to the spamoor API call instead of being caught at config build time. Please fail fast on parse errors rather than populating cfg[cfgKey] = val. As per coding guidelines, "Validate all inputs from external sources in Go code".

What JSON types does spamoor expect for the scenario config fields total_count, throughput, max_pending, max_wallets, gas_units_to_burn, base_fee, tip_fee, and rebroadcast?
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/internal/config.go` around lines 39 - 48, The code currently
converts env values to uint64 with strconv.ParseUint and on parse failure writes
the raw string into cfg (envMapping/cfg), which allows invalid types to pass
through; instead, fail fast: when strconv.ParseUint returns an error for an
envKey mapped to a numeric cfgKey, return (or propagate) an error describing the
invalid override (include envKey and val) rather than assigning cfg[cfgKey]=val;
for boolean-like keys such as rebroadcast, use strconv.ParseBool and likewise
return an error on failure; update the block referencing envMapping, envKey,
cfgKey, strconv.ParseUint and cfg to perform strict parsing and error
propagation so only correctly typed numeric/boolean values populate cfg.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@apps/loadgen/cmd/start.go`:
- Around line 110-120: The burst scheduling currently calls
nextBurstTimer(burstsRemaining, burstWindow) after each burst which resets the
follow-up delay to a full burstWindow and ignores the remaining time until
resetTimer fires; update the logic in the run loop where burstTimer and
burstsRemaining are updated (references: burstTimer, resetTimer,
burstsRemaining, nextBurstTimer, burstWindow, resetTimer.Reset) to compute the
remaining window until the next reset (e.g., time.Until(nextReset)) and pass
that reduced window into nextBurstTimer so subsequent bursts are scheduled only
within the remaining day; apply the same change to the other similar block
handling bursts (the later occurrence around where burstsRemaining is
decremented and burstTimer reset) so both places use the time until the pending
reset rather than a fresh burstWindow.
- Around line 66-67: The calculation in start.go (runsPerDay and
regularTxPerRun) currently truncates fractional budgets to zero and treats zero
as "run nothing"; instead preserve fractional budget or reject ambiguous zeroes
and treat zero as the sentinel meaning "use matrix defaults". Update the logic
around regularTxPerRun (the variables runsPerDay, regularTxPerRun) and
validateStartConfig so that: 1) inputs are validated (no negatives; reject or
require explicit flags if the caller intended zero vs defaults), 2) per-run
budget does not silently truncate fractional daily budgets to zero — either
carry the fractional remainder forward using a float accumulator on the start
config (e.g., txBudgetFloat / runsPerDay) and distribute it across runs, or use
rounding (math.Ceil) when appropriate so small budgets aren’t lost, and 3)
explicitly treat txCount==0 as the sentinel to "use matrix defaults" (so do not
skip applying matrix defaults) and ensure runner.go’s BENCH_COUNT_PER_SPAMMER
behavior is preserved by checking this sentinel instead of relying on
totalTxTarget > 0.
- Around line 77-85: The shutdown races occur because runWorkload goroutines
(launched from start.go, including the immediate goroutine and those started by
the ticker/burst logic) are not awaited before calling
internal.DeleteAllSpammers(api); update the scheduler to track all launched
workers with a sync.WaitGroup or an errgroup tied to ctx, incrementing the
counter inside the code that invokes runWorkload and every goroutine started
from the ticker/burst paths, then on ctx.Done() wait for the group to finish
(wg.Wait() or group.Wait()) before calling internal.DeleteAllSpammers(api) so
in-flight ExecuteMatrixWithOverridesFromFile and per-entry deferred
api.DeleteSpammer calls complete first.

---

Duplicate comments:
In `@apps/loadgen/internal/config.go`:
- Around line 39-48: The code currently converts env values to uint64 with
strconv.ParseUint and on parse failure writes the raw string into cfg
(envMapping/cfg), which allows invalid types to pass through; instead, fail
fast: when strconv.ParseUint returns an error for an envKey mapped to a numeric
cfgKey, return (or propagate) an error describing the invalid override (include
envKey and val) rather than assigning cfg[cfgKey]=val; for boolean-like keys
such as rebroadcast, use strconv.ParseBool and likewise return an error on
failure; update the block referencing envMapping, envKey, cfgKey,
strconv.ParseUint and cfg to perform strict parsing and error propagation so
only correctly typed numeric/boolean values populate cfg.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb41e19f-036f-4e69-a32b-b815da44cd89

📥 Commits

Reviewing files that changed from the base of the PR and between 3e6d05e and 9f5d982.

📒 Files selected for processing (10)
  • apps/loadgen/Dockerfile
  • apps/loadgen/README.md
  • apps/loadgen/cmd/flags_test.go
  • apps/loadgen/cmd/start.go
  • apps/loadgen/internal/client.go
  • apps/loadgen/internal/config.go
  • apps/loadgen/internal/config_test.go
  • apps/loadgen/internal/matrix.go
  • apps/loadgen/internal/runner.go
  • apps/loadgen/main.go
💤 Files with no reviewable changes (2)
  • apps/loadgen/cmd/flags_test.go
  • apps/loadgen/main.go
✅ Files skipped from review due to trivial changes (1)
  • apps/loadgen/README.md
🚧 Files skipped from review as they are similar to previous changes (5)
  • apps/loadgen/Dockerfile
  • apps/loadgen/internal/matrix.go
  • apps/loadgen/internal/config_test.go
  • apps/loadgen/internal/client.go
  • apps/loadgen/internal/runner.go

@chatton chatton marked this pull request as draft June 4, 2026 07:59
auricom and others added 18 commits June 10, 2026 10:47
…spamoor

Standalone Go binary that orchestrates spamoor-daemon via HTTP API to
drive sustained and burst transaction load against ev-reth. Structured
as a cobra CLI with subcommands: check (connectivity), regular (~1M
tx/day), burst (500K probabilistic), and run (custom matrix).

Includes Dockerfile, docker-compose, baseline/burst matrices, crontab
for supercronic scheduling, just targets for build and smoke testing.
Replace separate regular/burst subcommands invoked by supercronic with
a single `start` command that runs an infinite scheduling loop. Regular
workloads fire on a ticker, bursts at random times within rolling 24h
windows. All timing controlled via CLI flags with env fallbacks.

- remove supercronic from Dockerfile
- delete regular.go, burst.go, crontab
- add start.go with scheduler loop
- plumb context for graceful shutdown
- unify ExecuteMatrix/ExecuteMatrixWithOverrides via matrixOpts
- bump fees (BASE_FEE 20→500, TIP_FEE 2→50)
- remove healthcheck from docker-compose, rely on WaitForSync
- run container as non-root user
- use ParseUint for numeric env values in scenario config
- add language identifiers to fenced code blocks (MD040)
- wrap bare errors with context in client.go
- remove redundant root cmd in flags_test.go
- validate timeout duration during matrix validation
- remove startup banner from init()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- use remaining time until next reset for burst timer instead of full 24h window
- track workload goroutines with WaitGroup, wait before cleanup on shutdown

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Default burst-per-day changed from 2 to 0 so the start command
no longer fires bursts unless explicitly configured. New burst
subcommand triggers a single burst workload immediately, reusing
the same matrix execution path as the scheduler.
Counter-based progress tracking summed all spammer metrics globally,
causing overshoot when concurrent workloads (e.g. baseline + burst)
ran on the same spamoor instance. Filter by spammer_name label prefix
so each workload only tracks its own spammers.
- Split flat internal/ into internal/{matrix,spamoor,runner} packages
- Remove check subcommand (unused)
- Remove intermediate exported functions (ExecuteMatrix, ExecuteMatrixWithOverrides)
- Remove double validation in executeMatrix (already done by matrix.Load)
- Extract metric name and env var constants
- Store parsed timeout on Entry to avoid double time.ParseDuration
- Replace time.NewTimer per-iteration with single ticker in waitForSync
- Fix double time.Since call in poll loop
- Update README: fix burst-per-day default (0 not 2), remove stale
  serialization claim, document all subcommands clearly
@chatton chatton force-pushed the cian/benchmarks-binary branch from fe13ced to fa30616 Compare June 10, 2026 10:10
@chatton chatton marked this pull request as ready for review June 10, 2026 10:56

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/loadgen/cmd/burst.go (2)

24-29: ⚡ Quick win

Consider wrapping errors with command context.

The errors returned from WaitForSync and ExecuteMatrixWithOverridesFromFile lack command-specific context. Wrapping them would help users and operators identify which command failed during debugging.

♻️ Suggested error wrapping
 			if err := runner.WaitForSync(cmd.Context(), api); err != nil {
-				return err
+				return fmt.Errorf("burst: waiting for sync: %w", err)
 			}
 
 			log.Printf("==> burst workload starting (%d tx)", txCount)
-			return runner.ExecuteMatrixWithOverridesFromFile(cmd.Context(), matrixPath, api, txCount)
+			if err := runner.ExecuteMatrixWithOverridesFromFile(cmd.Context(), matrixPath, api, txCount); err != nil {
+				return fmt.Errorf("burst: executing matrix: %w", err)
+			}
+			return nil

Add the import:

import (
	"fmt"
	"log"
	// ... existing imports
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/burst.go` around lines 24 - 29, Wrap errors returned by
runner.WaitForSync and runner.ExecuteMatrixWithOverridesFromFile with
command-specific context before returning: import fmt, and change the return
paths so that if WaitForSync(...) or ExecuteMatrixWithOverridesFromFile(...)
returns an error you return fmt.Errorf("burst: WaitForSync failed: %w", err) and
fmt.Errorf("burst: ExecuteMatrixWithOverridesFromFile failed (matrix=%s tx=%d):
%w", matrixPath, txCount, err) respectively; keep existing log.Printf("==> burst
workload starting (%d tx)", txCount) as-is but ensure the wrapped errors use
cmd.Context(), runner and the matrixPath/txCount values for clear diagnostics.

Source: Coding guidelines


33-33: ⚡ Quick win

Validate --tx-count is positive.

The flag accepts any integer, including zero or negative values. While the runner treats txCount <= 0 as "use matrix defaults," users passing --tx-count=0 or --tx-count=-500 likely made a mistake and would benefit from clearer feedback.

🛡️ Add validation in RunE
 		RunE: func(cmd *cobra.Command, args []string) error {
+			if txCount <= 0 {
+				return fmt.Errorf("--tx-count must be positive, got %d", txCount)
+			}
 			api := spamoor.NewClient(resolveSpamoorURL())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/burst.go` at line 33, The flag txCount (declared via
cmd.Flags().IntVar(&txCount, "tx-count", ...)) currently allows zero or negative
values; add validation at the start of the burst command's RunE (or the function
that executes the command) to check if txCount <= 0 and return a clear error
(e.g., fmt.Errorf("invalid --tx-count: must be a positive integer")) so users
get immediate feedback; reference the txCount variable and the command's RunE
handler when applying the change and adjust any tests or callers that expect the
previous behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Line 21: Replace the tag-only usage of actions/checkout@v6.0.3 in the CI
workflow with the pinned commit SHA used elsewhere
(actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10) to prevent
tag-movement attacks; update the reference in .github/workflows/ci.yml so the
checkout step uses the full SHA instead of the v6.0.3 tag (leave other matrix
entries like apps/loadgen/Dockerfile unchanged).

In `@apps/loadgen/internal/spamoor/config_test.go`:
- Around line 21-36: The test passes a non-numeric value for BENCH_REBROADCAST
causing BuildScenarioConfig to leave cfg["rebroadcast"] as the original string;
change the test to supply a numeric string (e.g., "120") for BENCH_REBROADCAST
and update the assertion to expect uint64(120) for cfg["rebroadcast"] so it
matches the strconv.ParseUint path in BuildScenarioConfig.

---

Nitpick comments:
In `@apps/loadgen/cmd/burst.go`:
- Around line 24-29: Wrap errors returned by runner.WaitForSync and
runner.ExecuteMatrixWithOverridesFromFile with command-specific context before
returning: import fmt, and change the return paths so that if WaitForSync(...)
or ExecuteMatrixWithOverridesFromFile(...) returns an error you return
fmt.Errorf("burst: WaitForSync failed: %w", err) and fmt.Errorf("burst:
ExecuteMatrixWithOverridesFromFile failed (matrix=%s tx=%d): %w", matrixPath,
txCount, err) respectively; keep existing log.Printf("==> burst workload
starting (%d tx)", txCount) as-is but ensure the wrapped errors use
cmd.Context(), runner and the matrixPath/txCount values for clear diagnostics.
- Line 33: The flag txCount (declared via cmd.Flags().IntVar(&txCount,
"tx-count", ...)) currently allows zero or negative values; add validation at
the start of the burst command's RunE (or the function that executes the
command) to check if txCount <= 0 and return a clear error (e.g.,
fmt.Errorf("invalid --tx-count: must be a positive integer")) so users get
immediate feedback; reference the txCount variable and the command's RunE
handler when applying the change and adjust any tests or callers that expect the
previous behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb2a554e-5bb0-4d69-8091-1c8d7d7a3aa0

📥 Commits

Reviewing files that changed from the base of the PR and between 32c010f and fa30616.

⛔ Files ignored due to path filters (5)
  • apps/evm/go.sum is excluded by !**/*.sum
  • apps/grpc/go.sum is excluded by !**/*.sum
  • apps/loadgen/go.sum is excluded by !**/*.sum
  • apps/testapp/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (29)
  • .github/workflows/ci.yml
  • .gitignore
  • .just/build.just
  • .just/loadgen.just
  • apps/evm/go.mod
  • apps/grpc/go.mod
  • apps/loadgen/Dockerfile
  • apps/loadgen/README.md
  • apps/loadgen/cmd/burst.go
  • apps/loadgen/cmd/flags_test.go
  • apps/loadgen/cmd/root.go
  • apps/loadgen/cmd/run.go
  • apps/loadgen/cmd/start.go
  • apps/loadgen/cmd/start_test.go
  • apps/loadgen/docker-compose.yml
  • apps/loadgen/go.mod
  • apps/loadgen/internal/matrix/matrix.go
  • apps/loadgen/internal/matrix/matrix_test.go
  • apps/loadgen/internal/runner/runner.go
  • apps/loadgen/internal/runner/runner_test.go
  • apps/loadgen/internal/spamoor/client.go
  • apps/loadgen/internal/spamoor/config.go
  • apps/loadgen/internal/spamoor/config_test.go
  • apps/loadgen/main.go
  • apps/loadgen/matrices/baseline.json
  • apps/loadgen/matrices/burst.json
  • apps/testapp/go.mod
  • go.mod
  • justfile
✅ Files skipped from review due to trivial changes (4)
  • apps/testapp/go.mod
  • go.mod
  • .gitignore
  • apps/loadgen/matrices/baseline.json
🚧 Files skipped from review as they are similar to previous changes (13)
  • apps/evm/go.mod
  • justfile
  • apps/loadgen/cmd/run.go
  • apps/loadgen/main.go
  • .just/loadgen.just
  • apps/loadgen/docker-compose.yml
  • apps/loadgen/cmd/flags_test.go
  • apps/loadgen/matrices/burst.json
  • .just/build.just
  • apps/loadgen/cmd/start_test.go
  • apps/grpc/go.mod
  • apps/loadgen/Dockerfile
  • apps/loadgen/cmd/start.go

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 2

🧹 Nitpick comments (2)
apps/loadgen/cmd/burst.go (2)

24-29: ⚡ Quick win

Consider wrapping errors with command context.

The errors returned from WaitForSync and ExecuteMatrixWithOverridesFromFile lack command-specific context. Wrapping them would help users and operators identify which command failed during debugging.

♻️ Suggested error wrapping
 			if err := runner.WaitForSync(cmd.Context(), api); err != nil {
-				return err
+				return fmt.Errorf("burst: waiting for sync: %w", err)
 			}
 
 			log.Printf("==> burst workload starting (%d tx)", txCount)
-			return runner.ExecuteMatrixWithOverridesFromFile(cmd.Context(), matrixPath, api, txCount)
+			if err := runner.ExecuteMatrixWithOverridesFromFile(cmd.Context(), matrixPath, api, txCount); err != nil {
+				return fmt.Errorf("burst: executing matrix: %w", err)
+			}
+			return nil

Add the import:

import (
	"fmt"
	"log"
	// ... existing imports
)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/burst.go` around lines 24 - 29, Wrap errors returned by
runner.WaitForSync and runner.ExecuteMatrixWithOverridesFromFile with
command-specific context before returning: import fmt, and change the return
paths so that if WaitForSync(...) or ExecuteMatrixWithOverridesFromFile(...)
returns an error you return fmt.Errorf("burst: WaitForSync failed: %w", err) and
fmt.Errorf("burst: ExecuteMatrixWithOverridesFromFile failed (matrix=%s tx=%d):
%w", matrixPath, txCount, err) respectively; keep existing log.Printf("==> burst
workload starting (%d tx)", txCount) as-is but ensure the wrapped errors use
cmd.Context(), runner and the matrixPath/txCount values for clear diagnostics.

Source: Coding guidelines


33-33: ⚡ Quick win

Validate --tx-count is positive.

The flag accepts any integer, including zero or negative values. While the runner treats txCount <= 0 as "use matrix defaults," users passing --tx-count=0 or --tx-count=-500 likely made a mistake and would benefit from clearer feedback.

🛡️ Add validation in RunE
 		RunE: func(cmd *cobra.Command, args []string) error {
+			if txCount <= 0 {
+				return fmt.Errorf("--tx-count must be positive, got %d", txCount)
+			}
 			api := spamoor.NewClient(resolveSpamoorURL())
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/cmd/burst.go` at line 33, The flag txCount (declared via
cmd.Flags().IntVar(&txCount, "tx-count", ...)) currently allows zero or negative
values; add validation at the start of the burst command's RunE (or the function
that executes the command) to check if txCount <= 0 and return a clear error
(e.g., fmt.Errorf("invalid --tx-count: must be a positive integer")) so users
get immediate feedback; reference the txCount variable and the command's RunE
handler when applying the change and adjust any tests or callers that expect the
previous behavior.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/ci.yml:
- Line 21: Replace the tag-only usage of actions/checkout@v6.0.3 in the CI
workflow with the pinned commit SHA used elsewhere
(actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10) to prevent
tag-movement attacks; update the reference in .github/workflows/ci.yml so the
checkout step uses the full SHA instead of the v6.0.3 tag (leave other matrix
entries like apps/loadgen/Dockerfile unchanged).

In `@apps/loadgen/internal/spamoor/config_test.go`:
- Around line 21-36: The test passes a non-numeric value for BENCH_REBROADCAST
causing BuildScenarioConfig to leave cfg["rebroadcast"] as the original string;
change the test to supply a numeric string (e.g., "120") for BENCH_REBROADCAST
and update the assertion to expect uint64(120) for cfg["rebroadcast"] so it
matches the strconv.ParseUint path in BuildScenarioConfig.

---

Nitpick comments:
In `@apps/loadgen/cmd/burst.go`:
- Around line 24-29: Wrap errors returned by runner.WaitForSync and
runner.ExecuteMatrixWithOverridesFromFile with command-specific context before
returning: import fmt, and change the return paths so that if WaitForSync(...)
or ExecuteMatrixWithOverridesFromFile(...) returns an error you return
fmt.Errorf("burst: WaitForSync failed: %w", err) and fmt.Errorf("burst:
ExecuteMatrixWithOverridesFromFile failed (matrix=%s tx=%d): %w", matrixPath,
txCount, err) respectively; keep existing log.Printf("==> burst workload
starting (%d tx)", txCount) as-is but ensure the wrapped errors use
cmd.Context(), runner and the matrixPath/txCount values for clear diagnostics.
- Line 33: The flag txCount (declared via cmd.Flags().IntVar(&txCount,
"tx-count", ...)) currently allows zero or negative values; add validation at
the start of the burst command's RunE (or the function that executes the
command) to check if txCount <= 0 and return a clear error (e.g.,
fmt.Errorf("invalid --tx-count: must be a positive integer")) so users get
immediate feedback; reference the txCount variable and the command's RunE
handler when applying the change and adjust any tests or callers that expect the
previous behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: fb2a554e-5bb0-4d69-8091-1c8d7d7a3aa0

📥 Commits

Reviewing files that changed from the base of the PR and between 32c010f and fa30616.

⛔ Files ignored due to path filters (5)
  • apps/evm/go.sum is excluded by !**/*.sum
  • apps/grpc/go.sum is excluded by !**/*.sum
  • apps/loadgen/go.sum is excluded by !**/*.sum
  • apps/testapp/go.sum is excluded by !**/*.sum
  • go.sum is excluded by !**/*.sum
📒 Files selected for processing (29)
  • .github/workflows/ci.yml
  • .gitignore
  • .just/build.just
  • .just/loadgen.just
  • apps/evm/go.mod
  • apps/grpc/go.mod
  • apps/loadgen/Dockerfile
  • apps/loadgen/README.md
  • apps/loadgen/cmd/burst.go
  • apps/loadgen/cmd/flags_test.go
  • apps/loadgen/cmd/root.go
  • apps/loadgen/cmd/run.go
  • apps/loadgen/cmd/start.go
  • apps/loadgen/cmd/start_test.go
  • apps/loadgen/docker-compose.yml
  • apps/loadgen/go.mod
  • apps/loadgen/internal/matrix/matrix.go
  • apps/loadgen/internal/matrix/matrix_test.go
  • apps/loadgen/internal/runner/runner.go
  • apps/loadgen/internal/runner/runner_test.go
  • apps/loadgen/internal/spamoor/client.go
  • apps/loadgen/internal/spamoor/config.go
  • apps/loadgen/internal/spamoor/config_test.go
  • apps/loadgen/main.go
  • apps/loadgen/matrices/baseline.json
  • apps/loadgen/matrices/burst.json
  • apps/testapp/go.mod
  • go.mod
  • justfile
✅ Files skipped from review due to trivial changes (4)
  • apps/testapp/go.mod
  • go.mod
  • .gitignore
  • apps/loadgen/matrices/baseline.json
🚧 Files skipped from review as they are similar to previous changes (13)
  • apps/evm/go.mod
  • justfile
  • apps/loadgen/cmd/run.go
  • apps/loadgen/main.go
  • .just/loadgen.just
  • apps/loadgen/docker-compose.yml
  • apps/loadgen/cmd/flags_test.go
  • apps/loadgen/matrices/burst.json
  • .just/build.just
  • apps/loadgen/cmd/start_test.go
  • apps/grpc/go.mod
  • apps/loadgen/Dockerfile
  • apps/loadgen/cmd/start.go
🛑 Comments failed to post (2)
.github/workflows/ci.yml (1)

21-21: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check if other actions in the repository use SHA pinning or version tags

rg -n 'uses:.*@' .github/workflows/ -C2

Repository: evstack/ev-node

Length of output: 36341


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== ci.yml lines 1-40 ==="
nl -ba .github/workflows/ci.yml | sed -n '1,40p'

echo "=== ci.yml lines 70-110 ==="
nl -ba .github/workflows/ci.yml | sed -n '70,110p'

Repository: evstack/ev-node

Length of output: 146


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== .github/workflows/ci.yml (lines 15-30) ==="
awk 'NR>=15 && NR<=30 {printf "%d:%s\n", NR, $0}' .github/workflows/ci.yml

echo "=== .github/workflows/ci.yml (lines 75-95) ==="
awk 'NR>=75 && NR<=95 {printf "%d:%s\n", NR, $0}' .github/workflows/ci.yml

Repository: evstack/ev-node

Length of output: 1328


Pin actions/checkout to a commit SHA in .github/workflows/ci.yml

  • .github/workflows/ci.yml uses actions/checkout@v6.0.3 (tag-only). Pin to the full SHA (repo already uses actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 in benchmark.yml) to avoid tag-movement attacks.
  • The ev-node-loadgen matrix entry (apps/loadgen/Dockerfile) is fine.
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 21-21: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 21-21: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/ci.yml at line 21, Replace the tag-only usage of
actions/checkout@v6.0.3 in the CI workflow with the pinned commit SHA used
elsewhere (actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10) to prevent
tag-movement attacks; update the reference in .github/workflows/ci.yml so the
checkout step uses the full SHA instead of the v6.0.3 tag (leave other matrix
entries like apps/loadgen/Dockerfile unchanged).

Source: Linters/SAST tools

apps/loadgen/internal/spamoor/config_test.go (1)

21-36: ⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for rebroadcast field/usage in vendored or documented spamoor configs
rg -n --type=go -C3 'rebroadcast' -g '!**/*_test.go'

Repository: evstack/ev-node

Length of output: 610


🌐 Web query:

spamoor tastora framework scenario config rebroadcast field type

💡 Result:

In the context of the Spamoor transaction spamming tool, rebroadcast is a configuration parameter used within the transaction builder and scenario definitions [1][2]. Specifically, in the spamoor configuration schema—often used in automation tools like Kurtosis or test frameworks—the rebroadcast field is defined as an integer type [2]. This field controls the number of times a transaction will be rebroadcasted if it is not confirmed within the expected timeframe [3]. Within the spamoor/txbuilder Go package, the SendTransactionOptions struct includes MaxRebroadcasts (integer) and RebroadcastInterval (time duration) fields to manage this behavior [3]. In YAML configuration files for Spamoor scenarios, this is typically set under the config block, for example: yaml spamoor_params: spammers: - scenario: blobs config: # ... rebroadcast: 0 # Integer field type Tastora is a separate, specialized testing and development framework for the Celestia ecosystem [4][5]. While Tastora integrates or interacts with tools like Spamoor—for example, by including framework/docker/evstack/spamoor/node.go to facilitate testing scenarios within the Celestia/EVStack environment—the rebroadcast parameter itself is a core component of the Spamoor tool's own configuration schema rather than a native field type of the Tastora framework [6][7].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== BuildScenarioConfig implementation =="
sed -n '1,200p' apps/loadgen/internal/spamoor/config.go

echo
echo "== Search for cfg[\"rebroadcast\"] usage =="
rg -n --type=go 'cfg\["rebroadcast"\]' apps/loadgen/internal/spamoor || true

echo
echo "== Search for rebroadcast usage (non-test) =="
rg -n --type=go 'rebroadcast' apps/loadgen/internal/spamoor -g '!**/*_test.go'

Repository: evstack/ev-node

Length of output: 1364


Fix rebroadcast test/input to use a numeric value (not "true")
In apps/loadgen/internal/spamoor/config_test.go (lines 21-36), BuildScenarioConfig maps BENCH_REBROADCAST to cfg["rebroadcast"], converts it to uint64 only if strconv.ParseUint succeeds, and otherwise leaves it as the original string (so "true" becomes the string "true"). Since Spamoor’s rebroadcast scenario option is a numeric/integer field (count), the test should pass a numeric string (e.g., "120") and assert uint64(120) instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/loadgen/internal/spamoor/config_test.go` around lines 21 - 36, The test
passes a non-numeric value for BENCH_REBROADCAST causing BuildScenarioConfig to
leave cfg["rebroadcast"] as the original string; change the test to supply a
numeric string (e.g., "120") for BENCH_REBROADCAST and update the assertion to
expect uint64(120) for cfg["rebroadcast"] so it matches the strconv.ParseUint
path in BuildScenarioConfig.

@chatton chatton enabled auto-merge June 11, 2026 11:54
@chatton chatton added this pull request to the merge queue Jun 11, 2026
Merged via the queue into main with commit 3fed700 Jun 11, 2026
31 of 32 checks passed
@chatton chatton deleted the cian/benchmarks-binary branch June 11, 2026 12:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants