client-go: reset request body after response is read and closed

This commit refactors the retry logic to include resetting the
request body. The reset logic will be called iff it is not the
first attempt. This refactor is nescessary mainly because now
as per the retry logic, we always ensure that the request body
is reset *after* the response body is *fully* read and closed
in order to reuse the same TCP connection.

Previously, the reset of the request body and the call to read
and close the response body were not in the right order, which
leads to race conditions.

This commit also adds a test that verifies the order in which
the function calls are made to ensure that we seek only after
the response body is closed.

Co-authored-by: Madhav Jivrajani <madhav.jiv@gmail.com>

Kubernetes-commit: 68c8c458ee8f6629eef806c48c1a776dedad3ec4
This commit is contained in:
Abu Kashem
2022-03-28 20:40:49 -04:00
committed by Kubernetes Publisher
parent 8a672f0fd2
commit 01ab7fb211
3 changed files with 150 additions and 55 deletions

View File

@@ -938,7 +938,7 @@ func TestRequestWatch(t *testing.T) {
},
Err: true,
ErrFn: func(err error) bool {
return apierrors.IsInternalError(err)
return !apierrors.IsInternalError(err) && strings.Contains(err.Error(), "failed to reset the request body while retrying a request: EOF")
},
},
{
@@ -954,7 +954,10 @@ func TestRequestWatch(t *testing.T) {
serverReturns: []responseErr{
{response: nil, err: io.EOF},
},
Empty: true,
Err: true,
ErrFn: func(err error) bool {
return !apierrors.IsInternalError(err)
},
},
{
name: "max retries 2, server always returns a response with Retry-After header",
@@ -1130,7 +1133,7 @@ func TestRequestStream(t *testing.T) {
},
Err: true,
ErrFn: func(err error) bool {
return apierrors.IsInternalError(err)
return !apierrors.IsInternalError(err) && strings.Contains(err.Error(), "failed to reset the request body while retrying a request: EOF")
},
},
{
@@ -1371,8 +1374,6 @@ func (b *testBackoffManager) Sleep(d time.Duration) {
}
func TestCheckRetryClosesBody(t *testing.T) {
// unblock CI until http://issue.k8s.io/108906 is resolved in 1.24
t.Skip("http://issue.k8s.io/108906")
count := 0
ch := make(chan struct{})
testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
@@ -2435,6 +2436,7 @@ func TestRequestWithRetry(t *testing.T) {
body io.Reader
serverReturns responseErr
errExpected error
errContains string
transformFuncInvokedExpected int
roundTripInvokedExpected int
}{
@@ -2451,7 +2453,7 @@ func TestRequestWithRetry(t *testing.T) {
body: &readSeeker{err: io.EOF},
serverReturns: responseErr{response: retryAfterResponse(), err: nil},
errExpected: nil,
transformFuncInvokedExpected: 1,
transformFuncInvokedExpected: 0,
roundTripInvokedExpected: 1,
},
{
@@ -2474,7 +2476,7 @@ func TestRequestWithRetry(t *testing.T) {
name: "server returns retryable err, request body Seek returns error, retry aborted",
body: &readSeeker{err: io.EOF},
serverReturns: responseErr{response: nil, err: io.ErrUnexpectedEOF},
errExpected: io.ErrUnexpectedEOF,
errContains: "failed to reset the request body while retrying a request: EOF",
transformFuncInvokedExpected: 0,
roundTripInvokedExpected: 1,
},
@@ -2517,8 +2519,15 @@ func TestRequestWithRetry(t *testing.T) {
if test.transformFuncInvokedExpected != transformFuncInvoked {
t.Errorf("Expected transform func to be invoked %d times, but got: %d", test.transformFuncInvokedExpected, transformFuncInvoked)
}
if test.errExpected != unWrap(err) {
t.Errorf("Expected error: %v, but got: %v", test.errExpected, unWrap(err))
switch {
case test.errExpected != nil:
if test.errExpected != unWrap(err) {
t.Errorf("Expected error: %v, but got: %v", test.errExpected, unWrap(err))
}
case len(test.errContains) > 0:
if !strings.Contains(err.Error(), test.errContains) {
t.Errorf("Expected error message to caontain: %q, but got: %q", test.errContains, err.Error())
}
}
})
}
@@ -3531,3 +3540,103 @@ func TestTransportConcurrency(t *testing.T) {
})
}
}
// TODO: see if we can consolidate the other trackers into one.
type requestBodyTracker struct {
io.ReadSeeker
f func(string)
}
func (t *requestBodyTracker) Read(p []byte) (int, error) {
t.f("Request.Body.Read")
return t.ReadSeeker.Read(p)
}
func (t *requestBodyTracker) Seek(offset int64, whence int) (int64, error) {
t.f("Request.Body.Seek")
return t.ReadSeeker.Seek(offset, whence)
}
type responseBodyTracker struct {
io.ReadCloser
f func(string)
}
func (t *responseBodyTracker) Read(p []byte) (int, error) {
t.f("Response.Body.Read")
return t.ReadCloser.Read(p)
}
func (t *responseBodyTracker) Close() error {
t.f("Response.Body.Close")
return t.ReadCloser.Close()
}
type recorder struct {
order []string
}
func (r *recorder) record(call string) {
r.order = append(r.order, call)
}
func TestRequestBodyResetOrder(t *testing.T) {
recorder := &recorder{}
respBodyTracker := &responseBodyTracker{
ReadCloser: nil, // the server will fill it
f: recorder.record,
}
var attempts int
client := clientForFunc(func(req *http.Request) (*http.Response, error) {
defer func() {
attempts++
}()
// read the request body.
ioutil.ReadAll(req.Body)
// first attempt, we send a retry-after
if attempts == 0 {
resp := retryAfterResponse()
respBodyTracker.ReadCloser = ioutil.NopCloser(bytes.NewReader([]byte{}))
resp.Body = respBodyTracker
return resp, nil
}
return &http.Response{StatusCode: http.StatusOK}, nil
})
reqBodyTracker := &requestBodyTracker{
ReadSeeker: bytes.NewReader([]byte{}), // empty body ensures one Read operation at most.
f: recorder.record,
}
req := &Request{
verb: "POST",
body: reqBodyTracker,
c: &RESTClient{
content: defaultContentConfig(),
Client: client,
},
backoff: &noSleepBackOff{},
retry: &withRetry{maxRetries: 1},
}
req.Do(context.Background())
expected := []string{
// 1st attempt: the server handler reads the request body
"Request.Body.Read",
// the server sends a retry-after, client reads the
// response body, and closes it
"Response.Body.Read",
"Response.Body.Close",
// client retry logic seeks to the beginning of the request body
"Request.Body.Seek",
// 2nd attempt: the server reads the request body
"Request.Body.Read",
}
if !reflect.DeepEqual(expected, recorder.order) {
t.Errorf("Expected invocation request and response body operations for retry do not match: %s", cmp.Diff(expected, recorder.order))
}
}