ext_proc: Fix race in Test/FlowControl_PiggybackedWindowUpdates (#9578) Fixes : https://github.com/grpc/grpc-go/issues/9400 *Fix*: Capture `defaultWindowSize` once synchronously at stream initialization in `commonStream` and use `cs.defaultWindowSize` throughout the stream's lifetime. *Fix*: Call `stream.CloseSend()` before waiting on both `upstreamPiggybackedReceived` and `downstreamPiggybackedReceived`, and use `*grpcsync.Event` for signaling. RELEASE NOTES: None
diff --git a/internal/xds/httpfilter/extproc/ext_proc.go b/internal/xds/httpfilter/extproc/ext_proc.go index af7d3a2..6541460 100644 --- a/internal/xds/httpfilter/extproc/ext_proc.go +++ b/internal/xds/httpfilter/extproc/ext_proc.go
@@ -429,6 +429,7 @@ metricsRecorder: i.metricsRecorder, target: i.target, clientHeadersStartTime: timeNow(), + defaultWindowSize: iextproc.DefaultFlowControlWindowSize, // Construct request attributes once during stream initialization to capture // the original, unmutated request metadata and RPC info. For streaming // RPCs, where stream creation and message transmission are separable, @@ -500,8 +501,8 @@ downstreamToSidestreamPositiveUpdateCh: make(chan struct{}, 1), upstreamToSidestreamPositiveUpdateCh: make(chan struct{}, 1), } - cs.downstreamToSidestreamWindow.Store(iextproc.DefaultFlowControlWindowSize) - cs.upstreamToSidestreamWindow.Store(iextproc.DefaultFlowControlWindowSize) + cs.downstreamToSidestreamWindow.Store(cs.defaultWindowSize) + cs.upstreamToSidestreamWindow.Store(cs.defaultWindowSize) // When request header processing is "Send", defer creating the dataplane // stream until the external processor responds, because gRPC transmits @@ -559,6 +560,7 @@ metricsRecorder estats.MetricsRecorder target string clientHeadersStartTime time.Time // holds the start time of client headers processing for metrics and relative timestamp calculations + defaultWindowSize int64 // initial flow control window size captured at stream creation // onFinish stores OnFinishCallOption callbacks to execute if calling the // delegate (newStream) fails or if the RPC fails before delegating. @@ -645,10 +647,10 @@ } if !cs.config.observabilityMode { req.FlowControlInit = &v3procservicepb.ProcessingRequest_FlowControlInit{ - InitialWindowDownstreamToSidestream: iextproc.DefaultFlowControlWindowSize, - InitialWindowUpstreamToSidestream: iextproc.DefaultFlowControlWindowSize, - InitialWindowSidestreamToUpstream: iextproc.DefaultFlowControlWindowSize, - InitialWindowSidestreamToDownstream: iextproc.DefaultFlowControlWindowSize, + InitialWindowDownstreamToSidestream: cs.defaultWindowSize, + InitialWindowUpstreamToSidestream: cs.defaultWindowSize, + InitialWindowSidestreamToUpstream: cs.defaultWindowSize, + InitialWindowSidestreamToDownstream: cs.defaultWindowSize, } } } @@ -1176,7 +1178,7 @@ // standalone processing request with window update and clear the pending // update. if bodySize := int64(len(streamedResp.GetBody())); bodySize > 0 { - if cs.pendingRespWindowUpdate.Add(bodySize) >= iextproc.DefaultFlowControlWindowSize/2 { + if cs.pendingRespWindowUpdate.Add(bodySize) >= cs.defaultWindowSize/2 { if err := cs.sendClientWindowUpdate(); err != nil { return err } @@ -1456,7 +1458,7 @@ // standalone processing request with window update and clear the pending // update. if bodySize := int64(len(streamedResp.GetBody())); bodySize > 0 { - if cs.pendingReqWindowUpdate.Add(bodySize) >= iextproc.DefaultFlowControlWindowSize/2 { + if cs.pendingReqWindowUpdate.Add(bodySize) >= cs.defaultWindowSize/2 { if err := cs.sendClientWindowUpdate(); err != nil { return }
diff --git a/internal/xds/httpfilter/extproc/ext_proc_ext_test.go b/internal/xds/httpfilter/extproc/ext_proc_ext_test.go index 6a738c1..2186785 100644 --- a/internal/xds/httpfilter/extproc/ext_proc_ext_test.go +++ b/internal/xds/httpfilter/extproc/ext_proc_ext_test.go
@@ -6641,8 +6641,8 @@ const defaultWindowSize = 100 overrideDefaultFlowControlWindow(t, defaultWindowSize) - downstreamPiggybackedReceived := make(chan struct{}) - upstreamPiggybackedReceived := make(chan struct{}) + downstreamPiggybackedReceived := grpcsync.NewEvent() + upstreamPiggybackedReceived := grpcsync.NewEvent() mutatedResp := &testpb.StreamingOutputCallResponse{Payload: &testpb.Payload{Body: make([]byte, 15)}} mutatedRespBytes, err := proto.Marshal(mutatedResp) @@ -6671,10 +6671,10 @@ // Check for piggybacked window updates on ProcessingRequests. if update := req.GetClientWindowUpdate(); update != nil { if update.GetWindowIncrementSidestreamToDownstream() >= wantRespInc { - close(downstreamPiggybackedReceived) + downstreamPiggybackedReceived.Fire() } if update.GetWindowIncrementSidestreamToUpstream() >= wantReqInc { - close(upstreamPiggybackedReceived) + upstreamPiggybackedReceived.Fire() } } @@ -6751,36 +6751,36 @@ t.Fatalf("FullDuplexCall() failed: %v", err) } - // Send request 1 (mutated by extproc to 20k, forwarding to backend triggers - // upstream window update piggybacked on ResponseBody). + // Send request 1 (mutated by extproc to < defaultWindowSize/2, so its + // sidestream-to-upstream window increment stays pending for piggybacking). if err := stream.Send(&testpb.StreamingOutputCallRequest{ Payload: &testpb.Payload{Body: []byte("c1")}, }); err != nil { t.Fatalf("stream.Send() failed: %v", err) } - // Read response 1 (mutated by extproc to 15k, reading triggers downstream - // window update piggybacked on CloseSend). + // Read response 1 (mutated by extproc to < defaultWindowSize/2, so its + // sidestream-to-downstream window increment stays pending for piggybacking). if _, err := stream.Recv(); err != nil { t.Fatalf("stream.Recv() failed: %v", err) } - // Verify that upstream piggybacked window update is received. - select { - case <-upstreamPiggybackedReceived: - case <-time.After(defaultTestTimeout): - t.Fatalf("Timed out waiting for upstream piggybacked window update on processor stream") - } - - // CloseSend sends EndOfStreamWithoutMessage to proc server, piggybacking the - // downstream window update. + // CloseSend sends EndOfStreamWithoutMessage to proc server, piggybacking + // pending window updates (including upstream if ResponseBody was dispatched + // before requestForwardingToDataplaneLoop updated pendingReqWindowUpdate). if err := stream.CloseSend(); err != nil { t.Fatalf("stream.CloseSend() failed: %v", err) } - // Verify that downstream piggybacked window update is received. + // Verify that both upstream and downstream piggybacked window updates are + // received. select { - case <-downstreamPiggybackedReceived: + case <-upstreamPiggybackedReceived.Done(): + case <-time.After(defaultTestTimeout): + t.Fatalf("Timed out waiting for upstream piggybacked window update on processor stream") + } + select { + case <-downstreamPiggybackedReceived.Done(): case <-time.After(defaultTestTimeout): t.Fatalf("Timed out waiting for downstream piggybacked window update on processor stream") }