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
Expand stream's flow control in case of an active read. #1248
Changes from 7 commits
9012be0
c2d5ec9
4d09482
722d058
c9d1fc8
78f4eb0
da22a03
9d8f07c
04b882f
d4d90a8
1df7a2a
e85309f
a96ed0a
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 |
---|---|---|
|
@@ -445,6 +445,7 @@ type test struct { | |
streamServerInt grpc.StreamServerInterceptor | ||
unknownHandler grpc.StreamHandler | ||
sc <-chan grpc.ServiceConfig | ||
customCodec grpc.Codec | ||
serverInitialWindowSize int32 | ||
serverInitialConnWindowSize int32 | ||
clientInitialWindowSize int32 | ||
|
@@ -545,6 +546,9 @@ func (te *test) startServer(ts testpb.TestServiceServer) { | |
case "clientTimeoutCreds": | ||
sopts = append(sopts, grpc.Creds(&clientTimeoutCreds{})) | ||
} | ||
if te.customCodec != nil { | ||
sopts = append(sopts, grpc.CustomCodec(te.customCodec)) | ||
} | ||
s := grpc.NewServer(sopts...) | ||
te.srv = s | ||
if te.e.httpHandler { | ||
|
@@ -625,6 +629,9 @@ func (te *test) clientConn() *grpc.ClientConn { | |
if te.perRPCCreds != nil { | ||
opts = append(opts, grpc.WithPerRPCCredentials(te.perRPCCreds)) | ||
} | ||
if te.customCodec != nil { | ||
opts = append(opts, grpc.WithCodec(te.customCodec)) | ||
} | ||
var err error | ||
te.cc, err = grpc.Dial(te.srvAddr, opts...) | ||
if err != nil { | ||
|
@@ -2634,26 +2641,51 @@ func testServerStreamingConcurrent(t *testing.T, e env) { | |
|
||
} | ||
|
||
func generatePayloadSizes() [][]int { | ||
reqSizes := [][]int{ | ||
{27182, 8, 1828, 45904}, | ||
} | ||
|
||
num8KPayloads := 1024 | ||
eightKPayloads := []int{} | ||
for i := 0; i < num8KPayloads; i++ { | ||
eightKPayloads = append(eightKPayloads, (1 << 13)) | ||
} | ||
reqSizes = append(reqSizes, eightKPayloads) | ||
|
||
num2MPayloads := 8 | ||
twoMPayloads := []int{} | ||
for i := 0; i < num2MPayloads; i++ { | ||
twoMPayloads = append(twoMPayloads, (1 << 21)) | ||
} | ||
reqSizes = append(reqSizes, twoMPayloads) | ||
|
||
return reqSizes | ||
} | ||
|
||
func TestClientStreaming(t *testing.T) { | ||
defer leakCheck(t)() | ||
for _, e := range listTestEnv() { | ||
testClientStreaming(t, e) | ||
for _, s := range generatePayloadSizes() { | ||
for _, e := range listTestEnv() { | ||
testClientStreaming(t, e, s) | ||
} | ||
} | ||
} | ||
|
||
func testClientStreaming(t *testing.T, e env) { | ||
func testClientStreaming(t *testing.T, e env, sizes []int) { | ||
te := newTest(t, e) | ||
te.startServer(&testServer{security: e.security}) | ||
defer te.tearDown() | ||
tc := testpb.NewTestServiceClient(te.clientConn()) | ||
|
||
stream, err := tc.StreamingInputCall(te.ctx) | ||
ctx, _ := context.WithTimeout(te.ctx, time.Minute*3) | ||
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. Nit: 30s |
||
stream, err := tc.StreamingInputCall(ctx) | ||
if err != nil { | ||
t.Fatalf("%v.StreamingInputCall(_) = _, %v, want <nil>", tc, err) | ||
} | ||
|
||
var sum int | ||
for _, s := range reqSizes { | ||
for _, s := range sizes { | ||
payload, err := newPayload(testpb.PayloadType_COMPRESSABLE, int32(s)) | ||
if err != nil { | ||
t.Fatal(err) | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -58,6 +58,8 @@ const ( | |
defaultServerKeepaliveTime = time.Duration(2 * time.Hour) | ||
defaultServerKeepaliveTimeout = time.Duration(20 * time.Second) | ||
defaultKeepalivePolicyMinTime = time.Duration(5 * time.Minute) | ||
// max window limit set by HTTP2 Specs. | ||
maxWindowSize = math.MaxInt32 | ||
) | ||
|
||
// The following defines various control items which could flow through | ||
|
@@ -167,14 +169,37 @@ type inFlow struct { | |
// The amount of data the application has consumed but grpc has not sent | ||
// window update for them. Used to reduce window update frequency. | ||
pendingUpdate uint32 | ||
// delta is the extra window update given by receiver when an application | ||
// is reading data bigger in size than the inFlow limit. | ||
delta int32 | ||
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. Should this be unsigned since it can't be negative? (Also, you cast it to uint32 when reading and cast to int32 when assigning.) |
||
} | ||
|
||
func (f *inFlow) maybeAdjust(n uint32) uint32 { | ||
if n > uint32(math.MaxInt32) { | ||
n = uint32(math.MaxInt32) | ||
} | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
senderQuota := int32(f.limit - (f.pendingData + f.pendingUpdate)) | ||
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. small wording nit: rename to |
||
untransmittedData := int32(n - f.pendingData) // Casting into int32 since it could be negative. | ||
if untransmittedData > senderQuota { | ||
// Sender's window shouldn't go more than 2^31 - 1. | ||
if f.limit+n > uint32(maxWindowSize) { | ||
f.delta = maxWindowSize - int32(f.limit) | ||
} else { | ||
f.delta = int32(n) | ||
} | ||
return uint32(f.delta) | ||
} | ||
return 0 | ||
} | ||
|
||
// onData is invoked when some data frame is received. It updates pendingData. | ||
func (f *inFlow) onData(n uint32) error { | ||
f.mu.Lock() | ||
defer f.mu.Unlock() | ||
f.pendingData += n | ||
if f.pendingData+f.pendingUpdate > f.limit { | ||
if f.pendingData+f.pendingUpdate > f.limit+uint32(f.delta) { | ||
return fmt.Errorf("received %d-bytes data exceeding the limit %d bytes", f.pendingData+f.pendingUpdate, f.limit) | ||
} | ||
return nil | ||
|
@@ -189,6 +214,14 @@ func (f *inFlow) onRead(n uint32) uint32 { | |
return 0 | ||
} | ||
f.pendingData -= n | ||
if f.delta > 0 { | ||
f.delta -= int32(n) | ||
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 would be easier to read without the casts and taking the negative of a negative number: if n > f.delta {
n -= f.delta
f.delta = 0
} else {
f.delta -= n
n = 0
} (Also would allow f.delta to be uint32 like it seems like it wants to be.) |
||
n = 0 | ||
if f.delta < 0 { | ||
n = uint32(-f.delta) | ||
f.delta = 0 | ||
} | ||
} | ||
f.pendingUpdate += n | ||
if f.pendingUpdate >= f.limit/4 { | ||
wu := f.pendingUpdate | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -173,9 +173,9 @@ func newHTTP2Client(ctx context.Context, addr TargetInfo, opts ConnectOptions) ( | |
conn, err := dial(ctx, opts.Dialer, addr.Addr) | ||
if err != nil { | ||
if opts.FailOnNonTempDialError { | ||
return nil, connectionErrorf(isTemporary(err), err, "transport: %v", err) | ||
return nil, connectionErrorf(isTemporary(err), err, "transport: Error while dialing %v", err) | ||
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. Nit: lowercase "e" in "error", and add a colon before the error being appended. |
||
} | ||
return nil, connectionErrorf(true, err, "transport: %v", err) | ||
return nil, connectionErrorf(true, err, "transport: Error while dialing %v", err) | ||
} | ||
// Any further errors will close the underlying connection | ||
defer func(conn net.Conn) { | ||
|
@@ -194,7 +194,7 @@ func newHTTP2Client(ctx context.Context, addr TargetInfo, opts ConnectOptions) ( | |
// Credentials handshake errors are typically considered permanent | ||
// to avoid retrying on e.g. bad certificates. | ||
temp := isTemporary(err) | ||
return nil, connectionErrorf(temp, err, "transport: %v", err) | ||
return nil, connectionErrorf(temp, err, "transport: authentication handshake failed %v", err) | ||
} | ||
isSecure = true | ||
} | ||
|
@@ -269,7 +269,7 @@ func newHTTP2Client(ctx context.Context, addr TargetInfo, opts ConnectOptions) ( | |
n, err := t.conn.Write(clientPreface) | ||
if err != nil { | ||
t.Close() | ||
return nil, connectionErrorf(true, err, "transport: %v", err) | ||
return nil, connectionErrorf(true, err, "transport: failed to write client preface %v", err) | ||
} | ||
if n != len(clientPreface) { | ||
t.Close() | ||
|
@@ -285,13 +285,13 @@ func newHTTP2Client(ctx context.Context, addr TargetInfo, opts ConnectOptions) ( | |
} | ||
if err != nil { | ||
t.Close() | ||
return nil, connectionErrorf(true, err, "transport: %v", err) | ||
return nil, connectionErrorf(true, err, "transport: failed to write initial settings frame %v", err) | ||
} | ||
// Adjust the connection flow control window if needed. | ||
if delta := uint32(icwz - defaultWindowSize); delta > 0 { | ||
if err := t.framer.writeWindowUpdate(true, 0, delta); err != nil { | ||
t.Close() | ||
return nil, connectionErrorf(true, err, "transport: %v", err) | ||
return nil, connectionErrorf(true, err, "transport: failed to write window update %v", err) | ||
} | ||
} | ||
go t.controller() | ||
|
@@ -316,18 +316,24 @@ func (t *http2Client) newStream(ctx context.Context, callHdr *CallHdr) *Stream { | |
headerChan: make(chan struct{}), | ||
} | ||
t.nextID += 2 | ||
s.windowHandler = func(n int) { | ||
t.updateWindow(s, uint32(n)) | ||
s.requestRead = func(n int) { | ||
t.adjustWindow(s, uint32(n)) | ||
} | ||
// The client side stream context should have exactly the same life cycle with the user provided context. | ||
// That means, s.ctx should be read-only. And s.ctx is done iff ctx is done. | ||
// So we use the original context here instead of creating a copy. | ||
s.ctx = ctx | ||
s.dec = &recvBufferReader{ | ||
ctx: s.ctx, | ||
goAway: s.goAway, | ||
recv: s.buf, | ||
s.trReader = &transportReader{ | ||
reader: &recvBufferReader{ | ||
ctx: s.ctx, | ||
goAway: s.goAway, | ||
recv: s.buf, | ||
}, | ||
windowHandler: func(n int) { | ||
t.updateWindow(s, uint32(n)) | ||
}, | ||
} | ||
|
||
return s | ||
} | ||
|
||
|
@@ -802,6 +808,20 @@ func (t *http2Client) getStream(f http2.Frame) (*Stream, bool) { | |
return s, ok | ||
} | ||
|
||
// adjustWindow sends out extra window update over the initial window size | ||
// of stream if the application is requesting data larger in size than | ||
// the window. | ||
func (t *http2Client) adjustWindow(s *Stream, n uint32) { | ||
s.mu.Lock() | ||
defer s.mu.Unlock() | ||
if s.state == streamDone { | ||
return | ||
} | ||
if w := s.fc.maybeAdjust(n); n > 0 { | ||
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. should the If not, check |
||
t.controlBuf.put(&windowUpdate{s.id, w}) | ||
} | ||
} | ||
|
||
// updateWindow adjusts the inbound quota for the stream and the transport. | ||
// Window updates will deliver to the controller for sending when | ||
// the cumulative quota exceeds the corresponding threshold. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -274,10 +274,14 @@ func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func( | |
if len(state.mdata) > 0 { | ||
s.ctx = metadata.NewIncomingContext(s.ctx, state.mdata) | ||
} | ||
|
||
s.dec = &recvBufferReader{ | ||
ctx: s.ctx, | ||
recv: s.buf, | ||
s.trReader = &transportReader{ | ||
reader: &recvBufferReader{ | ||
ctx: s.ctx, | ||
recv: s.buf, | ||
}, | ||
windowHandler: func(n int) { | ||
t.updateWindow(s, uint32(n)) | ||
}, | ||
} | ||
s.recvCompress = state.encoding | ||
s.method = state.method | ||
|
@@ -316,8 +320,8 @@ func (t *http2Server) operateHeaders(frame *http2.MetaHeadersFrame, handle func( | |
t.idle = time.Time{} | ||
} | ||
t.mu.Unlock() | ||
s.windowHandler = func(n int) { | ||
t.updateWindow(s, uint32(n)) | ||
s.requestRead = func(n int) { | ||
t.adjustWindow(s, uint32(n)) | ||
} | ||
s.ctx = traceCtx(s.ctx, s.method) | ||
if t.stats != nil { | ||
|
@@ -358,7 +362,7 @@ func (t *http2Server) HandleStreams(handle func(*Stream), traceCtx func(context. | |
return | ||
} | ||
if err != nil { | ||
grpclog.Printf("transport: http2Server.HandleStreams failed to read frame: %v", err) | ||
grpclog.Printf("transport: http2Server.HandleStreams failed to read initial settings frame: %v", err) | ||
t.Close() | ||
return | ||
} | ||
|
@@ -432,6 +436,20 @@ func (t *http2Server) getStream(f http2.Frame) (*Stream, bool) { | |
return s, true | ||
} | ||
|
||
// adjustWindow sends out extra window update over the initial window size | ||
// of stream if the application is requesting data larger in size than | ||
// the window. | ||
func (t *http2Server) adjustWindow(s *Stream, n uint32) { | ||
s.mu.Lock() | ||
defer s.mu.Unlock() | ||
if s.state == streamDone { | ||
return | ||
} | ||
if w := s.fc.maybeAdjust(n); n > 0 { | ||
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. Same as above re: Can this function be shared somehow? Should it be a method on the Stream instead of server/client? |
||
t.controlBuf.put(&windowUpdate{s.id, w}) | ||
} | ||
} | ||
|
||
// updateWindow adjusts the inbound quota for the stream and the transport. | ||
// Window updates will deliver to the controller for sending when | ||
// the cumulative quota exceeds the corresponding threshold. | ||
|
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.
nit: I think the use of
fullReader
and other changes to this file can be reverted, since the transport is still anio.Reader
.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.
These test here rely on recvMsg's behavior of reading the full message, (which is true when it interacts with the transport stream). However, here the parser is given a "fake" buffer instead of the stream. Given that we changed recvMsg to go from io.ReadFull to p.r.Read, the "fake" buffer here needs to read the full message just like the transport stream does.