Skip to content

Commit 3e8da47

Browse files
Bryan C. Millsgopherbot
Bryan C. Mills
authored andcommitted
internal/jsonrpc2_v2: initiate shutdown when the Writer breaks
Prior to this CL we already shut down a jsonrpc2_v2.Conn when its Reader breaks, which we expect to be the common shutdown path. However, with certain kinds of connections (notably those over stdin+stdout), it is possible for the Writer side to fail while the Reader remains working. If the Writer has failed, we have no way to return the required Response messages for incoming calls, nor to write new Request messages of our own. Since we have no way to return a response, we will now mark those incoming calls as canceled. However, even if the Writer has failed we may still be able to read the responses for any outgoing calls that are already in flight. When our in-flight calls complete, we could in theory even continue to process Notification messages from the Reader; however, those are unlikely to be useful with half the connection broken. It seems more helpful — and less surprising — to go ahead and shut down the connection completely when it becomes idle. Updates golang/go#46520. Updates golang/go#49387. Change-Id: I713f172ca7031f4211da321560fe7eae57960a48 Reviewed-on: https://go-review.googlesource.com/c/tools/+/446315 TryBot-Result: Gopher Robot <[email protected]> Reviewed-by: Alan Donovan <[email protected]> Auto-Submit: Bryan Mills <[email protected]> Run-TryBot: Bryan Mills <[email protected]>
1 parent 3566f69 commit 3e8da47

File tree

1 file changed

+94
-35
lines changed

1 file changed

+94
-35
lines changed

internal/jsonrpc2_v2/conn.go

