Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

client: ensure requests are restarted in a race-free way. #71

Merged

Conversation

mhilton
Copy link
Contributor

@mhilton mhilton commented Aug 10, 2018

HTTP clients can return whilst they are still reading a request
body. Ensure that any request with a body is not restarted until the
previous request has closed it.

Copy link
Member

@rogpeppe rogpeppe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM modulo a couple of thoughts and suggestions, thanks.

if err != nil {
return
if requestData.ReqReader != nil && requestData.GetReqReader == nil {
requestData.ReqReader, requestData.GetReqReader = gooseio.MakeGetReqReader(requestData.ReqReader, int64(requestData.ReqLength))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it did this before too, but side-effecting the contents of requestData is really quite icky. A caller might be reasonably entitled to assume that the ReqReader they put into the RequestData is the same after the call as before.

if err != nil {
return nil, errors.Newf(err, "failed creating the request %s", URL)
}
req.GetBody = getReqReader
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some readers, GetBody is populated automatically; could use that if it does it. shrug

return br, br.getReqReader
}

type bufferingReader struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice to have a high level description of what this type does.

rp, wp int
}

func (r *bufferingReader) Read(p []byte) (n int, err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Much as I appreciate a nice bit of slice twiddling, I think it might be possible to avoid it in this case. How about something like this?

type bufferingReader struct {
	mu  sync.Mutex
	buf bytes.Buffer
	r   io.Reader
}

func (r *bufferingReader) getReqReader() (io.ReadCloser, error) {
	r.mu.Lock()
	return &unlockingReadCloser{
		mu: &r.mu,
		Reader: io.MultiReader(
			bytes.NewReader(r.buf.Bytes()),
			io.TeeReader(r.r, &r.buf),
		),
	}, nil
}

type seekingReader struct {
	mu sync.Mutex
	r  io.ReadSeeker
}

func (r *seekingReader) getReqReader() (io.ReadCloser, error) {
	r.mu.Lock()
	if _, err := r.r.Seek(0, 0); err != nil {
		r.mu.Unlock()
		return nil, err
	}
	return &unlockingReadCloser{
		mu:     &r.mu,
		Reader: r.r,
	}, nil
}

type unlockingReadCloser struct {
	mu *sync.Mutex
	io.Reader
}

func (c *unlockingReadCloser) Close() error {
	if c.mu != nil {
		c.mu.Unlock()
		c.mu = nil
	}
	return nil
}

@mhilton mhilton force-pushed the 002-ensure-restartable-requests-race-free branch from 82e5502 to 1fb000a Compare August 10, 2018 11:34
Copy link
Member

@rogpeppe rogpeppe left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great, but maybe a final tweak might be good?

}
if length > maxBufSize {
return nil, fmt.Errorf("body of length %d is too large to hold in memory (max %d bytes)", length, maxBufSize)
if size > maxBufSize {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This means that we flatly refuse to process any requests that are larger than the requested size. I thought we could do better than that, because it's quite possible that we will receive the error before all of maxBufSize bytes have been read.

The way I was previously envisaging can succeed in that case, I think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is still better than the previous behavior which wouldn't even try the first time if it thought it was too long.

mu.Lock()
return &unlockingReadCloser{
mu: &mu,
Reader: io.MultiReader(bytes.NewReader(buf.Bytes()), io.TeeReader(io.LimitReader(r, size-int64(buf.Len())), &buf)),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite long. Maybe put the various readers on different lines?
A comment would probably also be appropriate.

@mhilton mhilton force-pushed the 002-ensure-restartable-requests-race-free branch from 1fb000a to 1f1e1bd Compare August 10, 2018 13:12
HTTP clients can return whilst they are still reading a request
body. Ensure that any request with a body is not restarted until the
previous request has closed it.
@mhilton mhilton force-pushed the 002-ensure-restartable-requests-race-free branch from 1f1e1bd to 03fdd0e Compare August 10, 2018 13:23
@rogpeppe rogpeppe merged commit 03fdd0e into go-goose:v2 Aug 14, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants