-
Notifications
You must be signed in to change notification settings - Fork 62
fix leaking connections when user client closes connection #220
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -91,6 +91,18 @@ type ClientConfig struct { | |
ForceHttp2 bool | ||
} | ||
|
||
type RelayServerError struct { | ||
msg string | ||
} | ||
|
||
func NewRelayServerError(msg string) error { | ||
return &RelayServerError{msg} | ||
} | ||
|
||
func (e *RelayServerError) Error() string { | ||
return e.msg | ||
} | ||
|
||
func DefaultClientConfig() ClientConfig { | ||
return ClientConfig{ | ||
RemoteRequestTimeout: 60 * time.Second, | ||
|
@@ -384,7 +396,7 @@ func (c *Client) postResponse(remote *http.Client, br *pb.HttpResponse) error { | |
return fmt.Errorf("couldn't read relay server's response body: %v", err) | ||
} | ||
if resp.StatusCode != http.StatusOK { | ||
err := fmt.Errorf("relay server responded %s: %s", http.StatusText(resp.StatusCode), body) | ||
err := NewRelayServerError(fmt.Sprintf("relay server responded %s: %s", http.StatusText(resp.StatusCode), body)) | ||
if resp.StatusCode == http.StatusBadRequest { | ||
// http-relay-server may have restarted during the request. | ||
return backoff.Permanent(err) | ||
|
@@ -643,8 +655,11 @@ func (c *Client) handleRequest(remote *http.Client, local *http.Client, pbreq *p | |
log.Printf("[%s] Failed to post response to relay: %v", *resp.Id, err) | ||
}, | ||
) | ||
if _, ok := err.(*backoff.PermanentError); ok { | ||
if _, ok := err.(*RelayServerError); ok { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is a change in behavior: Before, only HTTP 400 would terminate the loop, now anything other than HTTP 200 terminates the loop. For example, if the nginx ingress is overloaded and returns 500s for a while, this will now terminate the request. I think this is a good thing: There is no reason for us to drop a chunk from the middle of the response body, and it's better to terminate the request. I think you could go even further: any error returned from RetryNotify indicates that we aren't sure we've posted the response, and that we should stop this loop rather than incorrectly continuing with the next response chunk. WDYT? Please also delete the next comment which is no longer true (this could be a "transient" 5xx error that lasted too long). Maybe There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That sounds good, silently dropping chunks will cause clients to receive corrupted data, it breaks the normal assumption of http/tcp connections. |
||
// A permanent error suggests the request should be aborted. | ||
log.Printf("[%s] %s", *resp.Id, err) | ||
log.Printf("[%s] Closing backend connection", *resp.Id) | ||
hresp.Body.Close() | ||
break | ||
} | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -171,6 +171,12 @@ func (r *broker) RelayRequest(server string, request *pb.HttpRequest) (<-chan *p | |
} | ||
} | ||
|
||
// StopRelayRequest forgets a relaying request, this causes the next chunk from the backend | ||
// with the relay id to not be recognized, resulting in the relay server returning an error. | ||
func (r *broker) StopRelayRequest(requestId string) { | ||
delete(r.resp, requestId) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Please hold the mutex while accessing the map, Go maps are not thread-safe. |
||
} | ||
|
||
// GetRequest obtains a client's request for the server identifier. It blocks | ||
// until a client makes a request. | ||
func (r *broker) GetRequest(ctx context.Context, server, path string) (*pb.HttpRequest, error) { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -15,6 +15,7 @@ | |
package main | ||
|
||
import ( | ||
"bufio" | ||
"bytes" | ||
"context" | ||
"fmt" | ||
|
@@ -128,7 +129,7 @@ func (r *relay) stop() error { | |
} | ||
|
||
// TestHttpRelay launches a local http relay (client + server) and connects a | ||
// test-hhtp-server as a backend. The test is then interacting with the backend | ||
// test-http-server as a backend. The test is then interacting with the backend | ||
// through the local relay. | ||
func TestHttpRelay(t *testing.T) { | ||
tests := []struct { | ||
|
@@ -201,6 +202,67 @@ func TestHttpRelay(t *testing.T) { | |
} | ||
} | ||
|
||
// TestDroppedUserClientFreesRelayChannel checks that when the user client closes a connection, | ||
// it is propagated to the relay server and client, closing the backend connection as well. | ||
func TestDroppedUserClientFreesRelayChannel(t *testing.T) { | ||
// setup http test server | ||
connClosed := make(chan error) | ||
defer close(connClosed) | ||
finishServer := make(chan bool) | ||
defer close(finishServer) | ||
|
||
// mock a long running backend that uses chunking to send periodic updates | ||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { | ||
for { | ||
select { | ||
case <-finishServer: | ||
return | ||
default: | ||
if _, err := fmt.Fprintln(w, "DEADBEEF"); err != nil { | ||
connClosed <- err | ||
return | ||
} | ||
if flusher, ok := w.(http.Flusher); ok { | ||
flusher.Flush() | ||
} else { | ||
t.Fatal("cannot flush") | ||
} | ||
time.Sleep(time.Second) | ||
} | ||
} | ||
})) | ||
defer func() { ts.Close() }() | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this can be |
||
|
||
backendAddress := strings.TrimPrefix(ts.URL, "http://") | ||
r := &relay{} | ||
if err := r.start(backendAddress); err != nil { | ||
t.Fatal("failed to start relay: ", err) | ||
} | ||
defer func() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would just do |
||
if err := r.stop(); err != nil { | ||
t.Fatal("failed to stop relay: ", err) | ||
} | ||
}() | ||
relayAddress := "http://127.0.0.1:" + r.rsPort | ||
|
||
res, err := http.Get(relayAddress + "/client/remote1/") | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
// receive the first chunk then terminates the connection | ||
if _, err := bufio.NewReader(res.Body).ReadString('\n'); err != nil { | ||
t.Fatal(err) | ||
} | ||
res.Body.Close() | ||
|
||
// wait for up to 30s for backend connection to be closed | ||
select { | ||
case <-connClosed: | ||
case <-time.After(30 * time.Second): | ||
t.Error("Server did not close connection") | ||
} | ||
} | ||
|
||
type testServer struct { | ||
testpb.UnimplementedTestServiceServer | ||
responsePayload []byte | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please extend this comment to say "or the client cancelled the request."