Lines changed: 94 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -82,10 +82,12 @@ type Connection struct {
8282
// inFlightState records the state of the incoming and outgoing calls on a
8383
// Connection.
8484
type inFlightState struct {
85-
closing bool // disallow enqueuing further requests, and close the Closer when transitioning to idle
86-
readErr error
85+
closing bool // disallow enqueuing further requests, and close the Closer when transitioning to idle
86+
readErr error
87+
writeErr error
8788

88-
outgoing map[ID]*AsyncCall // calls only
89+
outgoingCalls map[ID]*AsyncCall // calls only
90+
outgoingNotifications int // # of notifications awaiting "write"
8991

9092
// incoming stores the total number of incoming calls and notifications
9193
// that have not yet written or processed a result.
@@ -104,7 +106,7 @@ type inFlightState struct {
104106

105107
// updateInFlight locks the state of the connection's in-flight requests, allows
106108
// f to mutate that state, and closes the connection if it is idle and either
107-
// is closing or has a read error.
109+
// is closing or has a read or write error.
108110
func (c *Connection) updateInFlight(f func(*inFlightState)) {
109111
c.stateMu.Lock()
110112
defer c.stateMu.Unlock()
@@ -113,8 +115,8 @@ func (c *Connection) updateInFlight(f func(*inFlightState)) {
113115

114116
f(s)
115117

116-
idle := s.incoming == 0 && len(s.outgoing) == 0 && !s.handlerRunning
117-
if idle && (s.closing || s.readErr != nil) && !s.closed {
118+
idle := len(s.outgoingCalls) == 0 && s.outgoingNotifications == 0 && s.incoming == 0 && !s.handlerRunning
119+
if idle && (s.closing || s.readErr != nil || s.writeErr != nil) && !s.closed {
118120
c.closeErr <- c.closer.Close()
119121
if c.onDone != nil {
120122
c.onDone()
@@ -181,20 +183,42 @@ func newConnection(bindCtx context.Context, rwc io.ReadWriteCloser, binder Binde
181183
// Notify invokes the target method but does not wait for a response.
182184
// The params will be marshaled to JSON before sending over the wire, and will
183185
// be handed to the method invoked.
184-
func (c *Connection) Notify(ctx context.Context, method string, params interface{}) error {
185-
notify, err := NewNotification(method, params)
186-
if err != nil {
187-
return fmt.Errorf("marshaling notify parameters: %v", err)
188-
}
186+
func (c *Connection) Notify(ctx context.Context, method string, params interface{}) (err error) {
189187
ctx, done := event.Start(ctx, method,
190188
tag.Method.Of(method),
191189
tag.RPCDirection.Of(tag.Outbound),
192190
)
191+
attempted := false
192+
193+
defer func() {
194+
labelStatus(ctx, err)
195+
done()
196+
if attempted {
197+
c.updateInFlight(func(s *inFlightState) {
198+
s.outgoingNotifications--
199+
})
200+
}
201+
}()
202+
203+
c.updateInFlight(func(s *inFlightState) {
204+
if s.writeErr != nil {
205+
err = fmt.Errorf("%w: %v", ErrClientClosing, s.writeErr)
206+
return
207+
}
208+
s.outgoingNotifications++
209+
attempted = true
210+
})
211+
if err != nil {
212+
return err
213+
}
214+
215+
notify, err := NewNotification(method, params)
216+
if err != nil {
217+
return fmt.Errorf("marshaling notify parameters: %v", err)
218+
}
219+
193220
event.Metric(ctx, tag.Started.Of(1))
194-
err = c.write(ctx, notify)
195-
labelStatus(ctx, err)
196-
done()
197-
return err
221+
return c.write(ctx, notify)
198222
}
199223

200224
// Call invokes the target method and returns an object that can be used to await the response.
@@ -239,10 +263,18 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{}
239263
err = fmt.Errorf("%w: %v", ErrClientClosing, s.readErr)
240264
return
241265
}
242-
if s.outgoing == nil {
243-
s.outgoing = make(map[ID]*AsyncCall)
266+
if s.writeErr != nil {
267+
// Don't start the call if the write end has failed, either.
268+
// We have reason to believe that the write would not succeed,
269+
// and if we avoid adding in-flight calls then eventually
270+
// the connection will go idle and be closed.
271+
err = fmt.Errorf("%w: %v", ErrClientClosing, s.writeErr)
272+
return
273+
}
274+
if s.outgoingCalls == nil {
275+
s.outgoingCalls = make(map[ID]*AsyncCall)
244276
}
245-
s.outgoing[ac.id] = ac
277+
s.outgoingCalls[ac.id] = ac
246278
})
247279
if err != nil {
248280
ac.retire(&Response{ID: id, Error: err})
@@ -254,8 +286,8 @@ func (c *Connection) Call(ctx context.Context, method string, params interface{}
254286
// Sending failed. We will never get a response, so deliver a fake one if it
255287
// wasn't already retired by the connection breaking.
256288
c.updateInFlight(func(s *inFlightState) {
257-
if s.outgoing[ac.id] == ac {
258-
delete(s.outgoing, ac.id)
289+
if s.outgoingCalls[ac.id] == ac {
290+
delete(s.outgoingCalls, ac.id)
259291
ac.retire(&Response{ID: id, Error: err})
260292
} else {
261293
// ac was already retired by the readIncoming goroutine:
@@ -405,8 +437,8 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter
405437

406438
case *Response:
407439
c.updateInFlight(func(s *inFlightState) {
408-
if ac, ok := s.outgoing[msg.ID]; ok {
409-
delete(s.outgoing, msg.ID)
440+
if ac, ok := s.outgoingCalls[msg.ID]; ok {
441+
delete(s.outgoingCalls, msg.ID)
410442
ac.retire(msg)
411443
} else {
412444
// TODO: How should we report unexpected responses?
@@ -423,10 +455,10 @@ func (c *Connection) readIncoming(ctx context.Context, reader Reader, preempter
423455

424456
// Retire any outgoing requests that were still in flight: with the Reader no
425457
// longer being processed, they necessarily cannot receive a response.
426-
for id, ac := range s.outgoing {
458+
for id, ac := range s.outgoingCalls {
427459
ac.retire(&Response{ID: id, Error: err})
428460
}
429-
s.outgoing = nil
461+
s.outgoingCalls = nil
430462
})
431463
}
432464

@@ -482,6 +514,14 @@ func (c *Connection) acceptRequest(ctx context.Context, msg *Request, msgBytes i
482514
err = ErrServerClosing
483515
return
484516
}
517+
518+
if s.writeErr != nil {
519+
// The write side of the connection appears to be broken,
520+
// so we won't be able to write a response to this request.
521+
// Avoid unnecessary work to compute it.
522+
err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr)
523+
return
524+
}
485525
}
486526
})
487527
if err != nil {
@@ -557,12 +597,19 @@ func (c *Connection) handleAsync() {
557597
return
558598
}
559599

560-
var result interface{}
561-
err := req.ctx.Err()
562-
if err == nil {
563-
// Only deliver to the Handler if not already cancelled.
564-
result, err = c.handler.Handle(req.ctx, req.Request)
600+
// Only deliver to the Handler if not already canceled.
601+
if err := req.ctx.Err(); err != nil {
602+
c.updateInFlight(func(s *inFlightState) {
603+
if s.writeErr != nil {
604+
// Assume that req.ctx was canceled due to s.writeErr.
605+
// TODO(#51365): use a Context API to plumb this through req.ctx.
606+
err = fmt.Errorf("%w: %v", ErrServerClosing, s.writeErr)
607+
}
608+
})
609+
c.processResult("handleAsync", req, nil, err)
565610
}
611+
612+
result, err := c.handler.Handle(req.ctx, req.Request)
566613
c.processResult(c.handler, req, result, err)
567614
}
568615
}
@@ -646,12 +693,24 @@ func (c *Connection) write(ctx context.Context, msg Message) error {
646693
n, err := writer.Write(ctx, msg)
647694
event.Metric(ctx, tag.SentBytes.Of(n))
648695

649-
// TODO: if err != nil, that suggests that future writes will not succeed,
650-
// so we cannot possibly write the results of incoming Call requests.
651-
// If the read side of the connection is also broken, we also might not have
652-
// a way to receive cancellation notifications.
653-
//
654-
// Should we cancel the pending calls implicitly?
696+
if err != nil && ctx.Err() == nil {
697+
// The call to Write failed, and since ctx.Err() is nil we can't attribute
698+
// the failure (even indirectly) to Context cancellation. The writer appears
699+
// to be broken, and future writes are likely to also fail.
700+
//
701+
// If the read side of the connection is also broken, we might not even be
702+
// able to receive cancellation notifications. Since we can't reliably write
703+
// the results of incoming calls and can't receive explicit cancellations,
704+
// cancel the calls now.
705+
c.updateInFlight(func(s *inFlightState) {
706+
if s.writeErr == nil {
707+
s.writeErr = err
708+
for _, r := range s.incomingByID {
709+
r.cancel()
710+
}
711+
}
712+
})
713+
}
655714

656715
return err
657716
}

0 commit comments

Comments
 (0)