test(s3select): stop racing minio-go's parser for the response body

SelectResults spawns a parser goroutine that drains and closes the
response body when the stream ends; deferring res.Close() had the test
drain and close the same bytes.Reader concurrently, which the race
detector catches reliably. Give the test body a close signal and wait
for the parser to finish instead of competing with it.

The double-close lives in minio-go's client parser, which no server
code path uses; it remains worth an upstream report.

Co-authored-by: ChatGPT <noreply@openai.com>
Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Feng Ruohang
2026-08-04 21:47:33 +08:00
parent 1814ae52f4
commit 475236c79c
+22 -4
View File
@@ -26,6 +26,7 @@ import (
"os" "os"
"reflect" "reflect"
"strings" "strings"
"sync"
"testing" "testing"
"github.com/klauspost/cpuid/v2" "github.com/klauspost/cpuid/v2"
@@ -70,6 +71,17 @@ func (w *testResponseWriter) WriteHeader(statusCode int) {
func (w *testResponseWriter) Flush() { func (w *testResponseWriter) Flush() {
} }
type testResponseBody struct {
io.Reader
closed chan struct{}
closeOnce sync.Once
}
func (b *testResponseBody) Close() error {
b.closeOnce.Do(func() { close(b.closed) })
return nil
}
func evaluateSelectForTest(t *testing.T, requestXML, input []byte) ([]byte, error) { func evaluateSelectForTest(t *testing.T, requestXML, input []byte) ([]byte, error) {
t.Helper() t.Helper()
@@ -85,18 +97,24 @@ func evaluateSelectForTest(t *testing.T, requestXML, input []byte) ([]byte, erro
s3Select.Evaluate(w) s3Select.Evaluate(w)
s3Select.Close() s3Select.Close()
body := &testResponseBody{
Reader: bytes.NewReader(w.response),
closed: make(chan struct{}),
}
resp := http.Response{ resp := http.Response{
StatusCode: http.StatusOK, StatusCode: http.StatusOK,
Body: io.NopCloser(bytes.NewReader(w.response)), Body: body,
ContentLength: int64(len(w.response)), ContentLength: int64(len(w.response)),
} }
res, err := minio.NewSelectResults(&resp, "testbucket") res, err := minio.NewSelectResults(&resp, "testbucket")
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
defer res.Close() result, readErr := io.ReadAll(res)
// minio-go's parser closes the response after signaling EOF or an event
return io.ReadAll(res) // error. Wait for that owner instead of racing it with SelectResults.Close.
<-body.closed
return result, readErr
} }
func TestJSONLinesRejectsOversizedRecord(t *testing.T) { func TestJSONLinesRejectsOversizedRecord(t *testing.T) {