"strconv"
"strings"
"sync"
+ "sync/atomic"
"testing"
"time"
)
func (f closerFunc) Close() error { return f() }
+type writerFuncConn struct {
+ net.Conn
+ write func(p []byte) (n int, err error)
+}
+
+func (c writerFuncConn) Write(p []byte) (n int, err error) { return c.write(p) }
+
// Issue 4677. If we try to reuse a connection that the server is in the
// process of closing, we may end up successfully writing out our request (or a
// portion of our request) only to find a connection error when we try to read
func TestRetryIdempotentRequestsOnError(t *testing.T) {
defer afterTest(t)
+ var (
+ mu sync.Mutex
+ logbuf bytes.Buffer
+ )
+ logf := func(format string, args ...interface{}) {
+ mu.Lock()
+ defer mu.Unlock()
+ fmt.Fprintf(&logbuf, format, args...)
+ logbuf.WriteByte('\n')
+ }
+
ts := httptest.NewServer(HandlerFunc(func(w ResponseWriter, r *Request) {
+ logf("Handler")
+ w.Header().Set("X-Status", "ok")
}))
defer ts.Close()
- tr := &Transport{}
+ var writeNumAtomic int32
+ tr := &Transport{
+ Dial: func(network, addr string) (net.Conn, error) {
+ logf("Dial")
+ c, err := net.Dial(network, ts.Listener.Addr().String())
+ if err != nil {
+ logf("Dial error: %v", err)
+ return nil, err
+ }
+ return &writerFuncConn{
+ Conn: c,
+ write: func(p []byte) (n int, err error) {
+ if atomic.AddInt32(&writeNumAtomic, 1) == 2 {
+ logf("intentional write failure")
+ return 0, errors.New("second write fails")
+ }
+ logf("Write(%q)", p)
+ return c.Write(p)
+ },
+ }, nil
+ },
+ }
+ defer tr.CloseIdleConnections()
c := &Client{Transport: tr}
- const N = 2
- retryc := make(chan struct{}, N)
SetRoundTripRetried(func() {
- retryc <- struct{}{}
+ logf("Retried.")
})
defer SetRoundTripRetried(nil)
- for n := 0; n < 100; n++ {
- // open 2 conns
- errc := make(chan error, N)
- for i := 0; i < N; i++ {
- // start goroutines, send on errc
- go func() {
- res, err := c.Get(ts.URL)
- if err == nil {
- res.Body.Close()
- }
- errc <- err
- }()
- }
- for i := 0; i < N; i++ {
- if err := <-errc; err != nil {
- t.Fatal(err)
- }
- }
-
- ts.CloseClientConnections()
- for i := 0; i < N; i++ {
- go func() {
- res, err := c.Get(ts.URL)
- if err == nil {
- res.Body.Close()
- }
- errc <- err
- }()
+ for i := 0; i < 3; i++ {
+ res, err := c.Get("http://fake.golang/")
+ if err != nil {
+ t.Fatalf("i=%d: Get = %v", i, err)
}
+ res.Body.Close()
+ }
- for i := 0; i < N; i++ {
- if err := <-errc; err != nil {
- t.Fatal(err)
- }
- }
- for i := 0; i < N; i++ {
- select {
- case <-retryc:
- // we triggered a retry, test was successful
- t.Logf("finished after %d runs\n", n)
- return
- default:
- }
- }
+ mu.Lock()
+ got := logbuf.String()
+ mu.Unlock()
+ const want = `Dial
+Write("GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n")
+Handler
+intentional write failure
+Retried.
+Dial
+Write("GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n")
+Handler
+Write("GET / HTTP/1.1\r\nHost: fake.golang\r\nUser-Agent: Go-http-client/1.1\r\nAccept-Encoding: gzip\r\n\r\n")
+Handler
+`
+ if got != want {
+ t.Errorf("Log of events differs. Got:\n%s\nWant:\n%s", got, want)
}
- t.Fatal("did not trigger any retries")
}
// Issue 6981