To reduce bot traffic you must login to view /gokrazy/tools/src/commit/9a9519186b1b166c83808face703d256713017b7/integration/gokrun.
The GitHub login only links via username.
Files
Damiano Donati ef964f3028 gok logs: fix data race on http.Client.CheckRedirect
eventsource.SubscribeWith mutates the passed http.Client's
CheckRedirect field. Since streamLog is called concurrently for
stdout and stderr, this is a data race. Clone the client before
passing it to the library.

Add TestStreamLogConcurrentCancel to exercise both the concurrent
client usage and context cancellation under the race detector.
2026-06-23 08:58:23 +02:00

81 lines
2.6 KiB
Go

package gok
import (
"context"
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
"testing"
"golang.org/x/sync/errgroup"
)
// TestStreamLogConcurrentCancel exercises two races that existed in streamLog:
//
// 1. CheckRedirect data race: eventsource.SubscribeWith mutates
// http.Client.CheckRedirect. logsImplConfig.run calls streamLog
// concurrently for stdout and stderr with the same *http.Client,
// so both goroutines wrote to the same field. Fixed by cloning
// the client in streamLog before passing it to the library.
//
// 2. send-on-closed-channel panic: the old code deferred stream.Close(),
// which closes the Events/Errors channels. The library's background
// goroutine could still be sending on those channels after Close
// was called. Fixed by not calling stream.Close() at all — context
// cancellation aborts the HTTP request and the goroutine exits.
//
// Run with -race to verify both fixes.
func TestStreamLogConcurrentCancel(t *testing.T) {
// Mock SSE server: streams events as fast as possible until the
// client disconnects (simulates a long-lived log stream).
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/event-stream")
f, ok := w.(http.Flusher)
if !ok {
http.Error(w, "no flusher", http.StatusInternalServerError)
return
}
// Emit SSE events as fast as the client can read them.
// The loop never ends on its own; it only stops when the write fails
// after streamLog's context is canceled and the HTTP connection drops.
for i := 0; ; i++ {
_, err := fmt.Fprintf(w, "data: line %d\n\n", i)
if err != nil {
return
}
// Push each event immediately so both streamLog goroutines stay
// busy reading while we cancel the context below.
f.Flush()
}
}))
defer srv.Close()
ctx, cancel := context.WithCancel(context.Background())
cfg := &logsImplConfig{}
client := srv.Client()
// Start two concurrent streamLog calls sharing the same
// *http.Client, mirroring how logsImplConfig.run starts
// stdout and stderr streams via errgroup.
// Under -race this detects the CheckRedirect data race (fix 1).
var eg errgroup.Group
for range 2 {
eg.Go(func() error {
return cfg.streamLog(ctx, io.Discard, srv.URL, client)
})
}
// Cancel the context while both streams are active.
// Before the fix, this triggered a panic in the eventsource
// library's background goroutine (fix 2).
cancel()
err := eg.Wait()
if err != nil && !strings.Contains(err.Error(), "context canceled") {
t.Fatalf("unexpected error: %v", err)
}
}