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

Implement unary HTTP calls with retry support #649

Merged
merged 5 commits into from
Jan 22, 2024

Conversation

emcfarlane
Copy link
Contributor

Internal changes to the HTTP client to remove some of the overhead required for streaming calls but unnecessary for non-streaming client requests. This PR changes both of the non-streaming client request types: unary and server streams. These types will now create the client request with the message payload upfront before calling client.Do. This removes the overhead of the io.Pipe to convert the body from a reader to a writer and the extra goroutine required to write to the pipe asynchronously. From this we get improved functionality for non-streaming requests by setting the Content-Length header and implementing GetBody function to enable retries.

Implementation details

The buffer reuse semantics are kept the same for both unary and stream requests. This allows for simple error handling with a w.bufferPool.Get() matched to a defer w.bufferPool.Put(). To enforce this behaviour for the new unary calls we must wait for transport to be done with the request body ensuring no reads occur after a return from Send. Under testing a client.Do will almost always drain the body before returning the response. However, we need to enforce this behaviour.

To safely reuse the payload buffer a new type payloadCloser is added to implement the required HTTP body semantics. On receiving a response the request may still be read up and until the response body is closed. See the documentation on RoundTripper for details. To ensure the request body is safe to release we need to wait for a Close on the body. We can eagerly move this check forward by instead waiting for a complete read and then returning io.EOF on subsequent reads. Retries may read, close or rewind the body multiple times before a response is returned and this behaviour is supported. On detecting the payload has been drained or closed the payloadCloser releases the reference to the messagePayload ensuring it's safe to be reused.

Changes

  • Unary and ServerStreams client requests create the request with the body built from the message payload.
  • Content-Length header set for single messages client requests.
  • Implemented GetBody function for retries of single message client requests.messages.

Impact

Benchmarking with -bench=BenchmarkConnect//unary shows a small improvement in throughput and memory use for unary calls (~ -5% sec/op, -5% B/op, -3% allocs/op).

Fix for #541 and #609.

@emcfarlane emcfarlane mentioned this pull request Dec 8, 2023
6 tasks
@mattrobenolt
Copy link
Contributor

👀 I am going to look at this over the weekend!

Changes the internal duplexHTTPCall to switch on non streaming client
requests to block and wait for the response. This avoids the need to
convert the reader to a writer with io.Pipe and the go routine to run
asynchronously. On unary requests we are now able to set the
`Content-Length` header and the `GetBody` function for retries.

To safely reuse the payload buffer a new type `payloadCloser` is added
to implement the required HTTP body semantics. On receiving a response
the request may still be read up and until the response is body is
closed. To ensure the request body is safe to releasee we wait for a
complete read or close on the request body. Retries may read, close or
rewind the body multiple times before a response is returned.
@emcfarlane emcfarlane marked this pull request as ready for review December 9, 2023 00:21
Ensure we set GetBody on all zero length requests.
Copy link
Member

@jhump jhump left a comment

Choose a reason for hiding this comment

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

The blocking Wait makes me slightly nervous. An alternative might be a way to have the call invalidate the payload closer -- so instead of waiting for it to asynchronously complete, we force it complete synchronously. Any concurrent reader/interaction will suddenly get EOF after it's invalidated. That seems much more predictable and safer since it still prevents buffer use-after-release but does so without non-deterministic latency/blocking.

connect_ext_test.go Show resolved Hide resolved
duplex_http_call.go Outdated Show resolved Hide resolved
duplex_http_call.go Outdated Show resolved Hide resolved
duplex_http_call.go Outdated Show resolved Hide resolved
@mattrobenolt
Copy link
Contributor

If you'd like, I can get this into a staging environment for some more thorough testing on my end. This will take a bit, but I think it'll be worth it. This is quite a core change, and I'd rather not push it through too fast. But I'm happy to get this into some chaos monkey on our side.

(We currently don't leverage GetBody, but would like to! It just didn't exist before.)

@emcfarlane
Copy link
Contributor Author

@mattrobenolt that would be incredibly helpful! Will wait to see how this PR resolves and try do the same testing in staging.

Removes the wait and instead simplifies to dropping the payload when the
response is received. Should be valid for all unary servers.
@emcfarlane
Copy link
Contributor Author

Hey @mattrobenolt! I think this PR is in a good state for more testing. Would you like to run it through some chaos testing on your side?

@mattrobenolt
Copy link
Contributor

Hey @mattrobenolt! I think this PR is in a good state for more testing. Would you like to run it through some chaos testing on your side?

Yeah! Lemme construct some tests this weekend.

@jhump
Copy link
Member

jhump commented Jan 22, 2024

@mattrobenolt, we're going to go ahead and merge this. We are happy to iterate on this more post-merge if you see anything, whenever you get a chance to play with it in a staging environment.

@jhump jhump merged commit 0fb13df into connectrpc:main Jan 22, 2024
11 checks passed
@emcfarlane emcfarlane deleted the unaryCalls branch January 22, 2024 20:49
akshayjshah pushed a commit that referenced this pull request Jan 22, 2024
Fixes the LICENSE header for new test file introduced in
#649 . Causing CI failure
on main:
https://github.com/connectrpc/connect-go/actions/runs/7616456681/job/20743252336
@jhump jhump added the enhancement New feature or request label Feb 16, 2024
@jhump jhump mentioned this pull request Feb 16, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants