Skip to content

Commit 7a4c6c4

Browse files
committed
context cancellation callbacks for unary and streaming
1 parent d2c52f0 commit 7a4c6c4

2 files changed

Lines changed: 159 additions & 35 deletions

File tree

stream.go

Lines changed: 63 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -514,24 +514,25 @@ func newClientStreamWithParams(ctx context.Context, desc *StreamDesc, cc *Client
514514
}
515515
}
516516

517-
if desc != unaryStreamDesc {
518-
// Listen on cc and stream contexts to cleanup when the user closes the
519-
// ClientConn or cancels the stream context. In all other cases, an error
520-
// should already be injected into the recv buffer by the transport, which
521-
// the client will eventually receive, and then we will cancel the stream's
522-
// context in clientStream.finish.
523-
go func() {
524-
select {
525-
case <-cc.ctx.Done():
526-
cs.finish(ErrClientConnClosing)
527-
case <-ctx.Done():
528-
cs.finish(toRPCErr(ctx.Err()))
529-
}
530-
}()
531-
}
517+
cs.registerContextCallbacks()
532518
return cs, nil
533519
}
534520

521+
// registerContextCallbacks registers callbacks to clean up the stream when the
522+
// RPC context expires or the ClientConn is closed.
523+
func (cs *clientStream) registerContextCallbacks() {
524+
cs.mu.Lock()
525+
defer cs.mu.Unlock()
526+
if !cs.finished {
527+
cs.stopCtx = context.AfterFunc(cs.ctx, func() {
528+
cs.finish(toRPCErr(cs.ctx.Err()))
529+
})
530+
cs.stopCC = context.AfterFunc(cs.cc.ctx, func() {
531+
cs.finish(ErrClientConnClosing)
532+
})
533+
}
534+
}
535+
535536
// newAttemptLocked creates a new csAttempt without a transport or stream.
536537
func (cs *clientStream) newAttemptLocked(isTransparent bool) (*csAttempt, error) {
537538
if err := cs.ctx.Err(); err != nil {
@@ -713,6 +714,9 @@ type clientStream struct {
713714
replayBuffer []replayOp // operations to replay on retry
714715
replayBufferSize int // current size of replayBuffer
715716

717+
stopCtx func() bool // stops the callback registered to run on RPC context expiration
718+
stopCC func() bool // stops the callback registered to run on ClientConn context expiration
719+
716720
// Bool fields are grouped at the tail to eliminate the alignment padding
717721
// that would otherwise follow each bool when the next field is pointer- or
718722
// int-sized. See https://github.com/grpc/grpc-go/issues/9280 for benchmarks.
@@ -1195,6 +1199,10 @@ func (cs *clientStream) finish(err error) {
11951199
return
11961200
}
11971201
cs.finished = true
1202+
stopCtx := cs.stopCtx
1203+
stopCC := cs.stopCC
1204+
cs.stopCtx = nil
1205+
cs.stopCC = nil
11981206
cs.commitAttemptLocked()
11991207
attemptCreated := cs.attempt != nil
12001208
if attemptCreated {
@@ -1208,6 +1216,12 @@ func (cs *clientStream) finish(err error) {
12081216
}
12091217

12101218
cs.mu.Unlock()
1219+
if stopCtx != nil {
1220+
stopCtx()
1221+
}
1222+
if stopCC != nil {
1223+
stopCC()
1224+
}
12111225
// Only one of cancel or trailer needs to be logged.
12121226
if len(cs.binlogs) != 0 {
12131227
switch err {
@@ -1472,29 +1486,28 @@ func newNonRetryClientStream(ctx context.Context, desc *StreamDesc, method strin
14721486
as.transportStream = s
14731487
as.parser = parser{r: s, bufferPool: ac.dopts.copts.BufferPool}
14741488
ac.incrCallsStarted()
1475-
if desc != unaryStreamDesc {
1476-
// Listen on stream context to cleanup when the stream context is
1477-
// canceled. Also listen for the addrConn's context in case the
1478-
// addrConn is closed or reconnects to a different address. In all
1479-
// other cases, an error should already be injected into the recv
1480-
// buffer by the transport, which the client will eventually receive,
1481-
// and then we will cancel the stream's context in
1482-
// addrConnStream.finish.
1483-
go func() {
1484-
ac.mu.Lock()
1485-
acCtx := ac.ctx
1486-
ac.mu.Unlock()
1487-
select {
1488-
case <-acCtx.Done():
1489-
as.finish(status.Error(codes.Canceled, "grpc: the SubConn is closing"))
1490-
case <-ctx.Done():
1491-
as.finish(toRPCErr(ctx.Err()))
1492-
}
1493-
}()
1494-
}
1489+
as.registerContextCallbacks()
14951490
return &clientStreamWrapper{ClientStream: as, desc: desc}, nil
14961491
}
14971492

1493+
// registerContextCallbacks registers callbacks to clean up the stream when the
1494+
// RPC context expires or the SubConn is closed.
1495+
func (as *addrConnStream) registerContextCallbacks() {
1496+
as.mu.Lock()
1497+
defer as.mu.Unlock()
1498+
if !as.finished {
1499+
as.stopCtx = context.AfterFunc(as.ctx, func() {
1500+
as.finish(toRPCErr(as.ctx.Err()))
1501+
})
1502+
as.ac.mu.Lock()
1503+
acCtx := as.ac.ctx
1504+
as.ac.mu.Unlock()
1505+
as.stopCC = context.AfterFunc(acCtx, func() {
1506+
as.finish(status.Error(codes.Canceled, "grpc: the SubConn is closing"))
1507+
})
1508+
}
1509+
}
1510+
14981511
type addrConnStream struct {
14991512
transportStream *transport.ClientStream
15001513
ac *addrConn
@@ -1515,6 +1528,9 @@ type addrConnStream struct {
15151528
mu sync.Mutex
15161529
parser parser
15171530

1531+
stopCtx func() bool // stops the callback registered to run on RPC context expiration
1532+
stopCC func() bool // stops the callback registered to run on SubConn context expiration
1533+
15181534
// Bool fields are grouped at the tail to eliminate the alignment padding
15191535
// that would otherwise follow each bool when the next field is pointer- or
15201536
// int-sized. See https://github.com/grpc/grpc-go/issues/9348 for benchmarks.
@@ -1656,6 +1672,11 @@ func (as *addrConnStream) finish(err error) {
16561672
return
16571673
}
16581674
as.finished = true
1675+
stopCtx := as.stopCtx
1676+
stopCC := as.stopCC
1677+
as.stopCtx = nil
1678+
as.stopCC = nil
1679+
16591680
if err == io.EOF {
16601681
// Ending a stream with EOF indicates a success.
16611682
err = nil
@@ -1671,6 +1692,13 @@ func (as *addrConnStream) finish(err error) {
16711692
}
16721693
as.cancel()
16731694
as.mu.Unlock()
1695+
1696+
if stopCtx != nil {
1697+
stopCtx()
1698+
}
1699+
if stopCC != nil {
1700+
stopCC()
1701+
}
16741702
}
16751703

16761704
// ServerStream defines the server-side behavior of a streaming RPC.

test/context_canceled_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,3 +162,99 @@ func (s) TestCancelWhileRecvingWithCompression(t *testing.T) {
162162
t.Fatalf("Close failed with %v, want nil", err)
163163
}
164164
}
165+
166+
// Test verifies that an in-flight Unary RPC fails promptly when the ClientConn
167+
// is closed (canceling ClientConn context).
168+
func (s) TestUnary_ClientConnContextExpires(t *testing.T) {
169+
rpcStarted := make(chan struct{})
170+
ss := &stubserver.StubServer{
171+
EmptyCallF: func(ctx context.Context, _ *testpb.Empty) (*testpb.Empty, error) {
172+
close(rpcStarted)
173+
<-ctx.Done()
174+
return nil, ctx.Err()
175+
},
176+
}
177+
if err := ss.Start(nil); err != nil {
178+
t.Fatalf("Error starting endpoint server: %v", err)
179+
}
180+
defer ss.Stop()
181+
182+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
183+
defer cancel()
184+
errChan := make(chan error, 1)
185+
go func() {
186+
_, err := ss.Client.EmptyCall(ctx, &testpb.Empty{})
187+
errChan <- err
188+
}()
189+
190+
select {
191+
case <-rpcStarted:
192+
case <-time.After(defaultTestTimeout):
193+
t.Fatal("timed out waiting for RPC to start on server")
194+
}
195+
196+
// Close ClientConn while Unary RPC is in-flight.
197+
ss.CC.Close()
198+
199+
select {
200+
case err := <-errChan:
201+
if status.Code(err) != codes.Canceled {
202+
t.Fatalf("EmptyCall returned error: %v (code: %v), want Canceled", err, status.Code(err))
203+
}
204+
case <-time.After(2 * time.Second):
205+
t.Fatal("Unary RPC did not return promptly after ClientConn was closed")
206+
}
207+
}
208+
209+
// Test verifies that an in-flight Streaming RPC fails promptly when the
210+
// ClientConn is closed (canceling ClientConn context).
211+
func (s) TestStreaming_ClientConnContextExpires(t *testing.T) {
212+
rpcStarted := make(chan struct{})
213+
ss := &stubserver.StubServer{
214+
FullDuplexCallF: func(stream testgrpc.TestService_FullDuplexCallServer) error {
215+
close(rpcStarted)
216+
<-stream.Context().Done()
217+
return stream.Context().Err()
218+
},
219+
}
220+
if err := ss.Start(nil); err != nil {
221+
t.Fatalf("Error starting endpoint server: %v", err)
222+
}
223+
defer ss.Stop()
224+
225+
ctx, cancel := context.WithTimeout(context.Background(), defaultTestTimeout)
226+
defer cancel()
227+
stream, err := ss.Client.FullDuplexCall(ctx)
228+
if err != nil {
229+
t.Fatalf("FullDuplexCall failed: %v", err)
230+
}
231+
232+
// Send an initial message so the stream is active on the server.
233+
if err := stream.Send(&testpb.StreamingOutputCallRequest{}); err != nil {
234+
t.Fatalf("Send failed: %v", err)
235+
}
236+
237+
select {
238+
case <-rpcStarted:
239+
case <-time.After(defaultTestTimeout):
240+
t.Fatal("timed out waiting for stream to start on server")
241+
}
242+
243+
errChan := make(chan error, 1)
244+
go func() {
245+
_, err := stream.Recv()
246+
errChan <- err
247+
}()
248+
249+
// Close ClientConn while Streaming RPC is waiting in Recv.
250+
ss.CC.Close()
251+
252+
select {
253+
case err := <-errChan:
254+
if status.Code(err) != codes.Canceled {
255+
t.Fatalf("Recv returned error: %v (code: %v), want Canceled", err, status.Code(err))
256+
}
257+
case <-time.After(2 * time.Second):
258+
t.Fatal("Streaming RPC did not return promptly after ClientConn was closed")
259+
}
260+
}

0 commit comments

Comments
 (0)