Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions internal/benchrunner/runners/rally/runner.go
Original file line number Diff line number Diff line change
Expand Up @@ -925,16 +925,26 @@ func (r *runner) runRally(ctx context.Context) ([]rallyStat, error) {
return nil, fmt.Errorf("could not open esrally report in path: %s: %w", r.svcInfo.Logs.Folder.Local, err)
}

reader := csv.NewReader(reportCSV)
return parseRallyReport(reportCSV, r.svcInfo.Logs.Folder.Local, errOutput.String())

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The defer should rather be placed in the same method that opens the file. This avoids introducing an unexpected side effect in the function that just parses what is in the reader.

Suggested change
return parseRallyReport(reportCSV, r.svcInfo.Logs.Folder.Local, errOutput.String())
defer reportCSV.Close()
return parseRallyReport(reportCSV, r.svcInfo.Logs.Folder.Local, errOutput.String())

@buley buley Mar 27, 2026

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for the suggestion @jsoriano. I moved defer reportCSV.Close() back into runRally immediately after os.Open, so the function that opens the file also owns closing it. parseRallyReport now takes an io.Reader and only parses the CSV, which removes the side effect you called out. I also updated the test to verify parser output rather than close behavior.

}

func parseRallyReport(report io.ReadCloser, logsPath string, stderr string) (_ []rallyStat, err error) {
defer func() {
closeErr := report.Close()
if err == nil && closeErr != nil {
err = fmt.Errorf("could not close esrally report in path: %s: %w", logsPath, closeErr)
}
}()

reader := csv.NewReader(report)
stats := make([]rallyStat, 0)
for {
record, err := reader.Read()
if err == io.EOF {
record, readErr := reader.Read()
if readErr == io.EOF {
break
}
if err != nil {
return nil, fmt.Errorf("could not read esrally report in path: %s (stderr=%q): %w", r.svcInfo.Logs.Folder.Local, errOutput.String(), err)
if readErr != nil {
return nil, fmt.Errorf("could not read esrally report in path: %s (stderr=%q): %w", logsPath, stderr, readErr)
}

stats = append(stats, rallyStat{Metric: record[0], Task: record[1], Value: record[2], Unit: record[3]})
Expand Down
54 changes: 54 additions & 0 deletions internal/benchrunner/runners/rally/runner_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package rally

import (
"errors"
"io"
"strings"
"testing"
)

type trackingReadCloser struct {
io.Reader
closed bool
closeErr error
}

func (r *trackingReadCloser) Close() error {
r.closed = true
return r.closeErr
}

func TestParseRallyReportClosesReport(t *testing.T) {
report := &trackingReadCloser{
Reader: strings.NewReader("metric,task,value,unit\n"),
}

stats, err := parseRallyReport(report, "/tmp/rally", "")
if err != nil {
t.Fatalf("parseRallyReport returned error: %v", err)
}
if !report.closed {
t.Fatal("parseRallyReport did not close the report reader")
}
if len(stats) != 1 {
t.Fatalf("expected 1 stat, got %d", len(stats))
}
}

func TestParseRallyReportReturnsCloseError(t *testing.T) {
report := &trackingReadCloser{
Reader: strings.NewReader("metric,task,value,unit\n"),
closeErr: errors.New("close failed"),
}

_, err := parseRallyReport(report, "/tmp/rally", "")
if err == nil {
t.Fatal("parseRallyReport returned nil error")
}
if !report.closed {
t.Fatal("parseRallyReport did not close the report reader")
}
if !strings.Contains(err.Error(), "close failed") {
t.Fatalf("expected close error, got %v", err)
}
}