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

Fix underlying connection not being closed on protocol error #64

Merged
merged 4 commits into from
Feb 7, 2023

Conversation

samherrmann
Copy link
Contributor

Please expand the commit messages for details.

Before this commit, the underlying connection of `Conn` was not being
closed when a protocol error was encountered. This behavior contradicted
with `Conn.DisconnectNotify()` because it reported that the underlying
connection was being closed. Additionally, the underlying connection was
now orphaned because `Conn` was no longer processing any of the
subsequent requests.

With this commit, the underlying connection is now being closed when a
protocol error is encountered, matching what `Conn.DisconnectNotify()`
has already been reporting.
The purpose of this commit is to consolidate assertions that are
required for multiple tests related to `Conn.DisconnectNotify` into a
single function.
@samherrmann
Copy link
Contributor Author

Let me know if you wish to keep the change set of this PR to a minimum and remove the second commit.

@@ -366,11 +366,10 @@ type Conn struct {

h Handler

mu sync.Mutex
shutdown bool
closing bool
Copy link
Contributor Author

@samherrmann samherrmann Feb 7, 2023

Choose a reason for hiding this comment

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

Since there is now a single closing procedure, there doesn't appear to be the need for a closing AND shutdown state anymore.

func (readWriteCloser) Close() error { return nil }
t.Run("EOF", func(t *testing.T) {
connA, connB := net.Pipe()
c := jsonrpc2.NewConn(context.Background(), jsonrpc2.NewPlainObjectStream(connB), nil)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I switched from jsonrpc2.NewBufferedStream(conn, jsonrpc2.VarintObjectCodec{}), nil) to jsonrpc2.NewPlainObjectStream(conn) solely because the latter looks cleaner/shorter. The type of stream being used shouldn't impact the results of the these tests. I am happy to switching back to the former though if you prefer.

@@ -314,105 +314,88 @@ type noopHandler struct{}

func (noopHandler) Handle(ctx context.Context, conn *jsonrpc2.Conn, req *jsonrpc2.Request) {}

type readWriteCloser struct {
read, write func(p []byte) (n int, err error)
}
Copy link
Contributor Author

@samherrmann samherrmann Feb 7, 2023

Choose a reason for hiding this comment

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

Instead of modifying readWriteCloser so that it would allow us to test if it's been closed, I just used net.Pipe() from std lib instead.

@samherrmann samherrmann marked this pull request as ready for review February 7, 2023 04:11
if err != io.ErrUnexpectedEOF && !closing {
c.logger.Printf("jsonrpc2: protocol error: %v\n", err)
}
close(c.disconnect)
Copy link
Contributor Author

@samherrmann samherrmann Feb 7, 2023

Choose a reason for hiding this comment

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

This line is the source of the bug that's being fixed. Before, close(c.disconnect) was being called here but c.stream.Close() was not. That means that code that uses this library was being lied to if they were listening to Conn.DisconnectNotify(). c.stream.Close() was only being called if Conn was explicitly being closed by Conn.Close().

I rearranged the code so that the "closing" procedure is the same regardless if the cause is an error in Conn.readMessages or if the cause is a call to Conn.Close(). This should ensure that the state of the underlying connection matches what Conn.DiconnectNotify() reports.

From a high level, this is what it looks like:

func (c *Conn) readMessages() {
   var err error
   for err == nil {
     // read from underlying connection
   }
   // for-loop exits when an error is encountered.
   c.close(err)
}

func (c *Conn) Close() error {
   return c.close(nil)
}

func (c *Conn) close(cause err) error {
  // Common closing procedure that closes the underlying connection and also closes the 
  // `Conn.disconnect` channel.
}

Copy link
Member

@keegancsmith keegancsmith left a comment

Choose a reason for hiding this comment

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

LGTM! One inline comment, if it looks good to you I'll go ahead and land this :)

jsonrpc2.go Outdated
}

if err := c.stream.Close(); err != nil {
return err
Copy link
Member

Choose a reason for hiding this comment

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

we probably still want to call close(c.disconnect) and c.closed = true even if closing the stream fails.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Agreed. Addressed in 0c9de81.

closing := c.closing
if err == io.EOF {
if closing {
err = ErrClosed
Copy link
Member

Choose a reason for hiding this comment

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

It doesn't seem like in the new codepath we ever set err to ErrClosed. Which means we don't inform c.pending ErrClosed. Given your recent diving into this code you likely have more context to know if this seems correct than I do right now. So is the new code path alright with respect to that?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Excellent observation! I definitely overlooked this before you drew my attention to it. As it turns out, from what I can see, we don't actually need to send ErrClosed through the channel because the channel listener infers ErrClosed when the channel is being closed. Based on that, I'm coming to the conclusion that having the following two lines...

call.done <- ErrClosed
close(call.done)

...is redundant and we can reduce the code to just close(call.done) (see 85075f0). A follow-on question that may arise from my conclusion is "what about errors other than ErrClosed? Now those don't get sent to c.pending". It seems to me that if a connection is being closed, the cause of that closure is not the concern of a pending request. If a pending request is being cancelled due to the connection being closed, then all it cares about is ErrClosed.

Now, this investigation made me pay more attention to TestConn_Close: waiting for response, which is the test that I believe is responsible for verifying the behavior discussed above. That made me discover an issue with the test, which I discuss here.

The changes in this commit arrange TestConn_Close to execute as
intended by closing the connection after sending the request.
t.Errorf("got error %v, want %v", err, jsonrpc2.ErrClosed)
}
close(done)
}()
if err := c.Close(); err != nil && err != jsonrpc2.ErrClosed {
Copy link
Contributor Author

@samherrmann samherrmann Feb 7, 2023

Choose a reason for hiding this comment

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

I discovered that this test did not run as intended. It is my understanding that the intention of this test is to ensure that pending calls receive ErrClosed when the connection is closed. What was happening here though is that Close() was being called before Call() even had a chance to execute. The test passed because the results are the same, but the condition set up by the test is different.

Copy link
Member

Choose a reason for hiding this comment

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

nice! Now this all LGTM. Thanks for the deep diving :)

@keegancsmith keegancsmith merged commit 028a50b into sourcegraph:master Feb 7, 2023
@samherrmann
Copy link
Contributor Author

@keegancsmith, thank you very much for the quick turnaround time on the review 🙂

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