@@ -12710,6 +12710,253 @@ public void onCompleted() {
1271012710 channelManager.close();
1271112711 }
1271212712
12713+ @Test
12714+ @SuppressWarnings("unchecked")
12715+ public void testServerWindowUpdate_downstreamToSidestreamOverflowAvoidedAndNextMessageSent()
12716+ throws Exception {
12717+ ExternalProcessor proto = ExternalProcessor.newBuilder()
12718+ .setGrpcService(GrpcService.newBuilder()
12719+ .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder()
12720+ .setTargetUri("in-process:///" + extProcServerName)
12721+ .addChannelCredentialsPlugin(Any.newBuilder()
12722+ .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL)
12723+ .build())
12724+ .build())
12725+ .build())
12726+ .setProcessingMode(ProcessingMode.newBuilder()
12727+ .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC)
12728+ .setResponseBodyMode(ProcessingMode.BodySendMode.NONE)
12729+ .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP)
12730+ .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP)
12731+ .build())
12732+ .build();
12733+ ConfigOrError<ExternalProcessorFilterConfig> configOrError =
12734+ provider.parseFilterConfig(Any.pack(proto), filterContext);
12735+ assertThat(configOrError.errorDetail).isNull();
12736+ ExternalProcessorFilterConfig filterConfig = configOrError.config;
12737+
12738+ final List<ProcessingRequest> receivedRequests = new CopyOnWriteArrayList<>();
12739+ final CountDownLatch bodyLatch = new CountDownLatch(1);
12740+ final AtomicReference<StreamObserver<ProcessingResponse>> responseObserverRef =
12741+ new AtomicReference<>();
12742+
12743+ ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl =
12744+ new ExternalProcessorGrpc.ExternalProcessorImplBase() {
12745+ @Override
12746+ public StreamObserver<ProcessingRequest> process(
12747+ final StreamObserver<ProcessingResponse> responseObserver) {
12748+ responseObserverRef.set(responseObserver);
12749+ ((ServerCallStreamObserver<ProcessingResponse>) responseObserver).request(100);
12750+ return new StreamObserver<ProcessingRequest>() {
12751+ @Override
12752+ public void onNext(ProcessingRequest request) {
12753+ receivedRequests.add(request);
12754+ if (request.hasRequestHeaders()) {
12755+ responseObserver.onNext(ProcessingResponse.newBuilder()
12756+ .setRequestHeaders(HeadersResponse.newBuilder().build())
12757+ .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder()
12758+ .setWindowIncrementDownstreamToSidestream(Long.MAX_VALUE)
12759+ .build())
12760+ .build());
12761+ } else if (request.hasRequestBody()) {
12762+ bodyLatch.countDown();
12763+ }
12764+ }
12765+
12766+ @Override
12767+ public void onError(Throwable t) {}
12768+
12769+ @Override
12770+ public void onCompleted() {
12771+ responseObserver.onCompleted();
12772+ }
12773+ };
12774+ }
12775+ };
12776+
12777+ String uniqueExtProcServerName = InProcessServerBuilder.generateName();
12778+ grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName)
12779+ .addService(extProcImpl)
12780+ .directExecutor()
12781+ .build().start());
12782+
12783+ CachedChannelManager channelManager = new CachedChannelManager(config -> {
12784+ return grpcCleanup.register(
12785+ InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build());
12786+ });
12787+
12788+ ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor(
12789+ filterConfig, channelManager, scheduler, FAKE_CONTEXT);
12790+
12791+ dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService")
12792+ .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall(
12793+ (request, responseObserver) -> {
12794+ responseObserver.onNext("Hello " + request);
12795+ responseObserver.onCompleted();
12796+ }))
12797+ .build());
12798+
12799+ ManagedChannel dataPlaneChannel = grpcCleanup.register(
12800+ InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build());
12801+
12802+ CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor());
12803+ ClientCall<String, String> proxyCall =
12804+ interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel);
12805+
12806+ proxyCall.start(new ClientCall.Listener<String>() {}, new Metadata());
12807+ proxyCall.request(1);
12808+
12809+ // Verify proxyCall is ready and not blocked by negative window overflow
12810+ assertThat(proxyCall.isReady()).isTrue();
12811+
12812+ proxyCall.sendMessage("hello");
12813+
12814+ // Verify request body reached ext_proc without being blocked by negative overflow
12815+ assertThat(bodyLatch.await(5, TimeUnit.SECONDS)).isTrue();
12816+ assertThat(receivedRequests.size()).isAtLeast(2);
12817+ assertThat(receivedRequests.get(1).hasRequestBody()).isTrue();
12818+ assertThat(receivedRequests.get(1).getRequestBody().getBody().toStringUtf8()).isEqualTo("hello");
12819+
12820+ proxyCall.cancel("Cleanup", null);
12821+ channelManager.close();
12822+ }
12823+
12824+ @Test
12825+ @SuppressWarnings("unchecked")
12826+ public void testServerWindowUpdate_upstreamToSidestreamOverflowAvoidedAndNextMessageSent()
12827+ throws Exception {
12828+ ExternalProcessor proto = ExternalProcessor.newBuilder()
12829+ .setGrpcService(GrpcService.newBuilder()
12830+ .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder()
12831+ .setTargetUri("in-process:///" + extProcServerName)
12832+ .addChannelCredentialsPlugin(Any.newBuilder()
12833+ .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL)
12834+ .build())
12835+ .build())
12836+ .build())
12837+ .setProcessingMode(ProcessingMode.newBuilder()
12838+ .setRequestBodyMode(ProcessingMode.BodySendMode.NONE)
12839+ .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC)
12840+ .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP)
12841+ .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND)
12842+ .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND)
12843+ .build())
12844+ .build();
12845+ ConfigOrError<ExternalProcessorFilterConfig> configOrError =
12846+ provider.parseFilterConfig(Any.pack(proto), filterContext);
12847+ assertThat(configOrError.errorDetail).isNull();
12848+ ExternalProcessorFilterConfig filterConfig = configOrError.config;
12849+
12850+ final List<ProcessingRequest> receivedRequests = new CopyOnWriteArrayList<>();
12851+ final CountDownLatch responseBodyLatch = new CountDownLatch(1);
12852+ final AtomicReference<StreamObserver<ProcessingResponse>> responseObserverRef =
12853+ new AtomicReference<>();
12854+
12855+ ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl =
12856+ new ExternalProcessorGrpc.ExternalProcessorImplBase() {
12857+ @Override
12858+ public StreamObserver<ProcessingRequest> process(
12859+ final StreamObserver<ProcessingResponse> responseObserver) {
12860+ responseObserverRef.set(responseObserver);
12861+ ((ServerCallStreamObserver<ProcessingResponse>) responseObserver).request(100);
12862+ return new StreamObserver<ProcessingRequest>() {
12863+ @Override
12864+ public void onNext(ProcessingRequest request) {
12865+ receivedRequests.add(request);
12866+ if (request.hasResponseHeaders()) {
12867+ // Send ServerWindowUpdate with Long.MAX_VALUE increment for upstreamToSidestream
12868+ responseObserver.onNext(ProcessingResponse.newBuilder()
12869+ .setResponseHeaders(HeadersResponse.newBuilder().build())
12870+ .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder()
12871+ .setWindowIncrementUpstreamToSidestream(Long.MAX_VALUE)
12872+ .build())
12873+ .build());
12874+ } else if (request.hasResponseBody()) {
12875+ responseBodyLatch.countDown();
12876+ }
12877+ }
12878+
12879+ @Override
12880+ public void onError(Throwable t) {}
12881+
12882+ @Override
12883+ public void onCompleted() {
12884+ responseObserver.onCompleted();
12885+ }
12886+ };
12887+ }
12888+ };
12889+
12890+ String uniqueExtProcServerName = InProcessServerBuilder.generateName();
12891+ grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName)
12892+ .addService(extProcImpl)
12893+ .directExecutor()
12894+ .build().start());
12895+
12896+ CachedChannelManager channelManager = new CachedChannelManager(config -> {
12897+ return grpcCleanup.register(
12898+ InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build());
12899+ });
12900+
12901+ ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor(
12902+ filterConfig, channelManager, scheduler, FAKE_CONTEXT);
12903+
12904+ final AtomicReference<StreamObserver<String>> dataPlaneResponseObserverRef =
12905+ new AtomicReference<>();
12906+ final CountDownLatch upstreamConnectedLatch = new CountDownLatch(1);
12907+ dataPlaneServiceRegistry.addService(
12908+ ServerServiceDefinition.builder("test.TestService")
12909+ .addMethod(
12910+ METHOD_BIDI_STREAMING,
12911+ ServerCalls.asyncBidiStreamingCall(
12912+ new ServerCalls.BidiStreamingMethod<String, String>() {
12913+ @Override
12914+ public StreamObserver<String> invoke(
12915+ StreamObserver<String> responseObserver) {
12916+ dataPlaneResponseObserverRef.set(responseObserver);
12917+ upstreamConnectedLatch.countDown();
12918+ return new StreamObserver<String>() {
12919+ @Override
12920+ public void onNext(String value) {}
12921+
12922+ @Override
12923+ public void onError(Throwable t) {}
12924+
12925+ @Override
12926+ public void onCompleted() {}
12927+ };
12928+ }
12929+ }))
12930+ .build());
12931+
12932+ ManagedChannel dataPlaneChannel = grpcCleanup.register(
12933+ InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build());
12934+
12935+ ClientCall<String, String> proxyCall =
12936+ interceptCall(interceptor, METHOD_BIDI_STREAMING,
12937+ DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()),
12938+ dataPlaneChannel);
12939+
12940+ proxyCall.start(new ClientCall.Listener<String>() {}, new Metadata());
12941+ proxyCall.request(10);
12942+
12943+ assertThat(upstreamConnectedLatch.await(5, TimeUnit.SECONDS)).isTrue();
12944+ StreamObserver<String> upstreamResponseObserver = dataPlaneResponseObserverRef.get();
12945+ assertThat(upstreamResponseObserver).isNotNull();
12946+
12947+ // Upstream sends response message. Without saturated add, window would wrap to negative
12948+ // and buffer the message. With saturated add, it is sent to ext_proc immediately.
12949+ upstreamResponseObserver.onNext("server message");
12950+
12951+ assertThat(responseBodyLatch.await(5, TimeUnit.SECONDS)).isTrue();
12952+ assertThat(receivedRequests.size()).isAtLeast(2);
12953+ assertThat(receivedRequests.get(1).hasResponseBody()).isTrue();
12954+ assertThat(receivedRequests.get(1).getResponseBody().getBody().toStringUtf8()).isEqualTo("server message");
12955+
12956+ proxyCall.cancel("Cleanup", null);
12957+ channelManager.close();
12958+ }
12959+
1271312960 @Test
1271412961 @SuppressWarnings("unchecked")
1271512962 public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Exception {
0 commit comments