mirror of
https://github.com/kubernetes/client-go.git
synced 2025-09-06 01:20:40 +00:00
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:
committed by
Kubernetes Publisher
parent
8a672f0fd2
commit
01ab7fb211
@@ -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))
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user