mirror of
https://github.com/sourcegraph/jsonrpc2.git
synced 2026-08-16 10:08:12 +02:00
fix/conn: prevent re-entrant logger deadlock (#99)
Protocol errors invoke the configured logger while the connection is shutting down. Calling user code under the connection mutex meant a logger that sends over the same connection could never observe closure and instead deadlocked. Complete teardown before logging so re-entrant calls return ErrClosed. Amp-Thread-ID: https://ampcode.com/threads/T-019fb774-aef7-75df-8eb3-8888d6835754 Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
parent
28070556df
commit
c3088e5c5d
2 changed files with 43 additions and 6 deletions
17
conn.go
17
conn.go
|
|
@ -180,24 +180,29 @@ func (c *Conn) SendResponse(ctx context.Context, resp *Response) error {
|
||||||
|
|
||||||
func (c *Conn) close(cause error) error {
|
func (c *Conn) close(cause error) error {
|
||||||
c.mu.Lock()
|
c.mu.Lock()
|
||||||
defer c.mu.Unlock()
|
|
||||||
|
|
||||||
if c.closed {
|
if c.closed {
|
||||||
|
c.mu.Unlock()
|
||||||
return ErrClosed
|
return ErrClosed
|
||||||
}
|
}
|
||||||
|
c.closed = true
|
||||||
|
|
||||||
for _, call := range c.pending {
|
for _, call := range c.pending {
|
||||||
close(call.done)
|
close(call.done)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
close(c.disconnect)
|
||||||
|
c.mu.Unlock()
|
||||||
|
|
||||||
|
c.cancelCtx()
|
||||||
|
err := c.stream.Close()
|
||||||
|
|
||||||
|
// The logger may call back into c, so invoke it only after shutdown is
|
||||||
|
// complete and c.mu is unlocked.
|
||||||
if cause != nil && cause != io.EOF && cause != io.ErrUnexpectedEOF {
|
if cause != nil && cause != io.EOF && cause != io.ErrUnexpectedEOF {
|
||||||
c.logger.Printf("jsonrpc2: protocol error: %v\n", cause)
|
c.logger.Printf("jsonrpc2: protocol error: %v\n", cause)
|
||||||
}
|
}
|
||||||
|
|
||||||
close(c.disconnect)
|
return err
|
||||||
c.cancelCtx()
|
|
||||||
c.closed = true
|
|
||||||
return c.stream.Close()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Conn) readMessages(ctx context.Context) {
|
func (c *Conn) readMessages(ctx context.Context) {
|
||||||
|
|
|
||||||
32
conn_test.go
32
conn_test.go
|
|
@ -167,6 +167,38 @@ func TestConn_DisconnectNotify(t *testing.T) {
|
||||||
connA.Write([]byte("invalid json"))
|
connA.Write([]byte("invalid json"))
|
||||||
assertDisconnect(t, c, connB)
|
assertDisconnect(t, c, connB)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("protocol error logger uses connection", func(t *testing.T) {
|
||||||
|
connA, connB := net.Pipe()
|
||||||
|
logResult := make(chan error, 1)
|
||||||
|
var c *jsonrpc2.Conn
|
||||||
|
c = jsonrpc2.NewConn(
|
||||||
|
context.Background(),
|
||||||
|
jsonrpc2.NewPlainObjectStream(connB),
|
||||||
|
noopHandler{},
|
||||||
|
jsonrpc2.SetLogger(connNotifyLogger{conn: &c, result: logResult}),
|
||||||
|
)
|
||||||
|
connA.Write([]byte("invalid json"))
|
||||||
|
assertDisconnect(t, c, connB)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err := <-logResult:
|
||||||
|
if err != jsonrpc2.ErrClosed {
|
||||||
|
t.Errorf("logger Notify: got %v, want %v", err, jsonrpc2.ErrClosed)
|
||||||
|
}
|
||||||
|
case <-time.After(200 * time.Millisecond):
|
||||||
|
t.Error("logger blocked using connection")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type connNotifyLogger struct {
|
||||||
|
conn **jsonrpc2.Conn
|
||||||
|
result chan<- error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l connNotifyLogger) Printf(string, ...interface{}) {
|
||||||
|
l.result <- (*l.conn).Notify(context.Background(), "log", nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestConn_Close(t *testing.T) {
|
func TestConn_Close(t *testing.T) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue