From 7c6740fc23c30c260bd97c68a3539c16eb9fc3a3 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 20 Jul 2026 09:33:02 +0000 Subject: [PATCH 01/13] xds: bypass sidecar readiness for non-GRPC responses & fix draining conformance - Bypass sidecar readiness checks in `request(int)` when observability mode is false and response body mode is not `GRPC`. - Ensure that when response headers, body, or trailers are skipped/disabled, they bypass request draining and stream directly downstream instead of being blocked. - Fix sidecar mocks in tests to properly call `onCompleted()` when the request stream completes, preventing background stream leaks. - Set appropriate processing modes in existing tests to match their flow control expectations under the updated logic. --- .../ExternalProcessorClientInterceptor.java | 49 +- ...xternalProcessorClientInterceptorTest.java | 470 ++++++++++++++++++ 2 files changed, 503 insertions(+), 16 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java index f61e1002926..7a4585ee240 100644 --- a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java +++ b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java @@ -768,6 +768,11 @@ public void request(int numMessages) { super.request(numMessages); return; } + if (!config.getObservabilityMode() + && currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC) { + super.request(numMessages); + return; + } if (!isSidecarReady()) { pendingRequests.addAndGet(numMessages); return; @@ -795,6 +800,10 @@ public void sendMessage(InputStream message) { ExtProcStreamState state = extProcStreamState.get(); if (state.isDraining() || state.isCompleted()) { + if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) { + super.sendMessage(message); + return; + } try { ByteString copiedBody = ByteString.readFrom(message); pendingDrainingMessages.add(new KnownLengthInputStream(copiedBody)); @@ -1072,16 +1081,17 @@ public void onReady() { public void onHeaders(Metadata headers) { dataPlaneClientCall.setServerHeadersStartNanos(System.nanoTime()); responseHeadersSent.set(true); - if (dataPlaneClientCall.getExtProcStreamState().get().isDraining()) { - this.savedHeaders = headers; - return; - } boolean sendResponseHeaders = dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode() == ProcessingMode.HeaderSendMode.SEND || dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode() == ProcessingMode.HeaderSendMode.DEFAULT; + if (dataPlaneClientCall.getExtProcStreamState().get().isDraining() && sendResponseHeaders) { + this.savedHeaders = headers; + return; + } + if (dataPlaneClientCall.getPassThroughMode().get() || dataPlaneClientCall.getExtProcStreamState().get().isCompleted() || !sendResponseHeaders) { @@ -1110,8 +1120,11 @@ public void onMessage(InputStream message) { return; } - if (savedHeaders != null - || dataPlaneClientCall.getExtProcStreamState().get().isDraining()) { + boolean checkDrain = dataPlaneClientCall.getExtProcStreamState().get().isDraining() + && dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode() + == ProcessingMode.BodySendMode.GRPC; + + if (savedHeaders != null || checkDrain) { try { ByteString copiedBody = ByteString.readFrom(message); savedMessages.add(new KnownLengthInputStream(copiedBody)); @@ -1184,7 +1197,11 @@ public void onClose(Status status, Metadata trailers) { return; } - if (dataPlaneClientCall.getExtProcStreamState().get().isDraining()) { + boolean sendResponseTrailers = + dataPlaneClientCall.getCurrentProcessingMode().getResponseTrailerMode() + == ProcessingMode.HeaderSendMode.SEND; + + if (dataPlaneClientCall.getExtProcStreamState().get().isDraining() && sendResponseTrailers) { return; } @@ -1193,15 +1210,6 @@ public void onClose(Status status, Metadata trailers) { } triggerCloseHandshake(); - - if (dataPlaneClientCall.getConfig().getObservabilityMode()) { - proceedWithClose(); - @SuppressWarnings("unused") - ScheduledFuture unused = dataPlaneClientCall.getScheduler().schedule( - dataPlaneClientCall::closeExtProcStream, - dataPlaneClientCall.getConfig().getDeferredCloseTimeoutNanos(), - TimeUnit.NANOSECONDS); - } } void onReadyNotify() { @@ -1330,6 +1338,15 @@ private void triggerCloseHandshake() { dataPlaneClientCall.closeExtProcStream(); } } + + if (dataPlaneClientCall.getConfig().getObservabilityMode()) { + proceedWithClose(); + @SuppressWarnings("unused") + ScheduledFuture unused = dataPlaneClientCall.getScheduler().schedule( + dataPlaneClientCall::closeExtProcStream, + dataPlaneClientCall.getConfig().getDeferredCloseTimeoutNanos(), + TimeUnit.NANOSECONDS); + } } private void sendResponseBodyToExtProc( diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index 9b07cae3477..be25007e009 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -6104,6 +6104,468 @@ public void givenDataPlaneCallIdle_whenIsReadyCalled_thenReturnsFalse() throws E // --- Category 14: Ext-proc request draining --- + @Test + @SuppressWarnings("unchecked") + public void testRequestBodyDrainingBypassedWhenRequestBodyModeNone() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + uniqueExtProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final CountDownLatch sidecarActionLatch = new CountDownLatch(1); + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .setRequestDrain(true) // Trigger Request Drain + .build()); + sidecarActionLatch.countDown(); + } + } + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + })) + .build()); + + final List dataPlaneSentMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .directExecutor() + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void sendMessage(ReqT message) { + try { + InputStream stream = (InputStream) message; + byte[] bytes = com.google.common.io.ByteStreams.toByteArray(stream); + dataPlaneSentMessages.add(new String(bytes, java.nio.charset.StandardCharsets.UTF_8)); + super.sendMessage((ReqT) new java.io.ByteArrayInputStream(bytes)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + }; + } + }) + .build()); + + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // Wait for the drain signal to be received and processed by client call + Thread.sleep(100); + + // Call is now in DRAINING state. + // Send a message. Since request_body_mode is NONE, it should go directly to data plane. + proxyCall.sendMessage("Hello ExtProc"); + + assertThat(dataPlaneSentMessages).containsExactly("Hello ExtProc"); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testResponseBodyDrainingBypassedWhenResponseBodyModeNone() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + uniqueExtProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final CountDownLatch sidecarActionLatch = new CountDownLatch(1); + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .setRequestDrain(true) // Trigger Request Drain + .build()); + sidecarActionLatch.countDown(); + } + } + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch appMessageLatch = new CountDownLatch(1); + ClientCall.Listener appListener = new ClientCall.Listener() { + @Override + public void onMessage(String message) { + appReceivedMessages.add(message); + appMessageLatch.countDown(); + } + }; + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, callOptions, dataPlaneChannel); + proxyCall.start(appListener, new Metadata()); + proxyCall.request(10); + + assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // Wait for the drain signal to be received and processed by client call + Thread.sleep(100); + + // Send response headers first (they bypass ext_proc because send mode is default SKIP, so they proceed immediately) + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Dummy for headers"); + + // Now call is in DRAINING state, and savedHeaders is null. + // Send response body message. Since response_body_mode is NONE, it should go directly downstream. + upstreamResponseObserver.onNext("Hello Downstream"); + + assertThat(appMessageLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appReceivedMessages).contains("Hello Downstream"); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testResponseHeadersDrainingBypassedWhenResponseHeadersSkip() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + uniqueExtProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final CountDownLatch sidecarActionLatch = new CountDownLatch(1); + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .setRequestDrain(true) // Trigger Request Drain + .build()); + sidecarActionLatch.countDown(); + } + } + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + final CountDownLatch headersLatch = new CountDownLatch(1); + ClientCall.Listener appListener = new ClientCall.Listener() { + @Override + public void onHeaders(Metadata headers) { + headersLatch.countDown(); + } + }; + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, callOptions, dataPlaneChannel); + proxyCall.start(appListener, new Metadata()); + proxyCall.request(10); + + assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // Wait for the drain signal to be received and processed by client call + Thread.sleep(100); + + // Call is in DRAINING state. + // Send response headers from server. Since response_header_mode is SKIP, they should go directly downstream. + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Dummy for headers"); + + assertThat(headersLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testResponseTrailersDrainingBypassedWhenResponseTrailersSkip() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + uniqueExtProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final CountDownLatch sidecarActionLatch = new CountDownLatch(1); + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .setRequestDrain(true) // Trigger Request Drain + .build()); + sidecarActionLatch.countDown(); + } + } + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + final CountDownLatch closeLatch = new CountDownLatch(1); + ClientCall.Listener appListener = new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + closeLatch.countDown(); + } + }; + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, callOptions, dataPlaneChannel); + proxyCall.start(appListener, new Metadata()); + proxyCall.request(10); + + assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // Wait for the drain signal to be received and processed by client call + Thread.sleep(100); + + // Call is in DRAINING state. + // Complete the server call. Since response_trailer_mode is SKIP, onClose should trigger immediately. + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onCompleted(); + + assertThat(closeLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + @Test @SuppressWarnings("unchecked") public void givenRequestDrainActive_whenIsReadyCalled_thenReturnsFalse() throws Exception { @@ -7592,6 +8054,10 @@ public void givenRequestDrainActive_whenAppRequestsMessages_thenRequestsBuffered .build()) .build()) .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) .build(); ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); @@ -9980,6 +10446,10 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenAppRequestsAreBuffer .build()) .build()) .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) .setObservabilityMode(false) .build(); ConfigOrError configOrError = From 5713e3f0e3f0e6e6a4bde71a3fe18565fa68f8e7 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 14 Jul 2026 08:47:22 +0000 Subject: [PATCH 02/13] Application level flow control on ext_proc client interceptor. --- .../ExternalProcessorClientInterceptor.java | 385 +++++- ...xternalProcessorClientInterceptorTest.java | 1099 ++++++++++++++++- .../filters/http/ext_proc/v3/ext_proc.proto | 45 +- .../ext_proc/v3/external_processor.proto | 438 ++++--- 4 files changed, 1711 insertions(+), 256 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java index f61e1002926..6eb01ea0add 100644 --- a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java +++ b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java @@ -197,7 +197,16 @@ public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { - SerializingExecutor serializingExecutor = new SerializingExecutor(callOptions.getExecutor()); + java.util.concurrent.Executor callExecutor = callOptions.getExecutor(); + if (callExecutor == null) { + callExecutor = new java.util.concurrent.Executor() { + @Override + public void execute(Runnable command) { + command.run(); + } + }; + } + SerializingExecutor serializingExecutor = new SerializingExecutor(callExecutor); ExternalProcessorGrpc.ExternalProcessorStub extProcStub = ExternalProcessorGrpc.newStub( extProcChannel) @@ -290,6 +299,28 @@ private static class DataPlaneClientCall private final HeaderMutator mutator = HeaderMutator.create(); private final AtomicInteger pendingRequests = new AtomicInteger(0); private final ProcessingMode currentProcessingMode; + + private static final long DEFAULT_INITIAL_WINDOW_SIZE = 65536; + + // Outbound (sending) windows + private long downstreamToSidestreamWindow = DEFAULT_INITIAL_WINDOW_SIZE; + private long upstreamToSidestreamWindow = DEFAULT_INITIAL_WINDOW_SIZE; + + // Inbound (receiving) windows + private long sidestreamToUpstreamWindow = DEFAULT_INITIAL_WINDOW_SIZE; + private long sidestreamToDownstreamWindow = DEFAULT_INITIAL_WINDOW_SIZE; + + // Buffered request body messages from downstream + private final Queue pendingRequestBodyMessages = new java.util.concurrent.ConcurrentLinkedQueue<>(); + + private static final long WINDOW_UPDATE_THRESHOLD = DEFAULT_INITIAL_WINDOW_SIZE / 2; + + // Accumulated client window updates for external processor + private long accumulatedWindowUpdateSidestreamToUpstream = 0; + private long accumulatedWindowUpdateSidestreamToDownstream = 0; + + // Track whether flow_control_init has been sent on the stream + private boolean flowControlInitSent = false; private final MethodDescriptor method; private final Channel channel; private final MetricRecorder metricsRecorder; @@ -316,6 +347,10 @@ private static class DataPlaneClientCall final AtomicBoolean pendingHalfClose = new AtomicBoolean(false); final AtomicBoolean bodyMessageSentToExtProc = new AtomicBoolean(false); + Object getStreamLock() { + return streamLock; + } + protected DataPlaneClientCall( DataPlaneDelayedCall delayedCall, ClientCall rawCall, @@ -439,6 +474,23 @@ public void onNext(ProcessingResponse response) { return; } + if (response.hasServerWindowUpdate()) { + ProcessingResponse.ServerWindowUpdate update = response.getServerWindowUpdate(); + boolean wasReady = isReady(); + synchronized (streamLock) { + downstreamToSidestreamWindow += update.getWindowIncrementDownstreamToSidestream(); + upstreamToSidestreamWindow += update.getWindowIncrementUpstreamToSidestream(); + drainPendingRequestBodyMessages(); + drainPendingRequests(); + } + if (wrappedListener != null) { + wrappedListener.drainSavedMessages(); + } + if (!wasReady && isReady()) { + onReadyNotify(); + } + } + if (response.hasImmediateResponse()) { if (config.getDisableImmediateResponse()) { internalOnError(Status.UNAVAILABLE @@ -613,7 +665,7 @@ public void onCompleted() { == ProcessingMode.HeaderSendMode.DEFAULT; if (sendRequestHeaders) { - sendToExtProc(ProcessingRequest.newBuilder() + sendToExtProcDirect(ProcessingRequest.newBuilder() .setRequestHeaders(HttpHeaders.newBuilder() .setHeaders(toHeaderMap(headers, config.getForwardRulesConfig())) .setEndOfStream(false) @@ -626,12 +678,12 @@ public void onCompleted() { } } - private void sendToExtProc(ProcessingRequest request) { + private void sendToExtProcDirect(ProcessingRequest request) { synchronized (streamLock) { if (extProcStreamState.get().isCompleted()) { return; } - + if (request.hasRequestHeaders()) { expectedRequestResponse = EventType.REQUEST_HEADERS; } else if (request.hasResponseHeaders()) { @@ -667,21 +719,98 @@ private void sendToExtProc(ProcessingRequest request) { requestToSend = ProcessingRequest.newBuilder(requestToSend) .setObservabilityMode(true) .build(); + } else if (!flowControlInitSent) { + requestToSend = ProcessingRequest.newBuilder(requestToSend) + .setFlowControlInit(ProcessingRequest.FlowControlInit.newBuilder() + .setInitialWindowDownstreamToSidestream(DEFAULT_INITIAL_WINDOW_SIZE) + .setInitialWindowSidestreamToUpstream(DEFAULT_INITIAL_WINDOW_SIZE) + .setInitialWindowUpstreamToSidestreama(DEFAULT_INITIAL_WINDOW_SIZE) + .setInitialWindowSidestreamToDownstream(DEFAULT_INITIAL_WINDOW_SIZE) + .build()) + .build(); + flowControlInitSent = true; } extProcClientCallRequestObserver.onNext(requestToSend); } } + void mergeAccumulatedWindowUpdates(ProcessingRequest.Builder requestBuilder) { + synchronized (streamLock) { + long incrementUpstream = (super.isReady() || sidestreamToUpstreamWindow < 0) + ? accumulatedWindowUpdateSidestreamToUpstream : 0; + long incrementDownstream = accumulatedWindowUpdateSidestreamToDownstream; + + if (incrementUpstream > 0 || incrementDownstream > 0) { + requestBuilder.setClientWindowUpdate( + ProcessingRequest.ClientWindowUpdate.newBuilder() + .setWindowIncrementSidestreamToUpstream(incrementUpstream) + .setWindowIncrementSidestreamToDownstream(incrementDownstream) + .build()); + accumulatedWindowUpdateSidestreamToUpstream -= incrementUpstream; + accumulatedWindowUpdateSidestreamToDownstream -= incrementDownstream; + sidestreamToUpstreamWindow += incrementUpstream; + sidestreamToDownstreamWindow += incrementDownstream; + } + } + } + + private void trySendAccumulatedWindowUpdates() { + synchronized (streamLock) { + System.out.println("trySendAccumulatedWindowUpdates: completed=" + extProcStreamState.get().isCompleted() + + ", super.isReady()=" + super.isReady() + + ", sidestreamToUpstreamWindow=" + sidestreamToUpstreamWindow + + ", accumulatedUpstream=" + accumulatedWindowUpdateSidestreamToUpstream + + ", accumulatedDownstream=" + accumulatedWindowUpdateSidestreamToDownstream); + if (extProcStreamState.get().isCompleted()) { + return; + } + long incrementUpstream = (super.isReady() || sidestreamToUpstreamWindow < 0) + ? accumulatedWindowUpdateSidestreamToUpstream : 0; + long incrementDownstream = accumulatedWindowUpdateSidestreamToDownstream; + + boolean shouldSend = (incrementUpstream >= WINDOW_UPDATE_THRESHOLD) + || (incrementDownstream >= WINDOW_UPDATE_THRESHOLD) + || (sidestreamToUpstreamWindow < 0 && accumulatedWindowUpdateSidestreamToUpstream > 0) + || (sidestreamToDownstreamWindow < 0 && accumulatedWindowUpdateSidestreamToDownstream > 0); + + if (shouldSend) { + accumulatedWindowUpdateSidestreamToUpstream -= incrementUpstream; + accumulatedWindowUpdateSidestreamToDownstream -= incrementDownstream; + sidestreamToUpstreamWindow += incrementUpstream; + sidestreamToDownstreamWindow += incrementDownstream; + + sendToExtProcDirect(ProcessingRequest.newBuilder() + .setClientWindowUpdate(ProcessingRequest.ClientWindowUpdate.newBuilder() + .setWindowIncrementSidestreamToUpstream(incrementUpstream) + .setWindowIncrementSidestreamToDownstream(incrementDownstream) + .build()) + .build()); + } + } + } + private void onExtProcStreamReady() { drainPendingRequests(); onReadyNotify(); } - private void drainPendingRequests() { - int toRequest = pendingRequests.getAndSet(0); - if (toRequest > 0) { - super.request(toRequest); + void drainPendingRequests() { + synchronized (streamLock) { + if (config.getObservabilityMode() + || currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC) { + int toRequest = pendingRequests.getAndSet(0); + if (toRequest > 0) { + super.request(toRequest); + } + return; + } + + // Normal mode flow control: pull 1 message at a time + if (isSidecarReady() && upstreamToSidestreamWindow > 0 && pendingRequests.get() > 0) { + super.request(1); + pendingRequests.decrementAndGet(); + } } } @@ -730,7 +859,7 @@ private void onReadyNotify() { wrappedListener.onReadyNotify(); } - private boolean isSidecarReady() { + boolean isSidecarReady() { ExtProcStreamState state = extProcStreamState.get(); if (state.isCompleted()) { return true; @@ -755,11 +884,13 @@ public boolean isReady() { if (dataPlaneCallState.get() == DataPlaneCallState.IDLE && !config.getObservabilityMode()) { return false; } - boolean sidecarReady = isSidecarReady(); - if (config.getObservabilityMode()) { - return super.isReady() && sidecarReady; + synchronized (streamLock) { + boolean sidecarReady = isSidecarReady(); + if (config.getObservabilityMode()) { + return super.isReady() && sidecarReady; + } + return downstreamToSidestreamWindow > 0 && sidecarReady && pendingRequestBodyMessages.isEmpty(); } - return sidecarReady; } @Override @@ -772,7 +903,15 @@ public void request(int numMessages) { pendingRequests.addAndGet(numMessages); return; } - super.request(numMessages); + if (config.getObservabilityMode() + || currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC) { + super.request(numMessages); + return; + } + synchronized (streamLock) { + pendingRequests.addAndGet(numMessages); + drainPendingRequests(); + } } @Override @@ -803,29 +942,60 @@ public void sendMessage(InputStream message) { } return; } - } - if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) { - super.sendMessage(message); - return; + if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) { + super.sendMessage(message); + return; + } + + // Mode is GRPC + try { + ByteString bodyByteString = outboundStreamToByteString(message); + if (config.getObservabilityMode()) { + sendToExtProcDirect(ProcessingRequest.newBuilder() + .setRequestBody(HttpBody.newBuilder() + .setBody(bodyByteString) + .setEndOfStream(false) + .build()) + .build()); + bodyMessageSentToExtProc.set(true); + super.sendMessage(new KnownLengthInputStream(bodyByteString)); + } else { + if (bodyByteString.size() > downstreamToSidestreamWindow || !pendingRequestBodyMessages.isEmpty()) { + pendingRequestBodyMessages.add(bodyByteString); + } else { + sendRequestBodyToExtProc(bodyByteString); + } + } + } catch (IOException e) { + rawCall.cancel("Failed to serialize message for External Processor", e); + } } + } - // Mode is GRPC - try { - ByteString bodyByteString = outboundStreamToByteString(message); - sendToExtProc(ProcessingRequest.newBuilder() + private void sendRequestBodyToExtProc(ByteString body) { + synchronized (streamLock) { + downstreamToSidestreamWindow -= body.size(); + ProcessingRequest.Builder builder = ProcessingRequest.newBuilder() .setRequestBody(HttpBody.newBuilder() - .setBody(bodyByteString) + .setBody(body) .setEndOfStream(false) - .build()) - .build()); + .build()); + mergeAccumulatedWindowUpdates(builder); + sendToExtProcDirect(builder.build()); bodyMessageSentToExtProc.set(true); + } + } - if (config.getObservabilityMode()) { - super.sendMessage(new KnownLengthInputStream(bodyByteString)); + private void drainPendingRequestBodyMessages() { + synchronized (streamLock) { + while (downstreamToSidestreamWindow > 0 && !pendingRequestBodyMessages.isEmpty()) { + ByteString body = pendingRequestBodyMessages.poll(); + sendRequestBodyToExtProc(body); + } + if (pendingRequestBodyMessages.isEmpty() && pendingHalfClose.get()) { + halfClose(); } - } catch (IOException e) { - rawCall.cancel("Failed to serialize message for External Processor", e); } } @@ -840,6 +1010,10 @@ private void proceedWithHalfClose() { @Override public void halfClose() { + System.out.println("halfClose: passThrough=" + passThroughMode.get() + + ", extProcStreamCompleted=" + extProcStreamState.get().isCompleted() + + ", extProcStreamDraining=" + extProcStreamState.get().isDraining() + + ", bodyMode=" + currentProcessingMode.getRequestBodyMode()); clientHalfCloseStartNanos = System.nanoTime(); if (passThroughMode.get()) { if (requestSideClosed.compareAndSet(false, true)) { @@ -866,11 +1040,18 @@ public void halfClose() { } // Mode is GRPC - sendToExtProc(ProcessingRequest.newBuilder() - .setRequestBody(HttpBody.newBuilder() - .setEndOfStreamWithoutMessage(true) - .build()) - .build()); + synchronized (streamLock) { + if (!pendingRequestBodyMessages.isEmpty()) { + return; + } + + ProcessingRequest.Builder builder = ProcessingRequest.newBuilder() + .setRequestBody(HttpBody.newBuilder() + .setEndOfStreamWithoutMessage(true) + .build()); + mergeAccumulatedWindowUpdates(builder); + sendToExtProcDirect(builder.build()); + } } @Override @@ -893,7 +1074,23 @@ private void handleRequestBodyResponse(BodyResponse bodyResponse) { if (mutation.hasStreamedResponse()) { StreamedBodyResponse streamed = mutation.getStreamedResponse(); if (!streamed.getEndOfStreamWithoutMessage()) { - super.sendMessage(new KnownLengthInputStream(streamed.getBody())); + com.google.protobuf.ByteString body = streamed.getBody(); + if (!config.getObservabilityMode() && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { + synchronized (streamLock) { + if (body.size() > sidestreamToUpstreamWindow) { + internalOnError(Status.INTERNAL + .withDescription("Flow control violation: received client body from ext_proc when window is closed") + .asRuntimeException()); + return; + } + sidestreamToUpstreamWindow -= body.size(); + accumulatedWindowUpdateSidestreamToUpstream += body.size(); + } + } + super.sendMessage(new KnownLengthInputStream(body)); + if (!config.getObservabilityMode() && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { + trySendAccumulatedWindowUpdates(); + } } if (streamed.getEndOfStream() || streamed.getEndOfStreamWithoutMessage()) { if (requestSideClosed.compareAndSet(false, true)) { @@ -910,7 +1107,31 @@ private void handleResponseBodyResponse( BodyMutation mutation = bodyResponse.getResponse().getBodyMutation(); if (mutation.hasStreamedResponse()) { StreamedBodyResponse streamed = mutation.getStreamedResponse(); - listener.onExternalBody(streamed.getBody()); + com.google.protobuf.ByteString body = streamed.getBody(); + final int bodySize = body.size(); + if (!config.getObservabilityMode() && currentProcessingMode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC) { + synchronized (streamLock) { + if (bodySize > sidestreamToDownstreamWindow) { + internalOnError(Status.INTERNAL + .withDescription("Flow control violation: received server body from ext_proc when window is closed") + .asRuntimeException()); + return; + } + sidestreamToDownstreamWindow -= bodySize; + } + } + callContext.run(() -> { + try { + listener.onExternalBody(body); + } finally { + if (!config.getObservabilityMode() && currentProcessingMode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC) { + synchronized (streamLock) { + accumulatedWindowUpdateSidestreamToDownstream += bodySize; + } + trySendAccumulatedWindowUpdates(); + } + } + }); } } } @@ -1064,6 +1285,7 @@ void setImmediateResponse(Status status, Metadata trailers) { @Override public void onReady() { + dataPlaneClientCall.trySendAccumulatedWindowUpdates(); dataPlaneClientCall.drainPendingRequests(); onReadyNotify(); } @@ -1090,7 +1312,7 @@ public void onHeaders(Metadata headers) { } this.savedHeaders = headers; - dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder() + dataPlaneClientCall.sendToExtProcDirect(ProcessingRequest.newBuilder() .setResponseHeaders(HttpHeaders.newBuilder() .setHeaders( toHeaderMap(headers, dataPlaneClientCall.getConfig().getForwardRulesConfig())) @@ -1104,7 +1326,7 @@ public void onHeaders(Metadata headers) { @Override public void onMessage(InputStream message) { - synchronized (savedMessages) { + synchronized (dataPlaneClientCall.getStreamLock()) { if (inboundPassThrough) { dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message)); return; @@ -1120,34 +1342,60 @@ public void onMessage(InputStream message) { } return; } - } - if (dataPlaneClientCall.getPassThroughMode().get()) { - dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message)); - return; - } + if (dataPlaneClientCall.getPassThroughMode().get()) { + dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message)); + return; + } - if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted() - || dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode() - != ProcessingMode.BodySendMode.GRPC) { - dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message)); - return; - } + if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted() + || dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode() + != ProcessingMode.BodySendMode.GRPC) { + dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message)); + return; + } - try { - ByteString bodyByteString = ByteString.readFrom(message); - sendResponseBodyToExtProc(bodyByteString, false); - dataPlaneClientCall.bodyMessageSentToExtProc.set(true); + try { + ByteString bodyByteString = ByteString.readFrom(message); + if (dataPlaneClientCall.getConfig().getObservabilityMode()) { + sendResponseBodyToExtProc(bodyByteString, false); + dataPlaneClientCall.bodyMessageSentToExtProc.set(true); + dataPlaneClientCall.getCallContext().run( + () -> delegate().onMessage(bodyByteString.newInput())); + } else { + if (bodyByteString.size() > dataPlaneClientCall.upstreamToSidestreamWindow || !savedMessages.isEmpty()) { + savedMessages.add(new KnownLengthInputStream(bodyByteString)); + } else { + dataPlaneClientCall.upstreamToSidestreamWindow -= bodyByteString.size(); + sendResponseBodyToExtProc(bodyByteString, false); + dataPlaneClientCall.bodyMessageSentToExtProc.set(true); + } + dataPlaneClientCall.drainPendingRequests(); + } + } catch (IOException e) { + rawCall.cancel("Failed to read server response", e); + } + } + } - if (dataPlaneClientCall.getConfig().getObservabilityMode()) { - // If needed, downstream reading can be made more optimal by creating a wrapped - // Inputstream wraps the underlying bytestring and that implements HasByteBuffer, - // Detachable, KnownLength - dataPlaneClientCall.getCallContext().run( - () -> delegate().onMessage(bodyByteString.newInput())); + void drainSavedMessages() { + synchronized (dataPlaneClientCall.getStreamLock()) { + while (dataPlaneClientCall.isSidecarReady() + && dataPlaneClientCall.upstreamToSidestreamWindow > 0 + && !savedMessages.isEmpty()) { + InputStream msg = savedMessages.poll(); + if (msg != null) { + try { + ByteString bodyByteString = ByteString.readFrom(msg); + dataPlaneClientCall.upstreamToSidestreamWindow -= bodyByteString.size(); + sendResponseBodyToExtProc(bodyByteString, false); + dataPlaneClientCall.bodyMessageSentToExtProc.set(true); + } catch (IOException e) { + rawCall.cancel("Failed to read buffered response body", e); + } + } } - } catch (IOException e) { - rawCall.cancel("Failed to read server response", e); + dataPlaneClientCall.drainPendingRequests(); } } @@ -1211,7 +1459,7 @@ void onReadyNotify() { void proceedWithHeaders() { if (savedHeaders != null) { proceedWithHeaders(savedHeaders); - synchronized (savedMessages) { + synchronized (dataPlaneClientCall.getStreamLock()) { savedHeaders = null; if (!dataPlaneClientCall.getExtProcStreamState().get().isDraining()) { InputStream msg; @@ -1271,7 +1519,7 @@ void unblockAfterStreamComplete() { } private void proceedWithSavedMessages() { - synchronized (savedMessages) { + synchronized (dataPlaneClientCall.getStreamLock()) { InputStream msg; while ((msg = savedMessages.poll()) != null) { final InputStream finalMsg = msg; @@ -1299,7 +1547,7 @@ private void triggerCloseHandshake() { if (trailersOnly.get()) { if (sendResponseHeaders) { - dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder() + dataPlaneClientCall.sendToExtProcDirect(ProcessingRequest.newBuilder() .setResponseHeaders(HttpHeaders.newBuilder() .setHeaders( toHeaderMap( @@ -1316,7 +1564,7 @@ private void triggerCloseHandshake() { } } else if (sendResponseTrailers) { dataPlaneClientCall.getIsProcessingTrailers().set(true); - dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder() + dataPlaneClientCall.sendToExtProcDirect(ProcessingRequest.newBuilder() .setResponseTrailers(HttpTrailers.newBuilder() .setTrailers( toHeaderMap( @@ -1347,9 +1595,10 @@ private void sendResponseBodyToExtProc( } bodyBuilder.setEndOfStream(endOfStream); - dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder() - .setResponseBody(bodyBuilder.build()) - .build()); + ProcessingRequest.Builder builder = ProcessingRequest.newBuilder() + .setResponseBody(bodyBuilder.build()); + dataPlaneClientCall.mergeAccumulatedWindowUpdates(builder); + dataPlaneClientCall.sendToExtProcDirect(builder.build()); } } } diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index 9b07cae3477..d695f9fc40a 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -10146,7 +10146,7 @@ public void givenClientStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveM Metadata.Key.of("req-mutated", Metadata.ASCII_STRING_MARSHALLER); final List receivedPhases = Collections.synchronizedList(new ArrayList<>()); - final CountDownLatch sidecarActionLatch = new CountDownLatch(6); + final CountDownLatch sidecarActionLatch = new CountDownLatch(5); final ExecutorService sidecarResponseExecutor = Executors.newSingleThreadExecutor(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -10182,8 +10182,24 @@ public void onNext(ProcessingRequest request) { .build()) .build()); } else if (request.hasRequestBody()) { - if (request.getRequestBody().getEndOfStream() - || request.getRequestBody().getEndOfStreamWithoutMessage()) { + if (request.getRequestBody().getEndOfStreamWithoutMessage()) { + receivedPhases.add("REQ_BODY_EOS"); + resp.setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setEndOfStream(true) + .setEndOfStreamWithoutMessage(true) + .build()) + .build()) + .build()) + .build()); + } else if (request.getRequestBody().getEndOfStream()) { + receivedPhases.add("REQ_BODY_MSG"); receivedPhases.add("REQ_BODY_EOS"); resp.setRequestBody( BodyResponse.newBuilder() @@ -10193,10 +10209,9 @@ public void onNext(ProcessingRequest request) { BodyMutation.newBuilder() .setStreamedResponse( StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8( + "MutatedRequest")) .setEndOfStream(true) - .setEndOfStreamWithoutMessage( - request.getRequestBody() - .getEndOfStreamWithoutMessage()) .build()) .build()) .build()) @@ -10424,7 +10439,7 @@ public void givenBidiStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveMut Metadata.Key.of("req-mutated", Metadata.ASCII_STRING_MARSHALLER); final List receivedPhases = Collections.synchronizedList(new ArrayList<>()); - final CountDownLatch sidecarBidiLatch = new CountDownLatch(6); + final CountDownLatch sidecarBidiLatch = new CountDownLatch(5); final ExecutorService bidiSidecarResponseExecutor = Executors.newSingleThreadExecutor(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase bidiExtProcImpl; @@ -10460,8 +10475,24 @@ public void onNext(ProcessingRequest request) { .build()) .build()); } else if (request.hasRequestBody()) { - if (request.getRequestBody().getEndOfStream() - || request.getRequestBody().getEndOfStreamWithoutMessage()) { + if (request.getRequestBody().getEndOfStreamWithoutMessage()) { + receivedPhases.add("REQ_BODY_EOS"); + resp.setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setEndOfStream(true) + .setEndOfStreamWithoutMessage(true) + .build()) + .build()) + .build()) + .build()); + } else if (request.getRequestBody().getEndOfStream()) { + receivedPhases.add("REQ_BODY_MSG"); receivedPhases.add("REQ_BODY_EOS"); resp.setRequestBody( BodyResponse.newBuilder() @@ -10471,10 +10502,9 @@ public void onNext(ProcessingRequest request) { BodyMutation.newBuilder() .setStreamedResponse( StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8( + "MutatedBidiReq")) .setEndOfStream(true) - .setEndOfStreamWithoutMessage( - request.getRequestBody() - .getEndOfStreamWithoutMessage()) .build()) .build()) .build()) @@ -10489,8 +10519,8 @@ public void onNext(ProcessingRequest request) { BodyMutation.newBuilder() .setStreamedResponse( StreamedBodyResponse.newBuilder() - .setBody( - ByteString.copyFromUtf8("MutatedBidiReq")) + .setBody(ByteString.copyFromUtf8( + "MutatedBidiReq")) .build()) .build()) .build()) @@ -14634,6 +14664,1047 @@ public void onClose(Status status, Metadata trailers) { channelManager.close(); } + @Test + @SuppressWarnings("unchecked") + public void testFlowControlStateInitialization() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(2); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + extProcLatch.countDown(); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.sendMessage("Message 1"); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(receivedRequests).hasSize(2); + ProcessingRequest firstRequest = receivedRequests.get(0); + ProcessingRequest secondRequest = receivedRequests.get(1); + + assertThat(firstRequest.hasRequestHeaders()).isTrue(); + assertThat(firstRequest.hasFlowControlInit()).isTrue(); + assertThat(firstRequest.getFlowControlInit().getInitialWindowDownstreamToSidestream()).isEqualTo(65536); + assertThat(firstRequest.getFlowControlInit().getInitialWindowSidestreamToUpstream()).isEqualTo(65536); + + assertThat(secondRequest.hasRequestBody()).isTrue(); + assertThat(secondRequest.hasFlowControlInit()).isFalse(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch firstBodyLatch = new CountDownLatch(2); // Headers + First Body + final CountDownLatch secondBodyLatch = new CountDownLatch(1); + final AtomicReference> responseObserverRef = new AtomicReference<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + firstBodyLatch.countDown(); + } else if (request.hasRequestBody()) { + if (firstBodyLatch.getCount() > 0) { + // This is the first body (40000 bytes) + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + firstBodyLatch.countDown(); + } else { + // This is the second body (30000 bytes) + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + secondBodyLatch.countDown(); + } + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final List dataPlaneReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) { + dataPlaneReceivedMessages.add(value); + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + // Generate large messages + String largeMessage40k = new String(new char[40000]).replace('\0', 'a'); + String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); + + // Send first message (40000 bytes) - fits in 65536 window + proxyCall.sendMessage(largeMessage40k); + assertThat(firstBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(proxyCall.isReady()).isTrue(); + + // Send second message (30000 bytes) - total 70000 > 65536, should buffer + proxyCall.sendMessage(largeMessage30k); + + // Wait a brief moment to confirm it is NOT delivered to ext_proc (latch shouldn't tick) + assertThat(receivedRequests).hasSize(3); // Headers + First Body + Client Window Update (Path 2 replenishment) + assertThat(proxyCall.isReady()).isFalse(); + + // Now send ServerWindowUpdate from ext_proc to interceptor to increment window by 40000 + responseObserverRef.get().onNext(ProcessingResponse.newBuilder() + .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder() + .setWindowIncrementDownstreamToSidestream(40000) + .build()) + .build()); + + // The second body should now be flushed and received by ext_proc + assertThat(secondBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(receivedRequests.size()).isAtLeast(4); + + assertThat(receivedRequests.get(0).hasRequestHeaders()).isTrue(); + assertThat(receivedRequests.get(1).hasRequestBody()).isTrue(); + assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); + assertThat(receivedRequests.get(3).hasRequestBody()).isTrue(); + assertThat(receivedRequests.get(3).getRequestBody().getBody().size()).isEqualTo(30000); + + assertThat(proxyCall.isReady()).isTrue(); + + // Verify both messages reached the backend service + assertThat(dataPlaneReceivedMessages).containsExactly(largeMessage40k, largeMessage30k).inOrder(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testUpstreamToSidestreamFlowControl_EnforcesWindow() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(4); // Headers, Request Body, Response Headers, Response Body 1 + final CountDownLatch secondResponseBodyLatch = new CountDownLatch(1); + final AtomicReference> responseObserverRef = new AtomicReference<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + extProcLatch.countDown(); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getResponseBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + if (request.getResponseBody().getBody().size() == 30000) { + secondResponseBodyLatch.countDown(); + } + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch messagesLatch2 = new CountDownLatch(2); + final CountDownLatch messagesLatch3 = new CountDownLatch(3); + ClientCall.Listener appListener = new ClientCall.Listener() { + @Override + public void onMessage(String message) { + appReceivedMessages.add(message); + messagesLatch2.countDown(); + messagesLatch3.countDown(); + } + }; + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(appListener, new Metadata()); + proxyCall.request(10); + + proxyCall.sendMessage("Client Msg"); + + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Dummy for headers"); + + String largeMessage40k = new String(new char[40000]).replace('\0', 'a'); + String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + upstreamResponseObserver.onNext(largeMessage40k); + + assertThat(messagesLatch2.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appReceivedMessages).hasSize(2); + + upstreamResponseObserver.onNext(largeMessage30k); + + Thread.sleep(200); + assertThat(secondResponseBodyLatch.getCount()).isEqualTo(1); + assertThat(appReceivedMessages).hasSize(2); + + responseObserverRef.get().onNext(ProcessingResponse.newBuilder() + .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder() + .setWindowIncrementUpstreamToSidestream(40000) + .build()) + .build()); + + assertThat(secondResponseBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(messagesLatch3.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appReceivedMessages).hasSize(3); + assertThat(appReceivedMessages.get(2)).isEqualTo(largeMessage30k); + assertThat(receivedRequests).isNotEmpty(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testSidestreamToUpstreamFlowControl_Violations() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'c'); + final CountDownLatch callClosedLatch = new CountDownLatch(1); + final AtomicReference capturedStatus = new AtomicReference<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + // Respond with a mutated body that is larger than the initial window size of 65536 + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + capturedStatus.set(status); + callClosedLatch.countDown(); + } + }, new Metadata()); + + proxyCall.sendMessage("Message 1"); + + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // The call should fail immediately with INTERNAL error code due to flow control violation + assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); + assertThat(capturedStatus.get().getCause()).isInstanceOf(io.grpc.StatusRuntimeException.class); + assertThat(capturedStatus.get().getCause().getMessage()).contains("Flow control violation: received client body from ext_proc when window is closed"); + + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testSidestreamToDownstreamFlowControl_Violations() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'd'); + final CountDownLatch callClosedLatch = new CountDownLatch(1); + final AtomicReference capturedStatus = new AtomicReference<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseBody()) { + // Respond with a mutated body that is larger than the initial window size of 65536 + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + capturedStatus.set(status); + callClosedLatch.countDown(); + } + }, new Metadata()); + proxyCall.request(10); + + proxyCall.sendMessage("Client Msg"); + + // Send a response from upstream to trigger headers and then the body response + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Response Msg"); + + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // The call should fail immediately with INTERNAL error code due to flow control violation + assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); + assertThat(capturedStatus.get().getCause()).isInstanceOf(io.grpc.StatusRuntimeException.class); + assertThat(capturedStatus.get().getCause().getMessage()).contains("Flow control violation: received server body from ext_proc when window is closed"); + + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testThresholdBasedWindowUpdates() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(1); + final List> observers = new ArrayList<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + observers.add(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(filterClientRequests(receivedRequests)).hasSize(1); + assertThat(filterClientRequests(receivedRequests).get(0).hasRequestHeaders()).isTrue(); + + // 1. Send body message. It should be sent immediately. + proxyCall.sendMessage("Msg 1"); // size = 5 bytes + + assertThat(filterClientRequests(receivedRequests)).hasSize(2); + assertThat(filterClientRequests(receivedRequests).get(1).hasRequestBody()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(1).getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 1"); + // No window updates were accumulated yet. + assertThat(filterClientRequests(receivedRequests).get(1).hasClientWindowUpdate()).isFalse(); + + // 2. Trigger window replenishment below threshold (e.g. 5 bytes from Msg 1 response). + // The interceptor processes the response, forwards it upstream, and increments + // accumulatedWindowUpdateSidestreamToUpstream. Since 5 < 32768, it won't send standalone updates. + // We send another message "Msg 2" to trigger piggybacking. + proxyCall.sendMessage("Msg 2"); + + assertThat(filterClientRequests(receivedRequests)).hasSize(3); + assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 2"); + // Verify accumulated 5 bytes update is piggybacked. + assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(5); + + // 3. Accumulate past threshold (e.g. 35,000 bytes) without sending body messages. + // This should trigger an immediate standalone ClientWindowUpdate. + StreamObserver responseObserver = observers.get(0); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFrom(new byte[35000])) + .build()) + .build()) + .build()) + .build()) + .build()); + + // standalone client window update received. + assertThat(filterClientRequests(receivedRequests)).hasSize(4); + assertThat(filterClientRequests(receivedRequests).get(3).hasClientWindowUpdate()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(3).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(35005); + assertThat(filterClientRequests(receivedRequests).get(3).hasRequestBody()).isFalse(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testHalfClosePiggybacking() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(filterClientRequests(receivedRequests)).hasSize(1); + + proxyCall.sendMessage("Last Message"); + + + + // Verify 12 bytes are accumulated but no standalone update is sent. + assertThat(filterClientRequests(receivedRequests)).hasSize(2); + + proxyCall.halfClose(); + + // Verify halfClose sends EOF request piggybacking the accumulated 12 bytes update. + assertThat(filterClientRequests(receivedRequests)).hasSize(3); + assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).getRequestBody().getEndOfStreamWithoutMessage()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(12); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + private static List filterClientRequests(List requests) { + List clientRequests = new ArrayList<>(); + for (ProcessingRequest r : requests) { + if (r.hasRequestHeaders() || r.hasRequestBody() || (r.hasClientWindowUpdate() && !r.hasResponseBody() && !r.hasResponseHeaders() && !r.hasResponseTrailers())) { + clientRequests.add(r); + } + } + return clientRequests; + } + private static ClientCall interceptCall( ExternalProcessorClientInterceptor interceptor, MethodDescriptor method, diff --git a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto index b07811d5235..668ba0077ff 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/extensions/filters/http/ext_proc/v3/ext_proc.proto @@ -98,7 +98,7 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // ` object in a namespace matching the filter // name. // -// [#next-free-field: 26] +// [#next-free-field: 28] message ExternalProcessor { // Describes the route cache action to be taken when an external processor response // is received in response to request headers. @@ -285,14 +285,6 @@ message ExternalProcessor { // // 3. External processor may still close the stream to indicate that no more messages are needed. // - // .. warning:: - // - // Flow control is a necessary mechanism to prevent the fast sender (either downstream client or upstream server) - // from overwhelming the external processor when its processing speed is slower. - // This protective measure is being explored and developed but has not been ready yet, so please use your own - // discretion when enabling this feature. - // This work is currently tracked under https://github.com/envoyproxy/envoy/issues/33319. - // bool observability_mode = 17; // Prevents clearing the route-cache when the @@ -307,6 +299,17 @@ message ExternalProcessor { // received in response to request headers. It is recommended to set this field rather than set // :ref:`disable_clear_route_cache `. // Only one of ``disable_clear_route_cache`` or ``route_cache_action`` can be set. + // + // .. attention:: + // + // Clearing the route cache can cause Envoy to recompute route matching after earlier HTTP + // filters have already processed the request. This can be security-sensitive when filters + // that make route-dependent authorization decisions, such as the RBAC filter, run before + // ext_proc and ext_proc mutates route-matching inputs. + // + // Operators should only enable route cache clearing for trusted external processors, should + // carefully order route-dependent authorization filters, and should use mutation_rules to + // restrict sensitive mutations when appropriate. RouteCacheAction route_cache_action = 18 [(udpa.annotations.field_migrate).oneof_promotion = "clear_route_cache_type"]; @@ -369,6 +372,30 @@ message ExternalProcessor { // // The default status is ``HTTP 500 Internal Server Error``. type.v3.HttpStatus status_on_error = 24; + + // If true, the filter will not remove the ``content-length`` header from the request/response after external processing. + // It is typically used in + // :ref:`FULL_DUPLEX_STREAMED ` + // mode. If the original body has been modified, the external processing server needs to set the correct content-length header in HeaderMutation + // that matches the modified body length. + // + // .. warning:: + // + // This configuration should only be used if you are sure that the content length matches + // the body length after external processing. Otherwise, it may cause vulnerability issues such as + // request smuggling. Thus, please use your own discretion when enabling this feature. + // + bool allow_content_length_header = 26; + + // In ``FULL_DUPLEX_STREAMED`` body send mode, if the data plane does not + // receive the first response message on the ext_proc stream within this + // timeout, it will assume that the ext_proc server does not support flow + // control, and the stream will proceed without using flow control. + // This knob exists for backward compatibility with legacy ext_proc + // servers. + // + // [#not-implemented-hide:] + google.protobuf.Duration flow_control_init_timeout = 27; } // ExtProcHttpService is used for HTTP communication between the filter and the external processing service. diff --git a/xds/third_party/envoy/src/main/proto/envoy/service/ext_proc/v3/external_processor.proto b/xds/third_party/envoy/src/main/proto/envoy/service/ext_proc/v3/external_processor.proto index 1c033c08d26..a02779033be 100644 --- a/xds/third_party/envoy/src/main/proto/envoy/service/ext_proc/v3/external_processor.proto +++ b/xds/third_party/envoy/src/main/proto/envoy/service/ext_proc/v3/external_processor.proto @@ -23,37 +23,31 @@ option (udpa.annotations.file_status).package_version_status = ACTIVE; // [#protodoc-title: External processing service] -// A service that can access and modify HTTP requests and responses -// as part of a filter chain. +// A service that can access and modify HTTP requests and responses as part of a filter chain. // The overall external processing protocol works like this: // // 1. The data plane sends to the service information about the HTTP request. -// 2. The service sends back a ProcessingResponse message that directs -// the data plane to either stop processing, continue without it, or send -// it the next chunk of the message body. -// 3. If so requested, the data plane sends the server the message body in -// chunks, or the entire body at once. In either case, the server may send -// back a ProcessingResponse for each message it receives, or wait for -// a certain amount of body chunks received before streaming back the -// ProcessingResponse messages. -// 4. If so requested, the data plane sends the server the HTTP trailers, -// and the server sends back a ProcessingResponse. -// 5. At this point, request processing is done, and we pick up again -// at step 1 when the data plane receives a response from the upstream -// server. -// 6. At any point above, if the server closes the gRPC stream cleanly, -// then the data plane proceeds without consulting the server. -// 7. At any point above, if the server closes the gRPC stream with an error, -// then the data plane returns a 500 error to the client, unless the filter -// was configured to ignore errors. +// 2. The service sends back a ``ProcessingResponse`` message that directs the data plane to either +// stop processing, continue without it, or send it the next chunk of the message body. +// 3. If so requested, the data plane sends the server the message body in chunks, or the entire +// body at once. In either case, the server may send back a ``ProcessingResponse`` for each +// message it receives, or wait for a certain amount of body chunks to be received before +// streaming back the ``ProcessingResponse`` messages. +// 4. If so requested, the data plane sends the server the HTTP trailers, and the server sends back +// a ``ProcessingResponse``. +// 5. At this point, request processing is done, and we pick up again at step 1 when the data plane +// receives a response from the upstream server. +// 6. At any point above, if the server closes the gRPC stream cleanly, then the data plane +// proceeds without consulting the server. +// 7. At any point above, if the server closes the gRPC stream with an error, then the data plane +// returns a ``500`` error to the client, unless the filter was configured to ignore errors. // -// In other words, the process is a request/response conversation, but -// using a gRPC stream to make it easier for the server to -// maintain state. +// In other words, the process is a request/response conversation, but using a gRPC stream to make +// it easier for the server to maintain state. service ExternalProcessor { // This begins the bidirectional stream that the data plane will use to // give the server control over what the filter does. The actual - // protocol is described by the ProcessingRequest and ProcessingResponse + // protocol is described by the ``ProcessingRequest`` and ``ProcessingResponse`` // messages below. rpc Process(stream ProcessingRequest) returns (stream ProcessingResponse) { } @@ -61,30 +55,98 @@ service ExternalProcessor { // This message specifies the filter protocol configurations which will be sent to the ext_proc // server in a :ref:`ProcessingRequest `. -// If the server does not support these protocol configurations, it may choose to close the gRPC stream. -// If the server supports these protocol configurations, it should respond based on the API specifications. +// If the server does not support these protocol configurations, it may choose to close the gRPC +// stream. If the server supports these protocol configurations, it should respond based on the +// API specifications. message ProtocolConfiguration { - // Specify the filter configuration :ref:`request_body_mode - // ` + // Specifies the filter configuration + // :ref:`request_body_mode `. envoy.extensions.filters.http.ext_proc.v3.ProcessingMode.BodySendMode request_body_mode = 1 [(validate.rules).enum = {defined_only: true}]; - // Specify the filter configuration :ref:`response_body_mode - // ` + // Specifies the filter configuration + // :ref:`response_body_mode `. envoy.extensions.filters.http.ext_proc.v3.ProcessingMode.BodySendMode response_body_mode = 2 [(validate.rules).enum = {defined_only: true}]; - // Specify the filter configuration :ref:`send_body_without_waiting_for_header_response - // ` - // If the client is waiting for a header response from the server, setting ``true`` means the client will send body to the server - // as they arrive. Setting ``false`` means the client will buffer the arrived data and not send it to the server immediately. + // Specifies the filter configuration + // :ref:`send_body_without_waiting_for_header_response `. + // If the client is waiting for a header response from the server, setting to ``true`` means the + // client will send the body to the server as it arrives. Setting to ``false`` means the client + // will buffer the arrived data and not send it to the server immediately. bool send_body_without_waiting_for_header_response = 3; } // This represents the different types of messages that the data plane can send // to an external processing server. -// [#next-free-field: 12] +// [#next-free-field: 14] message ProcessingRequest { + // Initial flow control window sizes for ``FULL_DUPLEX_STREAMED`` and + // ``GRPC`` body send modes. + // + // A sender starts with this amount of flow control window. Whenever + // it sends body data, it must decrement its flow control window by + // the number of bytes that it has sent. When its flow control + // window is less than or equal to the amount of body data it wishes + // to send, it may not send until it receives a window update causing + // its flow control window to be large enough. + // + // However, note that in ``GRPC`` body send mode, whenever the flow + // control window is greater than zero, a sender may send a single + // message, even if the size of that message exceeds the available flow + // control window. At that point, the flow control window will be negative + // and the sender must not send the next message until it becomes positive. + // + // Note that the initial size for the to-sidestream windows are set by + // the sender, not the receiver. This is because each sidestream may be + // routed to a different ext_proc server instance, but there is no + // connection-level handshake to set a default for that server + // instance, so the only alternative here would be to have the + // ext_proc server instance set this on a per-stream basis, which + // would require an additional round-trip and therefore hurt latency. + // This unfortunately means that the ext_proc server instance has a + // bit less control: as soon as it receives these initial values, it can + // immediately send a window update that reduces the window, but it + // must be prepared to handle any data that the sender has already sent. + // The initial sizes for the to-sidestream windows are generally + // expected to be in the range of 32K to 64K. + // + // In ``FULL_DUPLEX_STREAMED`` body send mode, for backward compatibility + // with existing ext_proc servers that do not support flow control, if + // the ext_proc server's first response does not include a window + // update, or if the data plane does not receive the first response + // from the ext_proc server within the configured timeout, then the + // data plane will assume that the ext_proc server does not support + // flow control, and it will proceed without it. + // + // [#not-implemented-hide:] + message FlowControlInit { + // Downstream-to-sidestream initial window size. + int64 initial_window_downstream_to_sidestream = 1; + + // Sidestream-to-upstream initial window size. + int64 initial_window_sidestream_to_upstream = 2; + + // Upstream-to-sidestream initial window size. + int64 initial_window_upstream_to_sidestreama = 3; + + // Sidestream-to-downstream initial window size. + int64 initial_window_sidestream_to_downstream = 4; + } + + // Flow control window update. Values may be positive or negative. The + // sender must immediately add these values to its flow control window, + // which governs how much data can be sent. + // + // [#not-implemented-hide:] + message ClientWindowUpdate { + // Window update for sidestream-to-upstream. + int64 window_increment_sidestream_to_upstream = 1; + + // Window update for sidestream-to-downstream. + int64 window_increment_sidestream_to_downstream = 2; + } + reserved 1; reserved "async_mode"; @@ -93,35 +155,33 @@ message ProcessingRequest { // ones are set for a particular HTTP request/response depend on the // processing mode. oneof request { - option (validate.required) = true; - // Information about the HTTP request headers, as well as peer info and additional // properties. Unless ``observability_mode`` is ``true``, the server must send back a - // HeaderResponse message, an ImmediateResponse message, or close the stream. + // ``HeaderResponse`` message, an ``ImmediateResponse`` message, or close the stream. HttpHeaders request_headers = 2; // Information about the HTTP response headers, as well as peer info and additional // properties. Unless ``observability_mode`` is ``true``, the server must send back a - // HeaderResponse message or close the stream. + // ``HeaderResponse`` message or close the stream. HttpHeaders response_headers = 3; - // A chunk of the HTTP request body. Unless ``observability_mode`` is true, the server must send back - // a BodyResponse message, an ImmediateResponse message, or close the stream. + // A chunk of the HTTP request body. Unless ``observability_mode`` is ``true``, the server must + // send back a ``BodyResponse`` message, an ``ImmediateResponse`` message, or close the stream. HttpBody request_body = 4; - // A chunk of the HTTP response body. Unless ``observability_mode`` is ``true``, the server must send back - // a BodyResponse message or close the stream. + // A chunk of the HTTP response body. Unless ``observability_mode`` is ``true``, the server must + // send back a ``BodyResponse`` message or close the stream. HttpBody response_body = 5; // The HTTP trailers for the request path. Unless ``observability_mode`` is ``true``, the server - // must send back a TrailerResponse message or close the stream. + // must send back a ``TrailerResponse`` message or close the stream. // // This message is only sent if the trailers processing mode is set to ``SEND`` and // the original downstream request has trailers. HttpTrailers request_trailers = 6; // The HTTP trailers for the response path. Unless ``observability_mode`` is ``true``, the server - // must send back a TrailerResponse message or close the stream. + // must send back a ``TrailerResponse`` message or close the stream. // // This message is only sent if the trailers processing mode is set to ``SEND`` and // the original upstream response has trailers. @@ -137,39 +197,75 @@ message ProcessingRequest { // :ref:`attributes ` supported in the data plane. map attributes = 9; - // Specify whether the filter that sent this request is running in :ref:`observability_mode - // ` - // and defaults to false. + // Specifies whether the filter that sent this request is running in + // :ref:`observability_mode `. // - // * A value of ``false`` indicates that the server must respond - // to this message by either sending back a matching ProcessingResponse message, - // or by closing the stream. + // * A value of ``false`` indicates that the server must respond to this message by either + // sending back a matching ``ProcessingResponse`` message, or by closing the stream. // * A value of ``true`` indicates that the server should not respond to this message, as any - // responses will be ignored. However, it may still close the stream to indicate that no more messages - // are needed. + // responses will be ignored. However, it may still close the stream to indicate that no more + // messages are needed. // + // Defaults to ``false``. bool observability_mode = 10; // Specify the filter protocol configurations to be sent to the server. // ``protocol_config`` is only encoded in the first ``ProcessingRequest`` message from the client to the server. ProtocolConfiguration protocol_config = 11; + + // Flow control initialization for ``FULL_DUPLEX_STREAMED`` and + // ``GRPC`` body send modes. + // + // Must be set in the initial message on the stream. Not used in + // subsequent messages. + // + // [#not-implemented-hide:] + FlowControlInit flow_control_init = 12; + + // Flow control updates for ``FULL_DUPLEX_STREAMED`` and ``GRPC`` body + // send modes. + // + // This message may be included in a response message that also + // populates one of the fields in the ``request`` oneof above, or it + // may be sent in a response message that does not set the + // ``request`` oneof. + // + // In ``FULL_DUPLEX_STREAMED`` body send mode, for backward + // compatibility with data planes that do not yet support flow control, + // the data plane must not send a message containing only this field + // (i.e., not setting the ``request`` oneof) unless the ext_proc server + // has sent a window update, thus indicating that it supports flow control. + // + // [#not-implemented-hide:] + ClientWindowUpdate client_window_update = 13; } // This represents the different types of messages the server may send back to the data plane -// when the ``observability_mode`` field in the received ProcessingRequest is set to false. +// when the ``observability_mode`` field in the received ``ProcessingRequest`` is set to ``false``. // // * If the corresponding ``BodySendMode`` in the // :ref:`processing_mode ` -// is not set to ``FULL_DUPLEX_STREAMED``, then for every received ProcessingRequest, -// the server must send back exactly one ProcessingResponse message. +// is not set to ``FULL_DUPLEX_STREAMED``, then for every received ``ProcessingRequest``, +// the server must send back exactly one ``ProcessingResponse`` message. // * If it is set to ``FULL_DUPLEX_STREAMED``, the server must follow the API defined -// for this mode to send the ProcessingResponse messages. -// [#next-free-field: 13] +// for this mode to send the ``ProcessingResponse`` messages. +// [#next-free-field: 14] message ProcessingResponse { + // Flow control window update. Values may be positive or negative. The + // sender must immediately add these values to its flow control window, + // which governs how much data can be sent. + // + // [#not-implemented-hide:] + message ServerWindowUpdate { + // Window update for downstream-to-sidestream. + int64 window_increment_downstream_to_sidestream = 1; + + // Window update for upstream-to-sidestream. + int64 window_increment_upstream_to_sidestream = 2; + } + // The response type that is sent by the server. oneof response { - option (validate.required) = true; - // The server must send back this message in response to a message with the // ``request_headers`` field set. HeadersResponse request_headers = 1; @@ -204,17 +300,19 @@ message ProcessingResponse { ImmediateResponse immediate_response = 7; // The server sends back this message to initiate or continue local response streaming. - // The server must initiate local response streaming with the ``headers_response`` in response to a ProcessingRequest - // with the ``request_headers`` only. - // The server may follow up with multiple messages containing ``body_response``. The server must indicate - // end of stream by setting ``end_of_stream`` to ``true`` in the ``headers_response`` + // The server must initiate local response streaming with the ``headers_response`` in response + // to a ``ProcessingRequest`` with the ``request_headers`` only. + // The server may follow up with multiple messages containing ``body_response``. The server must + // indicate end of stream by setting ``end_of_stream`` to ``true`` in the ``headers_response`` // or ``body_response`` message or by sending a ``trailers_response`` message. - // The client may send a ``request_body`` or ``request_trailers`` to the server depending on configuration. + // The client may send a ``request_body`` or ``request_trailers`` to the server depending on + // configuration. // The streaming local response can only be sent when the ``request_header_mode`` in the filter // :ref:`processing_mode ` - // is set to ``SEND``. The ext_proc server should not send StreamedImmediateResponse if it did not observe request headers, - // as it will result in the race with the upstream server response and reset of the client request. - // Presently only the FULL_DUPLEX_STREAMED or NONE body modes are supported. + // is set to ``SEND``. The ext_proc server should not send ``StreamedImmediateResponse`` if it + // did not observe request headers, as it will result in a race with the upstream server + // response and reset of the client request. + // Presently only the ``FULL_DUPLEX_STREAMED`` or ``NONE`` body modes are supported. StreamedImmediateResponse streamed_immediate_response = 11; } @@ -223,19 +321,17 @@ message ProcessingResponse { // field name(s) of the struct. google.protobuf.Struct dynamic_metadata = 8; - // Override how parts of the HTTP request and response are processed - // for the duration of this particular request/response only. Servers - // may use this to intelligently control how requests are processed - // based on the headers and other metadata that they see. - // This field is only applicable when servers responding to the header requests. - // If it is set in the response to the body or trailer requests, it will be ignored by the data plane. + // Override how parts of the HTTP request and response are processed for the duration of this + // particular request/response only. Servers may use this to intelligently control how requests + // are processed based on the headers and other metadata that they see. + // + // This field is only applicable when servers are responding to the header requests. If it is set + // in the response to the body or trailer requests, it will be ignored by the data plane. // It is also ignored by the data plane when the ext_proc filter config - // :ref:`allow_mode_override - // ` - // is set to false, or - // :ref:`send_body_without_waiting_for_header_response - // ` - // is set to true. + // :ref:`allow_mode_override ` + // is set to ``false``, or + // :ref:`send_body_without_waiting_for_header_response ` + // is set to ``true``. envoy.extensions.filters.http.ext_proc.v3.ProcessingMode mode_override = 9; // [#not-implemented-hide:] @@ -251,70 +347,87 @@ message ProcessingResponse { // client had already sent before it saw the ext_proc stream termination. bool request_drain = 12; - // When ext_proc server receives a request message, in case it needs more - // time to process the message, it sends back a ProcessingResponse message - // with a new timeout value. When the data plane receives this response - // message, it ignores other fields in the response, just stop the original - // timer, which has the timeout value specified in - // :ref:`message_timeout - // ` - // and start a new timer with this ``override_message_timeout`` value and keep the - // data plane ext_proc filter state machine intact. - // Has to be >= 1ms and <= - // :ref:`max_message_timeout ` - // Such message can be sent at most once in a particular data plane ext_proc filter processing state. - // To enable this API, one has to set ``max_message_timeout`` to a number >= 1ms. + // When the ext_proc server receives a request message and needs more time to process it, it + // sends back a ``ProcessingResponse`` message with a new timeout value. When the data plane + // receives this response message, it ignores other fields in the response, stops the original + // timer (which has the timeout value specified in + // :ref:`message_timeout `), + // and starts a new timer with this ``override_message_timeout`` value while keeping the data + // plane ext_proc filter state machine intact. + // + // The value must be >= 1ms and <= + // :ref:`max_message_timeout `. + // Such a message can be sent at most once in a particular data plane ext_proc filter processing + // state. To enable this API, ``max_message_timeout`` must be set to a value >= 1ms. google.protobuf.Duration override_message_timeout = 10; + + // Flow control updates for ``FULL_DUPLEX_STREAMED`` and ``GRPC`` body + // send modes. + // + // This message may be included in a response message that also + // populates one of the fields in the ``response`` oneof above, or it + // may be sent in a response message that does not set the + // ``response`` oneof. + // + // In ``FULL_DUPLEX_STREAMED`` body send mode, for backward + // compatibility with data planes that do not yet support flow control, + // the ext_proc server must not set this field unless the data plane + // sent initial window sizes in its initial message on the stream. + // Conversely, if the data plane did send initial window sizes in its + // initial message on the stream, the ext_proc server must send a + // window update immediately to let the data plane know that it also + // supports flow control. If the ext_proc server is sending a message + // immediately anyway (e.g., for a header or body chunk), it can include + // this field in that same message; otherwise, the ext_proc server must + // send a message containing only this field. + // + // [#not-implemented-hide:] + ServerWindowUpdate server_window_update = 13; } // The following are messages that are sent to the server. -// This message is sent to the external server when the HTTP request and responses +// This message is sent to the external server when the HTTP request and response headers // are first received. message HttpHeaders { - // The HTTP request headers. All header keys will be - // lower-cased, because HTTP header keys are case-insensitive. - // The header value is encoded in the + // The HTTP request headers. All header keys will be lower-cased, because HTTP header keys are + // case-insensitive. The header value is encoded in the // :ref:`raw_value ` field. config.core.v3.HeaderMap headers = 1; // [#not-implemented-hide:] - // This field is deprecated and not implemented. Attributes will be sent in - // the top-level :ref:`attributes ` field. map attributes = 2 [deprecated = true, (envoy.annotations.deprecated_at_minor_version) = "3.0"]; - // If ``true``, then there is no message body associated with this - // request or response. + // If ``true``, then there is no message body associated with this request or response. bool end_of_stream = 3; } -// This message is sent to the external server when the HTTP request and -// response bodies are received. +// This message is sent to the external server when the HTTP request and response bodies are +// received. message HttpBody { - // The contents of the body in the HTTP request/response. Note that in - // streaming mode multiple ``HttpBody`` messages may be sent. + // The contents of the body in the HTTP request/response. Note that in streaming mode multiple + // ``HttpBody`` messages may be sent. // - // In ``GRPC`` body send mode, a separate ``HttpBody`` message will be - // sent for each message in the gRPC stream. + // In ``GRPC`` body send mode, a separate ``HttpBody`` message will be sent for each message in + // the gRPC stream. bytes body = 1; - // If ``true``, this will be the last ``HttpBody`` message that will be sent and no - // trailers will be sent for the current request/response. + // If ``true``, this will be the last ``HttpBody`` message that will be sent and no trailers + // will be sent for the current request/response. bool end_of_stream = 2; - // This field is used in ``GRPC`` body send mode when ``end_of_stream`` is - // true and ``body`` is empty. Those values would normally indicate an - // empty message on the stream with the end-of-stream bit set. - // However, if the half-close happens after the last message on the - // stream was already sent, then this field will be true to indicate an - // end-of-stream with *no* message (as opposed to an empty message). + // This field is used in ``GRPC`` body send mode when ``end_of_stream`` is ``true`` and ``body`` + // is empty. Those values would normally indicate an empty message on the stream with the + // end-of-stream bit set. However, if the half-close happens after the last message on the stream + // was already sent, then this field will be ``true`` to indicate an end-of-stream with *no* + // message (as opposed to an empty message). bool end_of_stream_without_message = 3; - // This field is used in ``GRPC`` body send mode to indicate whether - // the message is compressed. This will never be set to true by gRPC - // but may be set to true by a proxy like Envoy. + // This field is used in ``GRPC`` body send mode to indicate whether the message is compressed. + // This will never be set to ``true`` by gRPC but may be set to ``true`` by a proxy like Envoy. bool grpc_message_compressed = 4; } @@ -352,13 +465,14 @@ message TrailersResponse { HeaderMutation header_mutation = 1; } -// This message is sent by the external server to the data plane after ``HttpHeaders`` -// to initiate local response streaming. The server may follow up with multiple messages containing ``body_response``. -// The server must indicate end of stream by setting ``end_of_stream`` to ``true`` in the ``headers_response`` -// or ``body_response`` message or by sending a ``trailers_response`` message. +// This message is sent by the external server to the data plane after ``HttpHeaders`` to initiate +// local response streaming. The server may follow up with multiple messages containing +// ``body_response``. The server must indicate end of stream by setting ``end_of_stream`` to +// ``true`` in the ``headers_response`` or ``body_response`` message or by sending a +// ``trailers_response`` message. message StreamedImmediateResponse { oneof response { - // Response headers to be sent downstream. The ":status" header must be set. + // Response headers to be sent downstream. The ``:status`` header must be set. HttpHeaders headers_response = 1; // Response body to be sent downstream. @@ -384,7 +498,7 @@ message CommonResponse { // further messages for this request or response even if the processing // mode is configured to do so. // - // When used in response to a request_headers or response_headers message, + // When used in response to a ``request_headers`` or ``response_headers`` message, // this status makes it possible to either completely replace the body // while discarding the original body, or to add a body to a message that // formerly did not have one. @@ -401,23 +515,22 @@ message CommonResponse { ResponseStatus status = 1 [(validate.rules).enum = {defined_only: true}]; // Instructions on how to manipulate the headers. When responding to an - // HttpBody request, header mutations will only take effect if - // the current processing mode for the body is BUFFERED. + // ``HttpBody`` request, header mutations will only take effect if the current processing mode + // for the body is ``BUFFERED``. HeaderMutation header_mutation = 2; - // Replace the body of the last message sent to the remote server on this - // stream. If responding to an HttpBody request, simply replace or clear - // the body chunk that was sent with that request. Body mutations may take - // effect in response either to ``header`` or ``body`` messages. When it is - // in response to ``header`` messages, it only take effect if the + // Replace the body of the last message sent to the remote server on this stream. If responding + // to an ``HttpBody`` request, simply replace or clear the body chunk that was sent with that + // request. Body mutations may take effect in response either to ``header`` or ``body`` messages. + // When it is in response to ``header`` messages, it only takes effect if the // :ref:`status ` - // is set to CONTINUE_AND_REPLACE. + // is set to ``CONTINUE_AND_REPLACE``. BodyMutation body_mutation = 3; // [#not-implemented-hide:] - // Add new trailers to the message. This may be used when responding to either a - // HttpHeaders or HttpBody message, but only if this message is returned - // along with the CONTINUE_AND_REPLACE status. + // Add new trailers to the message. This may be used when responding to either an + // ``HttpHeaders`` or ``HttpBody`` message, but only if this message is returned + // along with the ``CONTINUE_AND_REPLACE`` status. // The header value is encoded in the // :ref:`raw_value ` field. config.core.v3.HeaderMap trailers = 4; @@ -429,34 +542,32 @@ message CommonResponse { bool clear_route_cache = 5; } -// This message causes the filter to attempt to create a locally -// generated response, send it downstream, stop processing -// additional filters, and ignore any additional messages received -// from the remote server for this request or response. If a response -// has already started, then this will either ship the reply directly -// to the downstream codec, or reset the stream. +// This message causes the filter to attempt to create a locally generated response, send it +// downstream, stop processing additional filters, and ignore any additional messages received +// from the remote server for this request or response. If a response has already started, then +// this will either ship the reply directly to the downstream codec, or reset the stream. // [#next-free-field: 6] message ImmediateResponse { // The response code to return. type.v3.HttpStatus status = 1 [(validate.rules).message = {required: true}]; - // Apply changes to the default headers, which will include content-type. + // Apply changes to the default headers, which will include ``content-type``. HeaderMutation headers = 2; // The message body to return with the response which is sent using the - // text/plain content type, or encoded in the grpc-message header. + // ``text/plain`` content type, or encoded in the ``grpc-message`` header. bytes body = 3; // If set, then include a gRPC status trailer. GrpcStatus grpc_status = 4; // A string detailing why this local reply was sent, which may be included - // in log and debug output (e.g. this populates the %RESPONSE_CODE_DETAILS% + // in log and debug output (e.g., this populates the ``%RESPONSE_CODE_DETAILS%`` // command operator field for use in access logging). string details = 5; } -// This message specifies a gRPC status for an ImmediateResponse message. +// This message specifies a gRPC status for an ``ImmediateResponse`` message. message GrpcStatus { // The actual gRPC status. uint32 status = 1; @@ -484,26 +595,24 @@ message StreamedBodyResponse { // a serialized gRPC message to be passed to the upstream/downstream by the data plane. bytes body = 1; - // The server sets this flag to true if it has received a body request with - // :ref:`end_of_stream ` set to true, - // and this is the last chunk of body responses. - // Note that in ``GRPC`` body send mode, this allows the ext_proc - // server to tell the data plane to send a half close after a client - // message, which will result in discarding any other messages sent by - // the client application. + // The server sets this flag to ``true`` if it has received a body request with + // :ref:`end_of_stream ` set to + // ``true``, and this is the last chunk of body responses. + // + // Note that in ``GRPC`` body send mode, this allows the ext_proc server to tell the data plane + // to send a half close after a client message, which will result in discarding any other + // messages sent by the client application. bool end_of_stream = 2; - // This field is used in ``GRPC`` body send mode when ``end_of_stream`` is - // true and ``body`` is empty. Those values would normally indicate an - // empty message on the stream with the end-of-stream bit set. - // However, if the half-close happens after the last message on the - // stream was already sent, then this field will be true to indicate an - // end-of-stream with *no* message (as opposed to an empty message). + // This field is used in ``GRPC`` body send mode when ``end_of_stream`` is ``true`` and ``body`` + // is empty. Those values would normally indicate an empty message on the stream with the + // end-of-stream bit set. However, if the half-close happens after the last message on the stream + // was already sent, then this field will be ``true`` to indicate an end-of-stream with *no* + // message (as opposed to an empty message). bool end_of_stream_without_message = 3; - // This field is used in ``GRPC`` body send mode to indicate whether - // the message is compressed. This will never be set to true by gRPC - // but may be set to true by a proxy like Envoy. + // This field is used in ``GRPC`` body send mode to indicate whether the message is compressed. + // This will never be set to ``true`` by gRPC but may be set to ``true`` by a proxy like Envoy. bool grpc_message_compressed = 4; } @@ -517,11 +626,10 @@ message BodyMutation { // is not set to ``FULL_DUPLEX_STREAMED`` or ``GRPC``. bytes body = 1; - // Clear the corresponding body chunk. - // Should only be used when the corresponding ``BodySendMode`` in the + // Clear the corresponding body chunk. Should only be used when the corresponding + // ``BodySendMode`` in the // :ref:`processing_mode ` // is not set to ``FULL_DUPLEX_STREAMED`` or ``GRPC``. - // Clear the corresponding body chunk. bool clear_body = 2; // Must be used when the corresponding ``BodySendMode`` in the From 4e01052d908dc06cec08d309f1695074533a9d55 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 20 Jul 2026 07:29:57 +0000 Subject: [PATCH 03/13] Make givenObservabilityModeFalse_whenExtProcBusy_thenAppRequestsAreBuffered use response processing mode GRPC so that buffered requests are actually sent to ext_proc instead of directly upstream in this test. --- ...xternalProcessorClientInterceptorTest.java | 92 +++++++++++++++++-- 1 file changed, 84 insertions(+), 8 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index d695f9fc40a..a61caf7f3a1 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -9980,6 +9980,12 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenAppRequestsAreBuffer .build()) .build()) .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) .setObservabilityMode(false) .build(); ConfigOrError configOrError = @@ -10005,6 +10011,22 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getResponseBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); } }).start(); } @@ -10057,11 +10079,22 @@ public boolean isReady() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } })) .build()); @@ -10087,8 +10120,9 @@ public void request(int numMessages) { CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + interceptCall(interceptor, METHOD_BIDI_STREAMING, callOptions, dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.request(1); // Bootstrap request for headers // Wait for activation assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); @@ -10101,19 +10135,61 @@ public void request(int numMessages) { // Sidecar busy -> request(5) should be buffered sidecarReady.set(false); proxyCall.request(5); - assertThat(dataPlaneRequestCount.get()).isEqualTo(0); + assertThat(dataPlaneRequestCount.get()).isEqualTo(1); // Only the initial bootstrap request went through - // Sidecar becomes ready -> buffered requests should be drained + // Sidecar becomes ready -> buffered requests should start draining (pulling next message) sidecarReady.set(true); sidecarListenerRef.get().onReady(); - + long startTime2 = System.currentTimeMillis(); + while (dataPlaneRequestCount.get() < 2 && System.currentTimeMillis() - startTime2 < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + assertThat(dataPlaneRequestCount.get()).isEqualTo(2); + + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + + // Server sends response headers (Dummy) + upstreamResponseObserver.onNext("Dummy for headers"); + + startTime2 = System.currentTimeMillis(); + while (dataPlaneRequestCount.get() < 3 && System.currentTimeMillis() - startTime2 < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + assertThat(dataPlaneRequestCount.get()).isEqualTo(3); + + // Server sends first data message -> pulls next + upstreamResponseObserver.onNext("Msg 1"); + + startTime2 = System.currentTimeMillis(); + while (dataPlaneRequestCount.get() < 4 && System.currentTimeMillis() - startTime2 < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + assertThat(dataPlaneRequestCount.get()).isEqualTo(4); + + // Server sends second data message -> pulls next + upstreamResponseObserver.onNext("Msg 2"); + + startTime2 = System.currentTimeMillis(); while (dataPlaneRequestCount.get() < 5 && System.currentTimeMillis() - startTime2 < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); Thread.sleep(10); } assertThat(dataPlaneRequestCount.get()).isEqualTo(5); + // Server sends third data message -> pulls next (which drains the final pending request) + upstreamResponseObserver.onNext("Msg 3"); + + startTime2 = System.currentTimeMillis(); + while (dataPlaneRequestCount.get() < 6 && System.currentTimeMillis() - startTime2 < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + assertThat(dataPlaneRequestCount.get()).isEqualTo(6); + proxyCall.cancel("Cleanup", null); channelManager.close(); } From f631bd7c29c850fb0746d692e8ae6ec6780b11c6 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 20 Jul 2026 10:06:25 +0000 Subject: [PATCH 04/13] Added the unit test givenResponseBodyModeNone_whenExtProcBusy_thenAppRequestsAreNotBuffered to verify that when response_body_mode is NONE (and observability mode is disabled), request(int) calls are passed upstream immediately without being gated by sidecar readiness or buffered. --- ...xternalProcessorClientInterceptorTest.java | 166 +++++++++++++++++- 1 file changed, 162 insertions(+), 4 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index bf8adfe6cb8..4208dfea656 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -10446,10 +10446,6 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenAppRequestsAreBuffer .build()) .build()) .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) .setProcessingMode(ProcessingMode.newBuilder() .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) @@ -10664,6 +10660,168 @@ public void request(int numMessages) { channelManager.close(); } + @Test + @SuppressWarnings("unchecked") + public void givenResponseBodyModeNone_whenExtProcBusy_thenAppRequestsAreNotBuffered() + throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .setObservabilityMode(false) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + // Sidecar server + final CountDownLatch sidecarActionLatch = new CountDownLatch(1); + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + @SuppressWarnings("unchecked") + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + new Thread(() -> { + if (request.hasRequestHeaders()) { + sidecarActionLatch.countDown(); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } + }).start(); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + new Thread(() -> responseObserver.onCompleted()).start(); + } + }; + } + }; + grpcCleanup.register(InProcessServerBuilder.forName(extProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + final AtomicBoolean sidecarReady = new AtomicBoolean(true); + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(extProcServerName) + .directExecutor() + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + super.start(responseListener, headers); + } + + @Override + public boolean isReady() { + return sidecarReady.get(); + } + }; + } + }) + .build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + final AtomicInteger dataPlaneRequestCount = new AtomicInteger(0); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .directExecutor() + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void request(int numMessages) { + dataPlaneRequestCount.addAndGet(numMessages); + super.request(numMessages); + } + }; + } + }) + .build()); + + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, callOptions, dataPlaneChannel); + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.request(1); // Bootstrap request + + // Wait for activation + assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); + for (int i = 0; i < 50 && !proxyCall.isReady(); i++) { + fakeClock.forwardTime(100, TimeUnit.MILLISECONDS); + Thread.sleep(10); + } + assertThat(proxyCall.isReady()).isTrue(); + + // Sidecar busy + sidecarReady.set(false); + + // Since responseBodyMode is NONE and not in observabilityMode, request(5) should be passed upstream immediately + proxyCall.request(5); + + long startTime = System.currentTimeMillis(); + while (dataPlaneRequestCount.get() < 6 && System.currentTimeMillis() - startTime < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + assertThat(dataPlaneRequestCount.get()).isEqualTo(6); // 1 bootstrap + 5 requested + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + // --- Category 21: Streaming Completeness (Client & Bi-Di) --- @Test From 98e258a393c03d82a45e830ec4c51144d090dc30 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 20 Jul 2026 12:13:33 +0000 Subject: [PATCH 05/13] Reordered flow control tests. --- ...xternalProcessorClientInterceptorTest.java | 7388 ++++++++--------- 1 file changed, 3694 insertions(+), 3694 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index 4208dfea656..864850e91e1 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -10822,22 +10822,22 @@ public void request(int numMessages) { channelManager.close(); } - // --- Category 21: Streaming Completeness (Client & Bi-Di) --- - @Test - @SuppressWarnings({"unchecked", "FutureReturnValueIgnored"}) - public void givenClientStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveMutatedData() - throws Exception { - String uniqueExtProcServerName = - "extProc-client-stream-" + InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = - "dataPlane-client-stream-" + InProcessServerBuilder.generateName(); - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + @SuppressWarnings("unchecked") + public void testFlowControlStateInitialization() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); @@ -10846,291 +10846,115 @@ public void givenClientStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveM assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final Metadata.Key reqKey = - Metadata.Key.of("req-mutated", Metadata.ASCII_STRING_MARSHALLER); + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(2); - final List receivedPhases = Collections.synchronizedList(new ArrayList<>()); - final CountDownLatch sidecarActionLatch = new CountDownLatch(5); - final ExecutorService sidecarResponseExecutor = Executors.newSingleThreadExecutor(); - // External Processor Server - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - return new StreamObserver() { + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override - public void onNext(ProcessingRequest request) { - sidecarResponseExecutor.submit(() -> { - synchronized (responseObserver) { - ProcessingResponse.Builder resp = ProcessingResponse.newBuilder(); + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + extProcLatch.countDown(); if (request.hasRequestHeaders()) { - receivedPhases.add("REQ_HEADERS"); - resp.setRequestHeaders( - HeadersResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setHeaderMutation( - HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("req-mutated") - .setValue("true") - .build()) - .build()) - .build()) - .build()) - .build()); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); } else if (request.hasRequestBody()) { - if (request.getRequestBody().getEndOfStreamWithoutMessage()) { - receivedPhases.add("REQ_BODY_EOS"); - resp.setRequestBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse.newBuilder() - .setEndOfStream(true) - .setEndOfStreamWithoutMessage(true) - .build()) - .build()) - .build()) - .build()); - } else if (request.getRequestBody().getEndOfStream()) { - receivedPhases.add("REQ_BODY_MSG"); - receivedPhases.add("REQ_BODY_EOS"); - resp.setRequestBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8( - "MutatedRequest")) - .setEndOfStream(true) - .build()) - .build()) - .build()) - .build()); - } else { - receivedPhases.add("REQ_BODY_MSG"); - resp.setRequestBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8( - "MutatedRequest")) - .build()) - .build()) - .build()) - .build()); - } - } else if (request.hasResponseHeaders()) { - receivedPhases.add("RESP_HEADERS"); - resp.setResponseHeaders(HeadersResponse.newBuilder().build()); - } else if (request.hasResponseBody()) { - receivedPhases.add("RESP_BODY"); - resp.setResponseBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getResponseBody().getBody()) + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) .build()) .build()) .build()) .build()); - } else if (request.hasResponseTrailers()) { - receivedPhases.add("RESP_TRAILERS"); - resp.setResponseTrailers(TrailersResponse.newBuilder().build()); - responseObserver.onNext(resp.build()); - responseObserver.onCompleted(); - sidecarActionLatch.countDown(); - return; } - responseObserver.onNext(resp.build()); - sidecarActionLatch.countDown(); } - }); - } - @Override - public void onError(Throwable t) { - } + @Override + public void onError(Throwable t) {} - @Override - public void onCompleted() { - responseObserver.onCompleted(); + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; } }; - } - }; - final ExecutorService testExecutor = Executors.newFixedThreadPool(20); - final ExecutorService sidecarExecutor = Executors.newSingleThreadExecutor(); - grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(extProcImpl).executor(sidecarExecutor).build().start()); - - // Data Plane Server (Client Streaming) - final AtomicReference serverReceivedHeaders = new AtomicReference<>(); - final AtomicReference serverReceivedBody = new AtomicReference<>(); - MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); - uniqueRegistry.addService(ServerInterceptors.intercept( - ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( - new ServerCalls.ClientStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - return new StreamObserver() { - @Override - public void onNext(String value) { - serverReceivedBody.set(value); - } - - @Override - public void onError(Throwable t) { - } - @Override - public void onCompleted() { - responseObserver.onNext("Ack"); - responseObserver.onCompleted(); - } - }; - } - })) - .build(), - new ServerInterceptor() { - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - serverReceivedHeaders.set(headers); - return next.startCall(call, headers); - } - })); - grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) - .fallbackHandlerRegistry(uniqueRegistry) - .executor(testExecutor) + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() .build().start()); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .executor(testExecutor) - .build()); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(testExecutor) - .build()); + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); }); - ScheduledExecutorService sidecarRealScheduler = Executors.newSingleThreadScheduledExecutor(); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, sidecarRealScheduler, FAKE_CONTEXT); + filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final CountDownLatch finishLatch = new CountDownLatch(1); - final AtomicReference headersFromInterceptor = new AtomicReference<>(); - Channel interceptingChannel = - io.grpc.ClientInterceptors.intercept( - dataPlaneChannel, - new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, CallOptions callOptions, Channel next) { - return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void start(Listener responseListener, Metadata headers) { - super.start( - new io.grpc.ForwardingClientCallListener - .SimpleForwardingClientCallListener(responseListener) { - @Override - public void onHeaders(Metadata headers) { - headersFromInterceptor.set(headers); - super.onHeaders(headers); - } - }, headers); - } - }; - } - }); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + })) + .build()); - final AtomicReference clientReceivedBody = new AtomicReference<>(); - StreamObserver requestObserver = ClientCalls.asyncClientStreamingCall( - interceptCall(interceptor, - METHOD_CLIENT_STREAMING, - DEFAULT_CALL_OPTIONS.withExecutor(testExecutor), - interceptingChannel), - new StreamObserver() { - @Override - public void onNext(String value) { - clientReceivedBody.set(value); - } + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - @Override - public void onError(Throwable t) { - finishLatch.countDown(); - } + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - @Override - public void onCompleted() { - finishLatch.countDown(); - } - }); + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.sendMessage("Message 1"); - requestObserver.onNext("OriginalRequest"); - requestObserver.onCompleted(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); - if (!sidecarActionLatch.await(10, TimeUnit.SECONDS)) { - throw new AssertionError("Sidecar actions failed. Received: " + receivedPhases); - } - assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - List expectedPhases = - Arrays.asList( - "REQ_HEADERS", - "REQ_BODY_MSG", - "REQ_BODY_EOS", - "RESP_HEADERS", - "RESP_BODY", - "RESP_TRAILERS"); - assertThat(receivedPhases).containsExactlyElementsIn(expectedPhases).inOrder(); + assertThat(receivedRequests).hasSize(2); + ProcessingRequest firstRequest = receivedRequests.get(0); + ProcessingRequest secondRequest = receivedRequests.get(1); - assertThat(serverReceivedHeaders.get().get(reqKey)).isEqualTo("true"); - assertThat(serverReceivedBody.get()).isEqualTo("MutatedRequest"); - assertThat(clientReceivedBody.get()).isEqualTo("Ack"); + assertThat(firstRequest.hasRequestHeaders()).isTrue(); + assertThat(firstRequest.hasFlowControlInit()).isTrue(); + assertThat(firstRequest.getFlowControlInit().getInitialWindowDownstreamToSidestream()).isEqualTo(65536); + assertThat(firstRequest.getFlowControlInit().getInitialWindowSidestreamToUpstream()).isEqualTo(65536); - sidecarRealScheduler.shutdown(); - sidecarResponseExecutor.shutdown(); - testExecutor.shutdown(); - sidecarExecutor.shutdown(); + assertThat(secondRequest.hasRequestBody()).isTrue(); + assertThat(secondRequest.hasFlowControlInit()).isFalse(); + + proxyCall.cancel("Cleanup", null); channelManager.close(); } @Test - @SuppressWarnings({"unchecked", "FutureReturnValueIgnored"}) - public void givenBidiStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveMutatedData() - throws Exception { - String uniqueExtProcServerName = - "extProc-bidi-stream-" + InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = - "dataPlane-bidi-stream-" + InProcessServerBuilder.generateName(); - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + @SuppressWarnings("unchecked") + public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); @@ -11139,1076 +10963,1503 @@ public void givenBidiStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveMut assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final Metadata.Key reqKey = - Metadata.Key.of("req-mutated", Metadata.ASCII_STRING_MARSHALLER); + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch firstBodyLatch = new CountDownLatch(2); // Headers + First Body + final CountDownLatch secondBodyLatch = new CountDownLatch(1); + final AtomicReference> responseObserverRef = new AtomicReference<>(); - final List receivedPhases = Collections.synchronizedList(new ArrayList<>()); - final CountDownLatch sidecarBidiLatch = new CountDownLatch(5); - final ExecutorService bidiSidecarResponseExecutor = Executors.newSingleThreadExecutor(); - // External Processor Server - ExternalProcessorGrpc.ExternalProcessorImplBase bidiExtProcImpl; - bidiExtProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - return new StreamObserver() { + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override - public void onNext(ProcessingRequest request) { - bidiSidecarResponseExecutor.submit(() -> { - synchronized (responseObserver) { - ProcessingResponse.Builder resp = ProcessingResponse.newBuilder(); + public StreamObserver process( + final StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); if (request.hasRequestHeaders()) { - receivedPhases.add("REQ_HEADERS"); - resp.setRequestHeaders( - HeadersResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setHeaderMutation( - HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("req-mutated") - .setValue("true") - .build()) - .build()) - .build()) - .build()) - .build()); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + firstBodyLatch.countDown(); } else if (request.hasRequestBody()) { - if (request.getRequestBody().getEndOfStreamWithoutMessage()) { - receivedPhases.add("REQ_BODY_EOS"); - resp.setRequestBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse.newBuilder() - .setEndOfStream(true) - .setEndOfStreamWithoutMessage(true) - .build()) - .build()) - .build()) - .build()); - } else if (request.getRequestBody().getEndOfStream()) { - receivedPhases.add("REQ_BODY_MSG"); - receivedPhases.add("REQ_BODY_EOS"); - resp.setRequestBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8( - "MutatedBidiReq")) - .setEndOfStream(true) - .build()) - .build()) + if (firstBodyLatch.getCount() > 0) { + // This is the first body (40000 bytes) + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) .build()) - .build()); + .build()) + .build()) + .build()); + firstBodyLatch.countDown(); } else { - receivedPhases.add("REQ_BODY_MSG"); - resp.setRequestBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8( - "MutatedBidiReq")) - .build()) - .build()) + // This is the second body (30000 bytes) + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) .build()) - .build()); + .build()) + .build()) + .build()); + secondBodyLatch.countDown(); } - } else if (request.hasResponseHeaders()) { - receivedPhases.add("RESP_HEADERS"); - resp.setResponseHeaders(HeadersResponse.newBuilder().build()); - } else if (request.hasResponseBody()) { - receivedPhases.add("RESP_BODY"); - resp.setResponseBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse.newBuilder() - .setBody(request.getResponseBody().getBody()) - .build()) - .build()) - .build()) - .build()); - } else if (request.hasResponseTrailers()) { - receivedPhases.add("RESP_TRAILERS"); - resp.setResponseTrailers(TrailersResponse.newBuilder().build()); - responseObserver.onNext(resp.build()); - responseObserver.onCompleted(); - sidecarBidiLatch.countDown(); - return; } - responseObserver.onNext(resp.build()); - sidecarBidiLatch.countDown(); } - }); - } - @Override - public void onError(Throwable t) { + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; } + }; - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; - } - }; - final ExecutorService bidiTestExecutor = Executors.newFixedThreadPool(20); - final ExecutorService sidecarExecutor = Executors.newSingleThreadExecutor(); - grpcCleanup.register( - InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(bidiExtProcImpl) - .executor(sidecarExecutor) - .build() - .start()); - - // Data Plane Server (Bidi) - final AtomicReference serverReceivedHeaders = new AtomicReference<>(); - MutableHandlerRegistry uniqueBidiRegistry = new MutableHandlerRegistry(); - uniqueBidiRegistry.addService(ServerInterceptors.intercept( - ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( - new ServerCalls.BidiStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - return new StreamObserver() { - @Override - public void onNext(String value) { - responseObserver.onNext(value + "Echo"); - } - - @Override - public void onError(Throwable t) { - } - - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; - } - })) - .build(), - new ServerInterceptor() { - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - serverReceivedHeaders.set(headers); - return next.startCall(call, headers); - } - })); - grpcCleanup.register( - InProcessServerBuilder.forName(uniqueDataPlaneServerName) - .fallbackHandlerRegistry(uniqueBidiRegistry) - .executor(bidiTestExecutor) - .build() - .start()); + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .executor(bidiTestExecutor) - .build()); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(bidiTestExecutor) - .build()); + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); }); - ScheduledExecutorService bidiRealScheduler = Executors.newSingleThreadScheduledExecutor(); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, bidiRealScheduler, FAKE_CONTEXT); - final AtomicReference clientReceivedBody = new AtomicReference<>(); - final CountDownLatch finishLatch = new CountDownLatch(1); - final AtomicReference bidiHeadersFromInterceptor = new AtomicReference<>(); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); - Channel bidiInterceptingChannel = - io.grpc.ClientInterceptors.intercept( - dataPlaneChannel, - new ClientInterceptor() { + final List dataPlaneReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { @Override - public ClientCall interceptCall( - MethodDescriptor method, CallOptions callOptions, Channel next) { - return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { @Override - public void start(Listener responseListener, Metadata headers) { - super.start( - new io.grpc.ForwardingClientCallListener - .SimpleForwardingClientCallListener(responseListener) { - @Override - public void onHeaders(Metadata headers) { - bidiHeadersFromInterceptor.set(headers); - super.onHeaders(headers); - } - }, headers); + public void onNext(String value) { + dataPlaneReceivedMessages.add(value); + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); } }; } - }); + })) + .build()); - StreamObserver bidiRequestObserver = ClientCalls.asyncBidiStreamingCall( - interceptCall(interceptor, - METHOD_BIDI_STREAMING, - DEFAULT_CALL_OPTIONS.withExecutor(bidiTestExecutor), - bidiInterceptingChannel), - new StreamObserver() { - @Override - public void onNext(String value) { - clientReceivedBody.set(value); - } + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - @Override - public void onError(Throwable t) { - finishLatch.countDown(); - } + ClientCall proxyCall = + interceptCall(interceptor, METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - @Override - public void onCompleted() { - finishLatch.countDown(); - } - }); + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - bidiRequestObserver.onNext("Bidi"); - bidiRequestObserver.onCompleted(); + // Generate large messages + String largeMessage40k = new String(new char[40000]).replace('\0', 'a'); + String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); - if (!sidecarBidiLatch.await(10, TimeUnit.SECONDS)) { - throw new AssertionError("Sidecar bidi actions failed. Received: " + receivedPhases); - } - assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - List expectedPhases = - Arrays.asList( - "REQ_HEADERS", - "REQ_BODY_MSG", - "REQ_BODY_EOS", - "RESP_HEADERS", - "RESP_BODY", - "RESP_TRAILERS"); - assertThat(receivedPhases).containsExactlyElementsIn(expectedPhases).inOrder(); + // Send first message (40000 bytes) - fits in 65536 window + proxyCall.sendMessage(largeMessage40k); + assertThat(firstBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(proxyCall.isReady()).isTrue(); - assertThat(serverReceivedHeaders.get().get(reqKey)).isEqualTo("true"); - assertThat(clientReceivedBody.get()).isEqualTo("MutatedBidiReqEcho"); + // Send second message (30000 bytes) - total 70000 > 65536, should buffer + proxyCall.sendMessage(largeMessage30k); - bidiRealScheduler.shutdown(); - bidiSidecarResponseExecutor.shutdown(); - bidiTestExecutor.shutdown(); - sidecarExecutor.shutdown(); - channelManager.close(); - } + // Wait a brief moment to confirm it is NOT delivered to ext_proc (latch shouldn't tick) + assertThat(receivedRequests).hasSize(3); // Headers + First Body + Client Window Update (Path 2 replenishment) + assertThat(proxyCall.isReady()).isFalse(); - // --- Category 22: Header Forwarding --- + // Now send ServerWindowUpdate from ext_proc to interceptor to increment window by 40000 + responseObserverRef.get().onNext(ProcessingResponse.newBuilder() + .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder() + .setWindowIncrementDownstreamToSidestream(40000) + .build()) + .build()); - @Test - public void - givenAllowedHeaders_whenRequestHeadersForwarded_thenOnlyAllowedAreSent() - throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + // The second body should now be flushed and received by ext_proc + assertThat(secondBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(receivedRequests.size()).isAtLeast(4); - final AtomicReference - capturedHeaders = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + assertThat(receivedRequests.get(0).hasRequestHeaders()).isTrue(); + assertThat(receivedRequests.get(1).hasRequestBody()).isTrue(); + assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); + assertThat(receivedRequests.get(3).hasRequestBody()).isTrue(); + assertThat(receivedRequests.get(3).getRequestBody().getBody().size()).isEqualTo(30000); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - capturedHeaders.set(request.getRequestHeaders()); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - } - } + assertThat(proxyCall.isReady()).isTrue(); - @Override - public void onError(Throwable t) { - } + // Verify both messages reached the backend service + assertThat(dataPlaneReceivedMessages).containsExactly(largeMessage40k, largeMessage30k).inOrder(); - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; - } - }; - grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(extProcImpl) - .executor(Executors.newSingleThreadExecutor()) - .build().start()); + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } - // Config with forward_rules: allowed_headers = ["x-allowed-*", "content-type"] - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setForwardRules(HeaderForwardingRules.newBuilder() - .setAllowedHeaders( - io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() - .addPatterns( - io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() - .setPrefix("x-allowed-") - .build()) - .addPatterns( - io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() - .setExact("content-type") - .build()) - .build()) - .build()) + @Test + @SuppressWarnings("unchecked") + public void testUpstreamToSidestreamFlowControl_EnforcesWindow() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(4); // Headers, Request Body, Response Headers, Response Body 1 + final CountDownLatch secondResponseBodyLatch = new CountDownLatch(1); + final AtomicReference> responseObserverRef = new AtomicReference<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + extProcLatch.countDown(); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getResponseBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + if (request.getResponseBody().getBody().size() == 30000) { + secondResponseBodyLatch.countDown(); + } + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); }); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod( - METHOD_SAY_HELLO, - ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })) + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) .build()); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); - Metadata headers = new Metadata(); - headers.put( - Metadata.Key.of("x-allowed-1", Metadata.ASCII_STRING_MARSHALLER), "v1"); - headers.put( - Metadata.Key.of("x-disallowed", Metadata.ASCII_STRING_MARSHALLER), "v2"); - headers.put( - Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER), "application/grpc"); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - final CountDownLatch appCloseLatch = new CountDownLatch(1); - ClientCall proxyCall = - interceptCall(interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), - dataPlaneChannel); - proxyCall.start(new ClientCall.Listener() { + final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch messagesLatch2 = new CountDownLatch(2); + final CountDownLatch messagesLatch3 = new CountDownLatch(3); + ClientCall.Listener appListener = new ClientCall.Listener() { @Override - public void onClose(Status status, Metadata trailers) { - appCloseLatch.countDown(); + public void onMessage(String message) { + appReceivedMessages.add(message); + messagesLatch2.countDown(); + messagesLatch3.countDown(); } - }, headers); + }; - proxyCall.request(1); - proxyCall.sendMessage("test"); - proxyCall.halfClose(); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - List headerNames = new ArrayList<>(); - for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : - capturedHeaders.get().getHeaders().getHeadersList()) { - headerNames.add(hv.getKey()); - } - assertThat(headerNames).contains("x-allowed-1"); - assertThat(headerNames).contains("content-type"); - assertThat(headerNames).doesNotContain("x-disallowed"); - - channelManager.close(); - } + proxyCall.start(appListener, new Metadata()); + proxyCall.request(10); - @Test - public void - givenAllowedHeaders_whenResponseHeadersForwarded_thenOnlyAllowedAreSent() - throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + proxyCall.sendMessage("Client Msg"); - final AtomicReference - capturedHeaders = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Dummy for headers"); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - } else if (request.hasResponseHeaders()) { - capturedHeaders.set(request.getResponseHeaders()); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - } - } + String largeMessage40k = new String(new char[40000]).replace('\0', 'a'); + String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); - @Override - public void onError(Throwable t) { - } + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; - } - }; - grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(extProcImpl) - .executor(Executors.newSingleThreadExecutor()) - .build().start()); + upstreamResponseObserver.onNext(largeMessage40k); - // Config with forward_rules: allowed_headers = ["x-allowed-*", "content-type"] - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setProcessingMode(ProcessingMode.newBuilder() - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .build()) - .setForwardRules(HeaderForwardingRules.newBuilder() - .setAllowedHeaders( - io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() - .addPatterns( - io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() - .setPrefix("x-allowed-") - .build()) - .addPatterns( - io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() - .setExact("content-type") - .build()) - .build()) - .build()) - .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; + assertThat(messagesLatch2.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appReceivedMessages).hasSize(2); - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); - }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); + upstreamResponseObserver.onNext(largeMessage30k); - dataPlaneServiceRegistry.addService(ServerInterceptors.intercept( - ServerServiceDefinition.builder("test.TestService") - .addMethod( - METHOD_SAY_HELLO, - ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })) - .build(), - new ServerInterceptor() { - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - Metadata responseHeaders = new Metadata(); - responseHeaders.put( - Metadata.Key.of("x-allowed-response", Metadata.ASCII_STRING_MARSHALLER), "v1"); - responseHeaders.put( - Metadata.Key.of("x-disallowed-response", Metadata.ASCII_STRING_MARSHALLER), "v2"); - responseHeaders.put( - Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER), - "application/grpc"); + Thread.sleep(200); + assertThat(secondResponseBodyLatch.getCount()).isEqualTo(1); + assertThat(appReceivedMessages).hasSize(2); - call.sendHeaders(responseHeaders); - return next.startCall(call, headers); - } - })); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + responseObserverRef.get().onNext(ProcessingResponse.newBuilder() + .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder() + .setWindowIncrementUpstreamToSidestream(40000) + .build()) + .build()); - final CountDownLatch appCloseLatch = new CountDownLatch(1); - ClientCall proxyCall = - interceptCall(interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), - dataPlaneChannel); - proxyCall.start(new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - appCloseLatch.countDown(); - } - }, new Metadata()); + assertThat(secondResponseBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); - proxyCall.request(1); - proxyCall.sendMessage("test"); - proxyCall.halfClose(); + assertThat(messagesLatch3.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appReceivedMessages).hasSize(3); + assertThat(appReceivedMessages.get(2)).isEqualTo(largeMessage30k); + assertThat(receivedRequests).isNotEmpty(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - List headerNames = new ArrayList<>(); - for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : - capturedHeaders.get().getHeaders().getHeadersList()) { - headerNames.add(hv.getKey()); - } - assertThat(headerNames).contains("x-allowed-response"); - assertThat(headerNames).contains("content-type"); - assertThat(headerNames).doesNotContain("x-disallowed-response"); - + proxyCall.cancel("Cleanup", null); channelManager.close(); } @Test - public void givenDisallowedHeaders_whenHeadersForwarded_thenSkipped() throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + @SuppressWarnings("unchecked") + public void testSidestreamToDownstreamFlowControl_Violations() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; - final AtomicReference capturedHeaders = - new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'd'); + final CountDownLatch callClosedLatch = new CountDownLatch(1); + final AtomicReference capturedStatus = new AtomicReference<>(); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - capturedHeaders.set(request.getRequestHeaders()); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - } - } + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseBody()) { + // Respond with a mutated body that is larger than the initial window size of 65536 + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } - @Override - public void onError(Throwable t) { - } + @Override + public void onError(Throwable t) {} - @Override - public void onCompleted() { - responseObserver.onCompleted(); + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; } }; - } - }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .executor(Executors.newSingleThreadExecutor()) + .directExecutor() .build().start()); - // Config with forward_rules: disallowed_headers = ["x-secret", "authorization"] - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setForwardRules(HeaderForwardingRules.newBuilder() - .setDisallowedHeaders( - io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() - .addPatterns( - io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() - .setExact("x-secret") - .build()) - .addPatterns( - io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() - .setExact("authorization") - .build()) - .build()) - .build()) - .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); }); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })).build()); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); - Metadata headers = new Metadata(); - headers.put(Metadata.Key.of("x-foo", Metadata.ASCII_STRING_MARSHALLER), "v1"); - headers.put(Metadata.Key.of("x-secret", Metadata.ASCII_STRING_MARSHALLER), "v2"); - headers.put(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), "v3"); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - final CountDownLatch appCloseLatch = new CountDownLatch(1); ClientCall proxyCall = - interceptCall(interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), - dataPlaneChannel); + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - appCloseLatch.countDown(); + capturedStatus.set(status); + callClosedLatch.countDown(); } - }, headers); + }, new Metadata()); + proxyCall.request(10); - proxyCall.request(1); - proxyCall.sendMessage("test"); - proxyCall.halfClose(); + proxyCall.sendMessage("Client Msg"); + + // Send a response from upstream to trigger headers and then the body response + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Response Msg"); + + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // The call should fail immediately with INTERNAL error code due to flow control violation + assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); + assertThat(capturedStatus.get().getCause()).isInstanceOf(io.grpc.StatusRuntimeException.class); + assertThat(capturedStatus.get().getCause().getMessage()).contains("Flow control violation: received server body from ext_proc when window is closed"); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - List headerNames = new ArrayList<>(); - for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : - capturedHeaders.get().getHeaders().getHeadersList()) { - headerNames.add(hv.getKey()); - } - assertThat(headerNames).contains("x-foo"); - assertThat(headerNames).doesNotContain("x-secret"); - assertThat(headerNames).doesNotContain("authorization"); - channelManager.close(); } @Test - public void givenBothRules_whenHeadersForwarded_thenBothAreApplied() throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + @SuppressWarnings("unchecked") + public void testSidestreamToUpstreamFlowControl_Violations() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; - final AtomicReference capturedHeaders = - new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'c'); + final CountDownLatch callClosedLatch = new CountDownLatch(1); + final AtomicReference capturedStatus = new AtomicReference<>(); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - capturedHeaders.set(request.getRequestHeaders()); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - } - } + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + // Respond with a mutated body that is larger than the initial window size of 65536 + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } - @Override - public void onError(Throwable t) { - } + @Override + public void onError(Throwable t) {} - @Override - public void onCompleted() { - responseObserver.onCompleted(); + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; } }; - } - }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .executor(Executors.newSingleThreadExecutor()) + .directExecutor() .build().start()); - // Config with forward_rules: allowed = ["x-foo-*"], disallowed = ["x-foo-secret"] - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setForwardRules(HeaderForwardingRules.newBuilder() - .setAllowedHeaders( - io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() - .addPatterns( - io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() - .setPrefix("x-foo-") - .build()) - .build()) - .setDisallowedHeaders( - io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() - .addPatterns( - io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() - .setExact("x-foo-secret") - .build()) - .build()) - .build()) - .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); }); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })).build()); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + })) + .build()); - Metadata headers = new Metadata(); - headers.put(Metadata.Key.of("x-foo-1", Metadata.ASCII_STRING_MARSHALLER), "v1"); - headers.put(Metadata.Key.of("x-foo-secret", Metadata.ASCII_STRING_MARSHALLER), "v2"); - headers.put(Metadata.Key.of("x-bar", Metadata.ASCII_STRING_MARSHALLER), "v3"); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - final CountDownLatch appCloseLatch = new CountDownLatch(1); ClientCall proxyCall = - interceptCall(interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), - dataPlaneChannel); + interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - appCloseLatch.countDown(); + capturedStatus.set(status); + callClosedLatch.countDown(); } - }, headers); + }, new Metadata()); - proxyCall.request(1); - proxyCall.sendMessage("test"); - proxyCall.halfClose(); + proxyCall.sendMessage("Message 1"); + + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // The call should fail immediately with INTERNAL error code due to flow control violation + assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); + assertThat(capturedStatus.get().getCause()).isInstanceOf(io.grpc.StatusRuntimeException.class); + assertThat(capturedStatus.get().getCause().getMessage()).contains("Flow control violation: received client body from ext_proc when window is closed"); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - List headerNames = new ArrayList<>(); - for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : - capturedHeaders.get().getHeaders().getHeadersList()) { - headerNames.add(hv.getKey()); - } - assertThat(headerNames).contains("x-foo-1"); - assertThat(headerNames).doesNotContain("x-foo-secret"); - assertThat(headerNames).doesNotContain("x-bar"); - channelManager.close(); } - // --- Category 23: Request Attributes --- - @Test - public void parseFilterConfig_withUnrecognizedRequestAttribute_isIgnored() { - ExternalProcessor proto = createBaseProto(extProcServerName) - .addRequestAttributes("invalid.attribute") + @SuppressWarnings("unchecked") + public void testThresholdBasedWindowUpdates() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) .build(); - ConfigOrError result = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(result.errorDetail).isNull(); - assertThat(result.config.getRequestAttributes()).containsExactly("invalid.attribute"); - } + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; - @Test - public void parseFilterConfig_withRecognizedRequestAttributes_succeeds() { - ExternalProcessor proto = createBaseProto(extProcServerName) - .addRequestAttributes("request.path") - .addRequestAttributes("request.host") - .addRequestAttributes("request.scheme") // Recognized but not set - .build(); - ConfigOrError result = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(result.errorDetail).isNull(); - assertThat(result.config.getRequestAttributes()).containsExactly( - "request.path", "request.host", "request.scheme"); - } + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(1); + final List> observers = new ArrayList<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + observers.add(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(filterClientRequests(receivedRequests)).hasSize(1); + assertThat(filterClientRequests(receivedRequests).get(0).hasRequestHeaders()).isTrue(); + + // 1. Send body message. It should be sent immediately. + proxyCall.sendMessage("Msg 1"); // size = 5 bytes + + assertThat(filterClientRequests(receivedRequests)).hasSize(2); + assertThat(filterClientRequests(receivedRequests).get(1).hasRequestBody()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(1).getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 1"); + // No window updates were accumulated yet. + assertThat(filterClientRequests(receivedRequests).get(1).hasClientWindowUpdate()).isFalse(); + + // 2. Trigger window replenishment below threshold (e.g. 5 bytes from Msg 1 response). + // The interceptor processes the response, forwards it upstream, and increments + // accumulatedWindowUpdateSidestreamToUpstream. Since 5 < 32768, it won't send standalone updates. + // We send another message "Msg 2" to trigger piggybacking. + proxyCall.sendMessage("Msg 2"); + + assertThat(filterClientRequests(receivedRequests)).hasSize(3); + assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 2"); + // Verify accumulated 5 bytes update is piggybacked. + assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(5); + + // 3. Accumulate past threshold (e.g. 35,000 bytes) without sending body messages. + // This should trigger an immediate standalone ClientWindowUpdate. + StreamObserver responseObserver = observers.get(0); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFrom(new byte[35000])) + .build()) + .build()) + .build()) + .build()) + .build()); + + // standalone client window update received. + assertThat(filterClientRequests(receivedRequests)).hasSize(4); + assertThat(filterClientRequests(receivedRequests).get(3).hasClientWindowUpdate()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(3).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(35005); + assertThat(filterClientRequests(receivedRequests).get(3).hasRequestBody()).isFalse(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } @Test - public void givenRequestAttributes_whenHeaderPhase_thenAttributesSent() throws Exception { + @SuppressWarnings("unchecked") + public void testHalfClosePiggybacking() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(filterClientRequests(receivedRequests)).hasSize(1); + + proxyCall.sendMessage("Last Message"); + + + + // Verify 12 bytes are accumulated but no standalone update is sent. + assertThat(filterClientRequests(receivedRequests)).hasSize(2); + + proxyCall.halfClose(); + + // Verify halfClose sends EOF request piggybacking the accumulated 12 bytes update. + assertThat(filterClientRequests(receivedRequests)).hasSize(3); + assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).getRequestBody().getEndOfStreamWithoutMessage()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(12); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + // --- Category 21: Streaming Completeness (Client & Bi-Di) --- + + @Test + @SuppressWarnings({"unchecked", "FutureReturnValueIgnored"}) + public void givenClientStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveMutatedData() + throws Exception { + String uniqueExtProcServerName = + "extProc-client-stream-" + InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = + "dataPlane-client-stream-" + InProcessServerBuilder.generateName(); + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final Metadata.Key reqKey = + Metadata.Key.of("req-mutated", Metadata.ASCII_STRING_MARSHALLER); + + final List receivedPhases = Collections.synchronizedList(new ArrayList<>()); + final CountDownLatch sidecarActionLatch = new CountDownLatch(5); + final ExecutorService sidecarResponseExecutor = Executors.newSingleThreadExecutor(); + // External Processor Server + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + sidecarResponseExecutor.submit(() -> { + synchronized (responseObserver) { + ProcessingResponse.Builder resp = ProcessingResponse.newBuilder(); + if (request.hasRequestHeaders()) { + receivedPhases.add("REQ_HEADERS"); + resp.setRequestHeaders( + HeadersResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setHeaderMutation( + HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("req-mutated") + .setValue("true") + .build()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasRequestBody()) { + if (request.getRequestBody().getEndOfStreamWithoutMessage()) { + receivedPhases.add("REQ_BODY_EOS"); + resp.setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setEndOfStream(true) + .setEndOfStreamWithoutMessage(true) + .build()) + .build()) + .build()) + .build()); + } else if (request.getRequestBody().getEndOfStream()) { + receivedPhases.add("REQ_BODY_MSG"); + receivedPhases.add("REQ_BODY_EOS"); + resp.setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8( + "MutatedRequest")) + .setEndOfStream(true) + .build()) + .build()) + .build()) + .build()); + } else { + receivedPhases.add("REQ_BODY_MSG"); + resp.setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8( + "MutatedRequest")) + .build()) + .build()) + .build()) + .build()); + } + } else if (request.hasResponseHeaders()) { + receivedPhases.add("RESP_HEADERS"); + resp.setResponseHeaders(HeadersResponse.newBuilder().build()); + } else if (request.hasResponseBody()) { + receivedPhases.add("RESP_BODY"); + resp.setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getResponseBody().getBody()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasResponseTrailers()) { + receivedPhases.add("RESP_TRAILERS"); + resp.setResponseTrailers(TrailersResponse.newBuilder().build()); + responseObserver.onNext(resp.build()); + responseObserver.onCompleted(); + sidecarActionLatch.countDown(); + return; + } + responseObserver.onNext(resp.build()); + sidecarActionLatch.countDown(); + } + }); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + final ExecutorService testExecutor = Executors.newFixedThreadPool(20); + final ExecutorService sidecarExecutor = Executors.newSingleThreadExecutor(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl).executor(sidecarExecutor).build().start()); + + // Data Plane Server (Client Streaming) + final AtomicReference serverReceivedHeaders = new AtomicReference<>(); + final AtomicReference serverReceivedBody = new AtomicReference<>(); + MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); + uniqueRegistry.addService(ServerInterceptors.intercept( + ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) { + serverReceivedBody.set(value); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + responseObserver.onNext("Ack"); + responseObserver.onCompleted(); + } + }; + } + })) + .build(), + new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + serverReceivedHeaders.set(headers); + return next.startCall(call, headers); + } + })); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) + .fallbackHandlerRegistry(uniqueRegistry) + .executor(testExecutor) + .build().start()); + + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) + .executor(testExecutor) + .build()); + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(testExecutor) + .build()); + }); + ScheduledExecutorService sidecarRealScheduler = Executors.newSingleThreadScheduledExecutor(); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, sidecarRealScheduler, FAKE_CONTEXT); + + final CountDownLatch finishLatch = new CountDownLatch(1); + final AtomicReference headersFromInterceptor = new AtomicReference<>(); + Channel interceptingChannel = + io.grpc.ClientInterceptors.intercept( + dataPlaneChannel, + new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + super.start( + new io.grpc.ForwardingClientCallListener + .SimpleForwardingClientCallListener(responseListener) { + @Override + public void onHeaders(Metadata headers) { + headersFromInterceptor.set(headers); + super.onHeaders(headers); + } + }, headers); + } + }; + } + }); + + final AtomicReference clientReceivedBody = new AtomicReference<>(); + StreamObserver requestObserver = ClientCalls.asyncClientStreamingCall( + interceptCall(interceptor, + METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(testExecutor), + interceptingChannel), + new StreamObserver() { + @Override + public void onNext(String value) { + clientReceivedBody.set(value); + } + + @Override + public void onError(Throwable t) { + finishLatch.countDown(); + } + + @Override + public void onCompleted() { + finishLatch.countDown(); + } + }); + + requestObserver.onNext("OriginalRequest"); + requestObserver.onCompleted(); + + if (!sidecarActionLatch.await(10, TimeUnit.SECONDS)) { + throw new AssertionError("Sidecar actions failed. Received: " + receivedPhases); + } + assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + List expectedPhases = + Arrays.asList( + "REQ_HEADERS", + "REQ_BODY_MSG", + "REQ_BODY_EOS", + "RESP_HEADERS", + "RESP_BODY", + "RESP_TRAILERS"); + assertThat(receivedPhases).containsExactlyElementsIn(expectedPhases).inOrder(); + + assertThat(serverReceivedHeaders.get().get(reqKey)).isEqualTo("true"); + assertThat(serverReceivedBody.get()).isEqualTo("MutatedRequest"); + assertThat(clientReceivedBody.get()).isEqualTo("Ack"); + + sidecarRealScheduler.shutdown(); + sidecarResponseExecutor.shutdown(); + testExecutor.shutdown(); + sidecarExecutor.shutdown(); + channelManager.close(); + } + @Test + @SuppressWarnings({"unchecked", "FutureReturnValueIgnored"}) + public void givenBidiStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveMutatedData() + throws Exception { + String uniqueExtProcServerName = + "extProc-bidi-stream-" + InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = + "dataPlane-bidi-stream-" + InProcessServerBuilder.generateName(); ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .addRequestAttributes("request.path") - .addRequestAttributes("request.url_path") - .addRequestAttributes("request.host") - .addRequestAttributes("request.method") - .addRequestAttributes("request.query") + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; - final AtomicReference capturedRequest = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); - final CountDownLatch callLatch = new CountDownLatch(1); + final Metadata.Key reqKey = + Metadata.Key.of("req-mutated", Metadata.ASCII_STRING_MARSHALLER); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + final List receivedPhases = Collections.synchronizedList(new ArrayList<>()); + final CountDownLatch sidecarBidiLatch = new CountDownLatch(5); + final ExecutorService bidiSidecarResponseExecutor = Executors.newSingleThreadExecutor(); + // External Processor Server + ExternalProcessorGrpc.ExternalProcessorImplBase bidiExtProcImpl; + bidiExtProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - capturedRequest.set(request); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - } + bidiSidecarResponseExecutor.submit(() -> { + synchronized (responseObserver) { + ProcessingResponse.Builder resp = ProcessingResponse.newBuilder(); + if (request.hasRequestHeaders()) { + receivedPhases.add("REQ_HEADERS"); + resp.setRequestHeaders( + HeadersResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setHeaderMutation( + HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("req-mutated") + .setValue("true") + .build()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasRequestBody()) { + if (request.getRequestBody().getEndOfStreamWithoutMessage()) { + receivedPhases.add("REQ_BODY_EOS"); + resp.setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setEndOfStream(true) + .setEndOfStreamWithoutMessage(true) + .build()) + .build()) + .build()) + .build()); + } else if (request.getRequestBody().getEndOfStream()) { + receivedPhases.add("REQ_BODY_MSG"); + receivedPhases.add("REQ_BODY_EOS"); + resp.setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8( + "MutatedBidiReq")) + .setEndOfStream(true) + .build()) + .build()) + .build()) + .build()); + } else { + receivedPhases.add("REQ_BODY_MSG"); + resp.setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8( + "MutatedBidiReq")) + .build()) + .build()) + .build()) + .build()); + } + } else if (request.hasResponseHeaders()) { + receivedPhases.add("RESP_HEADERS"); + resp.setResponseHeaders(HeadersResponse.newBuilder().build()); + } else if (request.hasResponseBody()) { + receivedPhases.add("RESP_BODY"); + resp.setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(request.getResponseBody().getBody()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasResponseTrailers()) { + receivedPhases.add("RESP_TRAILERS"); + resp.setResponseTrailers(TrailersResponse.newBuilder().build()); + responseObserver.onNext(resp.build()); + responseObserver.onCompleted(); + sidecarBidiLatch.countDown(); + return; + } + responseObserver.onNext(resp.build()); + sidecarBidiLatch.countDown(); + } + }); + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); } + }; + } + }; + final ExecutorService bidiTestExecutor = Executors.newFixedThreadPool(20); + final ExecutorService sidecarExecutor = Executors.newSingleThreadExecutor(); + grpcCleanup.register( + InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(bidiExtProcImpl) + .executor(sidecarExecutor) + .build() + .start()); + + // Data Plane Server (Bidi) + final AtomicReference serverReceivedHeaders = new AtomicReference<>(); + MutableHandlerRegistry uniqueBidiRegistry = new MutableHandlerRegistry(); + uniqueBidiRegistry.addService(ServerInterceptors.intercept( + ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) { + responseObserver.onNext(value + "Echo"); + } - @Override - public void onError(Throwable t) { - } + @Override + public void onError(Throwable t) { + } + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + })) + .build(), + new ServerInterceptor() { @Override - public void onCompleted() { - responseObserver.onCompleted(); + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + serverReceivedHeaders.set(headers); + return next.startCall(call, headers); } - }; - } - }; - grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(extProcImpl) - .executor(Executors.newSingleThreadExecutor()) - .build().start()); - - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; + })); + grpcCleanup.register( + InProcessServerBuilder.forName(uniqueDataPlaneServerName) + .fallbackHandlerRegistry(uniqueBidiRegistry) + .executor(bidiTestExecutor) + .build() + .start()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) + .executor(bidiTestExecutor) + .build()); CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(bidiTestExecutor) + .build()); }); + ScheduledExecutorService bidiRealScheduler = Executors.newSingleThreadScheduledExecutor(); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); + filterConfig, channelManager, bidiRealScheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })).build()); + final AtomicReference clientReceivedBody = new AtomicReference<>(); + final CountDownLatch finishLatch = new CountDownLatch(1); + final AtomicReference bidiHeadersFromInterceptor = new AtomicReference<>(); - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + Channel bidiInterceptingChannel = + io.grpc.ClientInterceptors.intercept( + dataPlaneChannel, + new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + super.start( + new io.grpc.ForwardingClientCallListener + .SimpleForwardingClientCallListener(responseListener) { + @Override + public void onHeaders(Metadata headers) { + bidiHeadersFromInterceptor.set(headers); + super.onHeaders(headers); + } + }, headers); + } + }; + } + }); - ClientCall proxyCall = + StreamObserver bidiRequestObserver = ClientCalls.asyncBidiStreamingCall( interceptCall(interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), - dataPlaneChannel); - - proxyCall.start(new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - callLatch.countDown(); - } - }, new Metadata()); - proxyCall.request(1); - proxyCall.sendMessage("test"); - proxyCall.halfClose(); - - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(callLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - ProcessingRequest request = capturedRequest.get(); - java.util.Map attributes = request.getAttributesMap(); - assertThat(attributes.get("request.path").getFieldsOrThrow("").getStringValue()) - .isEqualTo("/test.TestService/SayHello"); - assertThat(attributes.get("request.url_path").getFieldsOrThrow("").getStringValue()) - .isEqualTo("/test.TestService/SayHello"); - assertThat(attributes.get("request.host").getFieldsOrThrow("").getStringValue()) - .isEqualTo(dataPlaneChannel.authority()); - - channelManager.close(); - } - - @Test - public void givenMetadataAttributes_whenHeadersPresent_thenAttributesSent() throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .addRequestAttributes("request.referer") - .addRequestAttributes("request.useragent") - .addRequestAttributes("request.id") - .addRequestAttributes("request.headers") - .build(); - - final AtomicReference capturedRequest = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); - final CountDownLatch callLatch = new CountDownLatch(1); - - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { + METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(bidiTestExecutor), + bidiInterceptingChannel), + new StreamObserver() { @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - capturedRequest.set(request); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - } + public void onNext(String value) { + clientReceivedBody.set(value); } @Override public void onError(Throwable t) { + finishLatch.countDown(); } @Override public void onCompleted() { - responseObserver.onCompleted(); + finishLatch.countDown(); } - }; - } - }; - grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(extProcImpl) - .executor(Executors.newSingleThreadExecutor()) - .build().start()); - - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; - - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); - }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); - - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })).build()); - - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); - - Metadata headers = new Metadata(); - headers.put(Metadata.Key.of("referer", Metadata.ASCII_STRING_MARSHALLER), "http://google.com"); - headers.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "custom-ua"); - headers.put(Metadata.Key.of("x-request-id", Metadata.ASCII_STRING_MARSHALLER), "req-123"); - headers.put( - Metadata.Key.of("custom-header", Metadata.ASCII_STRING_MARSHALLER), "val"); - headers.put( - Metadata.Key.of("x-bin-key-bin", Metadata.BINARY_BYTE_MARSHALLER), new byte[]{1, 2}); + }); - ClientCall proxyCall = - interceptCall(interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(Executors.newSingleThreadExecutor()), - dataPlaneChannel); - - proxyCall.start(new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - callLatch.countDown(); - } - }, headers); - proxyCall.request(1); - proxyCall.sendMessage("test"); - proxyCall.halfClose(); + bidiRequestObserver.onNext("Bidi"); + bidiRequestObserver.onCompleted(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(callLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - ProcessingRequest request = capturedRequest.get(); - java.util.Map attributes = request.getAttributesMap(); - assertThat(attributes.get("request.referer").getFieldsOrThrow("").getStringValue()) - .isEqualTo("http://google.com"); - assertThat(attributes.get("request.useragent").getFieldsOrThrow("").getStringValue()) - .isEqualTo("custom-ua"); - assertThat(attributes.get("request.id").getFieldsOrThrow("").getStringValue()) - .isEqualTo("req-123"); - - com.google.protobuf.Struct headersStruct = attributes.get("request.headers"); - assertThat(headersStruct.getFieldsOrThrow("x-bin-key-bin").getStringValue()) - .isEqualTo("AQI"); + if (!sidecarBidiLatch.await(10, TimeUnit.SECONDS)) { + throw new AssertionError("Sidecar bidi actions failed. Received: " + receivedPhases); + } + assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); - channelManager.close(); - } + List expectedPhases = + Arrays.asList( + "REQ_HEADERS", + "REQ_BODY_MSG", + "REQ_BODY_EOS", + "RESP_HEADERS", + "RESP_BODY", + "RESP_TRAILERS"); + assertThat(receivedPhases).containsExactlyElementsIn(expectedPhases).inOrder(); + assertThat(serverReceivedHeaders.get().get(reqKey)).isEqualTo("true"); + assertThat(clientReceivedBody.get()).isEqualTo("MutatedBidiReqEcho"); + bidiRealScheduler.shutdown(); + bidiSidecarResponseExecutor.shutdown(); + bidiTestExecutor.shutdown(); + sidecarExecutor.shutdown(); + channelManager.close(); + } - // --- Category 24: Response Ordering Checks --- + // --- Category 22: Header Forwarding --- @Test - public void givenOutOfOrderReqResponses_whenMessageArrivesBeforeHeaders_thenFails() + public void + givenAllowedHeaders_whenRequestHeadersForwarded_thenOnlyAllowedAreSent() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + final AtomicReference + capturedHeaders = new AtomicReference<>(); final CountDownLatch sidecarLatch = new CountDownLatch(1); - final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -12220,26 +12471,20 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { - // Violate order: send RequestBody response before RequestHeaders response + capturedHeaders.set(request.getRequestHeaders()); responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setEndOfStream(true) - .build()) - .build()) - .build()) - .build()) + .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); sidecarLatch.countDown(); - responseObserver.onCompleted(); // Complete stream to allow cleanup + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); } } @Override public void onError(Throwable t) { - extProcError.set(t); } @Override @@ -12254,264 +12499,69 @@ public void onCompleted() { .executor(Executors.newSingleThreadExecutor()) .build().start()); + // Config with forward_rules: allowed_headers = ["x-allowed-*", "content-type"] ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .build()) - .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; - - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); - }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); - - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })).build()); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); - - final CountDownLatch appCloseLatch = new CountDownLatch(1); - final AtomicReference appStatus = new AtomicReference<>(); - ClientCall proxyCall = - interceptCall(interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), - dataPlaneChannel); - proxyCall.start(new ClientCall.Listener() { - @Override public void onClose(Status status, Metadata trailers) { - appStatus.set(status); - appCloseLatch.countDown(); - } - }, new Metadata()); - - proxyCall.request(1); - proxyCall.sendMessage("test"); - proxyCall.halfClose(); - - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - // The call should fail with INTERNAL status - // due to stream failure triggered by protocol error - assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(appStatus.get().getDescription()).contains("External processor stream failed"); - - channelManager.close(); - } - - @Test - public void givenUnexpectedResponseHeaders_whenHeadersArriveBeforeServerHeaders_thenFails() - throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - - final CountDownLatch sidecarLatch = new CountDownLatch(1); - final AtomicReference extProcError = new AtomicReference<>(); - - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - // Violate order: send ResponseHeaders response instead of RequestHeaders response - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - responseObserver.onCompleted(); - } - } - - @Override - public void onError(Throwable t) { - extProcError.set(t); - } - - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; - } - }; - - grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(extProcImpl) - .executor(Executors.newSingleThreadExecutor()) - .build().start()); - - // Configure processing mode to SEND both request and response headers - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .build()) - .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); - }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); - - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })).build()); - - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); - - final CountDownLatch appCloseLatch = new CountDownLatch(1); - final AtomicReference appStatus = new AtomicReference<>(); - ClientCall proxyCall = interceptCall( - interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), - dataPlaneChannel); - - proxyCall.start(new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - appStatus.set(status); - appCloseLatch.countDown(); - } - }, new Metadata()); - - proxyCall.request(1); - proxyCall.sendMessage("test"); - proxyCall.halfClose(); - - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - // The call should fail with INTERNAL status due to protocol error - assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(appStatus.get().getDescription()).contains("External processor stream failed"); - - // The data plane call should have the local cause set to the protocol violation - assertThat(appStatus.get().getCause()).isNotNull(); - assertThat(appStatus.get().getCause().getMessage()) - .contains("Protocol error: received response out of order"); - - channelManager.close(); - } - - @Test - public void givenUnexpectedResponseTrailers_whenTrailersArriveBeforeServerTrailers_thenFails() - throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - - final CountDownLatch sidecarLatch = new CountDownLatch(1); - final AtomicReference extProcError = new AtomicReference<>(); - - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - // Violate order: send ResponseTrailers response instead of RequestHeaders - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseTrailers(TrailersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - responseObserver.onCompleted(); - } - } - - @Override - public void onError(Throwable t) { - extProcError.set(t); - } - - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; - } - }; - - grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(extProcImpl) - .executor(Executors.newSingleThreadExecutor()) - .build().start()); - - // Configure processing mode to SEND both request headers and response trailers - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .setForwardRules(HeaderForwardingRules.newBuilder() + .setAllowedHeaders( + io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() + .addPatterns( + io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() + .setPrefix("x-allowed-") + .build()) + .addPatterns( + io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() + .setExact("content-type") + .build()) + .build()) .build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })).build()); + .addMethod( + METHOD_SAY_HELLO, + ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); + })) + .build()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + Metadata headers = new Metadata(); + headers.put( + Metadata.Key.of("x-allowed-1", Metadata.ASCII_STRING_MARSHALLER), "v1"); + headers.put( + Metadata.Key.of("x-disallowed", Metadata.ASCII_STRING_MARSHALLER), "v2"); + headers.put( + Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER), "application/grpc"); final CountDownLatch appCloseLatch = new CountDownLatch(1); - final AtomicReference appStatus = new AtomicReference<>(); - ClientCall proxyCall = interceptCall( - interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), - dataPlaneChannel); - + ClientCall proxyCall = + interceptCall(interceptor, + METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - appStatus.set(status); appCloseLatch.countDown(); } - }, new Metadata()); + }, headers); proxyCall.request(1); proxyCall.sendMessage("test"); @@ -12519,115 +12569,140 @@ public void onClose(Status status, Metadata trailers) { assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - // The call should fail with INTERNAL status due to protocol error - assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(appStatus.get().getDescription()).contains("External processor stream failed"); - - // The data plane call should have the local cause set to the protocol violation - assertThat(appStatus.get().getCause()).isNotNull(); - assertThat(appStatus.get().getCause().getMessage()) - .contains("Protocol error: received response out of order"); - + + List headerNames = new ArrayList<>(); + for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : + capturedHeaders.get().getHeaders().getHeadersList()) { + headerNames.add(hv.getKey()); + } + assertThat(headerNames).contains("x-allowed-1"); + assertThat(headerNames).contains("content-type"); + assertThat(headerNames).doesNotContain("x-disallowed"); + channelManager.close(); } @Test - public void givenOutOfOrderRespResponses_whenResponseBodyArrivesBeforeResponseHeaders_thenFails() + public void + givenAllowedHeaders_whenResponseHeadersForwarded_thenOnlyAllowedAreSent() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + final AtomicReference + capturedHeaders = new AtomicReference<>(); final CountDownLatch sidecarLatch = new CountDownLatch(1); - final AtomicReference extProcError = new AtomicReference<>(); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - // Send valid RequestHeaders response first - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - } else if (request.hasResponseHeaders()) { - // Violate order: send ResponseBody response instead of ResponseHeaders response - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseBody(BodyResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - responseObserver.onCompleted(); - } - } + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseHeaders()) { + capturedHeaders.set(request.getResponseHeaders()); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + sidecarLatch.countDown(); + } + } - @Override - public void onError(Throwable t) { - extProcError.set(t); - } + @Override + public void onError(Throwable t) { + } - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; + @Override + public void onCompleted() { + responseObserver.onCompleted(); } }; - + } + }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .executor(Executors.newSingleThreadExecutor()) .build().start()); - // Configure processing mode to SEND request headers, response headers, response body, - // and response trailers + // Config with forward_rules: allowed_headers = ["x-allowed-*", "content-type"] ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .setForwardRules(HeaderForwardingRules.newBuilder() + .setAllowedHeaders( + io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() + .addPatterns( + io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() + .setPrefix("x-allowed-") + .build()) + .addPatterns( + io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() + .setExact("content-type") + .build()) + .build()) .build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - // The data plane server responds to trigger response headers on the client - responseObserver.onNext("Hello"); - })).build()); + dataPlaneServiceRegistry.addService(ServerInterceptors.intercept( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_SAY_HELLO, + ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); + })) + .build(), + new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + Metadata responseHeaders = new Metadata(); + responseHeaders.put( + Metadata.Key.of("x-allowed-response", Metadata.ASCII_STRING_MARSHALLER), "v1"); + responseHeaders.put( + Metadata.Key.of("x-disallowed-response", Metadata.ASCII_STRING_MARSHALLER), "v2"); + responseHeaders.put( + Metadata.Key.of("content-type", Metadata.ASCII_STRING_MARSHALLER), + "application/grpc"); - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + call.sendHeaders(responseHeaders); + return next.startCall(call, headers); + } + })); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); final CountDownLatch appCloseLatch = new CountDownLatch(1); - final AtomicReference appStatus = new AtomicReference<>(); - ClientCall proxyCall = interceptCall( - interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), - dataPlaneChannel); - + ClientCall proxyCall = + interceptCall(interceptor, + METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - appStatus.set(status); appCloseLatch.countDown(); } }, new Metadata()); @@ -12638,23 +12713,26 @@ public void onClose(Status status, Metadata trailers) { assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - // The call should fail with INTERNAL status due to protocol error - assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(appStatus.get().getDescription()).contains("External processor stream failed"); - - // The data plane call should have the local cause set to the protocol violation - assertThat(appStatus.get().getCause()).isNotNull(); - assertThat(appStatus.get().getCause().getMessage()) - .contains("Protocol error: received response_body before headers response."); - + + List headerNames = new ArrayList<>(); + for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : + capturedHeaders.get().getHeaders().getHeadersList()) { + headerNames.add(hv.getKey()); + } + assertThat(headerNames).contains("x-allowed-response"); + assertThat(headerNames).contains("content-type"); + assertThat(headerNames).doesNotContain("x-disallowed-response"); + channelManager.close(); } @Test - public void givenValidOrder_whenResponsesArriveInOrder_thenSucceeds() throws Exception { + public void givenDisallowedHeaders_whenHeadersForwarded_thenSkipped() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + final AtomicReference capturedHeaders = + new AtomicReference<>(); final CountDownLatch sidecarLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -12667,10 +12745,15 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { + capturedHeaders.set(request.getRequestHeaders()); responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); sidecarLatch.countDown(); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); } } @@ -12690,11 +12773,20 @@ public void onCompleted() { .executor(Executors.newSingleThreadExecutor()) .build().start()); - // Configure processing mode to SEND request headers, but SKIP response headers + // Config with forward_rules: disallowed_headers = ["x-secret", "authorization"] ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setForwardRules(HeaderForwardingRules.newBuilder() + .setDisallowedHeaders( + io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() + .addPatterns( + io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() + .setExact("x-secret") + .build()) + .addPatterns( + io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() + .setExact("authorization") + .build()) + .build()) .build()) .build(); ExternalProcessorFilterConfig filterConfig = @@ -12709,24 +12801,22 @@ public void onCompleted() { filterConfig, channelManager, scheduler, FAKE_CONTEXT); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod( - METHOD_SAY_HELLO, - ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })) - .build()); - + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); + })).build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .executor(Executors.newSingleThreadExecutor()) .build()); - final CountDownLatch callLatch = new CountDownLatch(1); - final AtomicReference capturedStatus = new AtomicReference<>(); + Metadata headers = new Metadata(); + headers.put(Metadata.Key.of("x-foo", Metadata.ASCII_STRING_MARSHALLER), "v1"); + headers.put(Metadata.Key.of("x-secret", Metadata.ASCII_STRING_MARSHALLER), "v2"); + headers.put(Metadata.Key.of("authorization", Metadata.ASCII_STRING_MARSHALLER), "v3"); + final CountDownLatch appCloseLatch = new CountDownLatch(1); ClientCall proxyCall = interceptCall(interceptor, METHOD_SAY_HELLO, @@ -12735,222 +12825,37 @@ public void onCompleted() { proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - capturedStatus.set(status); - callLatch.countDown(); + appCloseLatch.countDown(); } - }, new Metadata()); + }, headers); proxyCall.request(1); proxyCall.sendMessage("test"); proxyCall.halfClose(); - // Verify that headers are processed correctly and the ordering check passes - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); - - // Verify that the call completes successfully - assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(capturedStatus.get().isOk()).isTrue(); - - channelManager.close(); - } - - @Test - public void givenBidiStreamInterleavedEvents_whenExtProcRespondsOutOfLockstep_thenSucceeds() - throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - - final CountDownLatch sidecarRequestBodyLatch = new CountDownLatch(1); - final CountDownLatch sidecarResponseHeadersLatch = new CountDownLatch(1); - final CountDownLatch allDoneLatch = new CountDownLatch(1); - - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - final AtomicReference> observerRef = - new AtomicReference<>(responseObserver); - return new StreamObserver() { - private ProcessingRequest savedRequestBody; - - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestBody()) { - if (request.getRequestBody().getEndOfStream() - || request.getRequestBody().getEndOfStreamWithoutMessage()) { - // This is the half-close request! - observerRef.get().onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setEndOfStream(true) - .build()) - .build()) - .build()) - .build()) - .build()); - } else { - savedRequestBody = request; - sidecarRequestBodyLatch.countDown(); - } - } else if (request.hasResponseHeaders()) { - // When RESPONSE_HEADERS is received, we respond to it first! - // This is out-of-lockstep because REQUEST_BODY response is still outstanding. - observerRef.get().onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarResponseHeadersLatch.countDown(); - - // Now send response to REQUEST_BODY with streamed response containing the body - if (savedRequestBody != null) { - observerRef.get().onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(savedRequestBody.getRequestBody().getBody()) - .build()) - .build()) - .build()) - .build()) - .build()); - } - } - } - - @Override - public void onError(Throwable t) {} - - @Override - public void onCompleted() { - observerRef.get().onCompleted(); - } - }; - } - }; - - grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(extProcImpl) - .executor(scheduler) - .build().start()); - - MutableHandlerRegistry uniqueBidiRegistry = new MutableHandlerRegistry(); - uniqueBidiRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( - new ServerCalls.BidiStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - // Send headers immediately by sending a message when stream starts - responseObserver.onNext("Welcome"); - return new StreamObserver() { - @Override - public void onNext(String value) {} - - @Override - public void onError(Throwable t) {} - - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; - } - })) - .build()); - - grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) - .fallbackHandlerRegistry(uniqueBidiRegistry) - .executor(scheduler) - .build().start()); - - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setProcessingMode(ProcessingMode.newBuilder() - // SKIP so data plane call starts immediately - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) - // GRPC body mode to trigger REQUEST_BODY - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - // SEND to trigger RESPONSE_HEADERS - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .build()) - .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; - - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(scheduler) - .build()); - }); - - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); - - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .executor(scheduler) - .build()); - - ClientCall clientCall = interceptCall(interceptor, - METHOD_BIDI_STREAMING, - DEFAULT_CALL_OPTIONS.withExecutor(scheduler), - dataPlaneChannel); - - StreamObserver bidiRequestObserver = ClientCalls.asyncBidiStreamingCall( - clientCall, - new StreamObserver() { - @Override - public void onNext(String value) {} - - @Override - public void onError(Throwable t) {} - - @Override - public void onCompleted() { - allDoneLatch.countDown(); - } - }); - - // Send client message to trigger REQUEST_BODY to ext_proc - bidiRequestObserver.onNext("ClientMsg"); - - // Wait for ext_proc to process out-of-lockstep events - while (sidecarRequestBodyLatch.getCount() > 0 || sidecarResponseHeadersLatch.getCount() > 0) { - if (fakeClock.numPendingTasks() == 0) { - break; - } - fakeClock.runDueTasks(); - } - assertThat(sidecarRequestBodyLatch.getCount()).isEqualTo(0); - assertThat(sidecarResponseHeadersLatch.getCount()).isEqualTo(0); - - // Complete the bidi stream - bidiRequestObserver.onCompleted(); - while (allDoneLatch.getCount() > 0) { - if (fakeClock.numPendingTasks() == 0) { - break; - } - fakeClock.runDueTasks(); - } - assertThat(allDoneLatch.getCount()).isEqualTo(0); - - // Clean up by cancelling the call explicitly - clientCall.cancel("Test finished", null); - + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + List headerNames = new ArrayList<>(); + for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : + capturedHeaders.get().getHeaders().getHeadersList()) { + headerNames.add(hv.getKey()); + } + assertThat(headerNames).contains("x-foo"); + assertThat(headerNames).doesNotContain("x-secret"); + assertThat(headerNames).doesNotContain("authorization"); + channelManager.close(); } - // --- Category 25: Header Response Status Checks --- - @Test - public void givenRequestHeadersResponse_whenStatusIsContinueAndReplace_thenFails() - throws Exception { + public void givenBothRules_whenHeadersForwarded_thenBothAreApplied() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + final AtomicReference capturedHeaders = + new AtomicReference<>(); final CountDownLatch sidecarLatch = new CountDownLatch(1); - final CountDownLatch sidecarFinishedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -12962,26 +12867,24 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { + capturedHeaders.set(request.getRequestHeaders()); responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setStatus(CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) - .build()) - .build()) + .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); sidecarLatch.countDown(); - responseObserver.onCompleted(); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); } } @Override public void onError(Throwable t) { - sidecarFinishedLatch.countDown(); } @Override public void onCompleted() { - sidecarFinishedLatch.countDown(); responseObserver.onCompleted(); } }; @@ -12992,9 +12895,24 @@ public void onCompleted() { .executor(Executors.newSingleThreadExecutor()) .build().start()); - // Enable fail-open + // Config with forward_rules: allowed = ["x-foo-*"], disallowed = ["x-foo-secret"] ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setFailureModeAllow(true) + .setForwardRules(HeaderForwardingRules.newBuilder() + .setAllowedHeaders( + io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() + .addPatterns( + io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() + .setPrefix("x-foo-") + .build()) + .build()) + .setDisallowedHeaders( + io.envoyproxy.envoy.type.matcher.v3.ListStringMatcher.newBuilder() + .addPatterns( + io.envoyproxy.envoy.type.matcher.v3.StringMatcher.newBuilder() + .setExact("x-foo-secret") + .build()) + .build()) + .build()) .build(); ExternalProcessorFilterConfig filterConfig = provider.parseFilterConfig(Any.pack(proto), filterContext).config; @@ -13012,51 +12930,92 @@ public void onCompleted() { responseObserver.onNext("Hello"); responseObserver.onCompleted(); })).build()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .executor(Executors.newSingleThreadExecutor()) - .build()); + Metadata headers = new Metadata(); + headers.put(Metadata.Key.of("x-foo-1", Metadata.ASCII_STRING_MARSHALLER), "v1"); + headers.put(Metadata.Key.of("x-foo-secret", Metadata.ASCII_STRING_MARSHALLER), "v2"); + headers.put(Metadata.Key.of("x-bar", Metadata.ASCII_STRING_MARSHALLER), "v3"); final CountDownLatch appCloseLatch = new CountDownLatch(1); - final AtomicReference appStatus = new AtomicReference<>(); ClientCall proxyCall = interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); proxyCall.start(new ClientCall.Listener() { - @Override public void onClose(Status status, Metadata trailers) { - appStatus.set(status); + @Override + public void onClose(Status status, Metadata trailers) { appCloseLatch.countDown(); } - }, new Metadata()); + }, headers); proxyCall.request(1); proxyCall.sendMessage("test"); - try { - proxyCall.halfClose(); - } catch (IllegalStateException ignored) { - // ignore + proxyCall.halfClose(); + + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + List headerNames = new ArrayList<>(); + for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : + capturedHeaders.get().getHeaders().getHeadersList()) { + headerNames.add(hv.getKey()); } + assertThat(headerNames).contains("x-foo-1"); + assertThat(headerNames).doesNotContain("x-foo-secret"); + assertThat(headerNames).doesNotContain("x-bar"); + + channelManager.close(); + } - assertThat(sidecarLatch.await(30, TimeUnit.SECONDS)).isTrue(); - assertThat(sidecarFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(30, TimeUnit.SECONDS)).isTrue(); + // --- Category 23: Request Attributes --- - // Call should succeed due to fail-open - assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.OK); + @Test + public void parseFilterConfig_withUnrecognizedRequestAttribute_isIgnored() { + ExternalProcessor proto = createBaseProto(extProcServerName) + .addRequestAttributes("invalid.attribute") + .build(); + ConfigOrError result = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(result.errorDetail).isNull(); + assertThat(result.config.getRequestAttributes()).containsExactly("invalid.attribute"); + } - channelManager.close(); + @Test + public void parseFilterConfig_withRecognizedRequestAttributes_succeeds() { + ExternalProcessor proto = createBaseProto(extProcServerName) + .addRequestAttributes("request.path") + .addRequestAttributes("request.host") + .addRequestAttributes("request.scheme") // Recognized but not set + .build(); + ConfigOrError result = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(result.errorDetail).isNull(); + assertThat(result.config.getRequestAttributes()).containsExactly( + "request.path", "request.host", "request.scheme"); } @Test - public void givenResponseHeadersResponse_whenStatusIsContinueAndReplace_thenFails() - throws Exception { + public void givenRequestAttributes_whenHeaderPhase_thenAttributesSent() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .addRequestAttributes("request.path") + .addRequestAttributes("request.url_path") + .addRequestAttributes("request.host") + .addRequestAttributes("request.method") + .addRequestAttributes("request.query") + .build(); + final AtomicReference capturedRequest = new AtomicReference<>(); final CountDownLatch sidecarLatch = new CountDownLatch(1); - final CountDownLatch sidecarFinishedLatch = new CountDownLatch(1); + final CountDownLatch callLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -13068,30 +13027,24 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { + capturedRequest.set(request); responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); + sidecarLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setStatus(CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) - .build()) - .build()) + .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); - responseObserver.onCompleted(); } } @Override public void onError(Throwable t) { - sidecarFinishedLatch.countDown(); } @Override public void onCompleted() { - sidecarFinishedLatch.countDown(); responseObserver.onCompleted(); } }; @@ -13102,13 +13055,6 @@ public void onCompleted() { .executor(Executors.newSingleThreadExecutor()) .build().start()); - // Enable response headers and fail-open - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setFailureModeAllow(true) - .setProcessingMode(ProcessingMode.newBuilder() - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .build()) - .build(); ExternalProcessorFilterConfig filterConfig = provider.parseFilterConfig(Any.pack(proto), filterContext).config; @@ -13127,96 +13073,95 @@ public void onCompleted() { })).build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .executor(Executors.newSingleThreadExecutor()) .build()); - final CountDownLatch appCloseLatch = new CountDownLatch(1); - final AtomicReference appStatus = new AtomicReference<>(); ClientCall proxyCall = interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + proxyCall.start(new ClientCall.Listener() { - @Override public void onClose(Status status, Metadata trailers) { - appStatus.set(status); - appCloseLatch.countDown(); + @Override + public void onClose(Status status, Metadata trailers) { + callLatch.countDown(); } }, new Metadata()); - proxyCall.request(1); proxyCall.sendMessage("test"); - try { - proxyCall.halfClose(); - } catch (IllegalStateException ignored) { - // ignore - } - - assertThat(sidecarLatch.await(30, TimeUnit.SECONDS)).isTrue(); - assertThat(sidecarFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(30, TimeUnit.SECONDS)).isTrue(); - - // The call should succeed due to fail-open - assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.OK); + proxyCall.halfClose(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(callLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + ProcessingRequest request = capturedRequest.get(); + java.util.Map attributes = request.getAttributesMap(); + assertThat(attributes.get("request.path").getFieldsOrThrow("").getStringValue()) + .isEqualTo("/test.TestService/SayHello"); + assertThat(attributes.get("request.url_path").getFieldsOrThrow("").getStringValue()) + .isEqualTo("/test.TestService/SayHello"); + assertThat(attributes.get("request.host").getFieldsOrThrow("").getStringValue()) + .isEqualTo(dataPlaneChannel.authority()); + channelManager.close(); } @Test - public void givenExtProcCall_whenExecutionSucceeds_thenAll4MetricsAreRecorded() throws Exception { - final String uniqueExtProcServerName = "ext-proc-server-metrics-" + java.util.UUID.randomUUID(); - final CountDownLatch sidecarRequestHeadersLatch = new CountDownLatch(1); + public void givenMetadataAttributes_whenHeadersPresent_thenAttributesSent() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .addRequestAttributes("request.referer") + .addRequestAttributes("request.useragent") + .addRequestAttributes("request.id") + .addRequestAttributes("request.headers") + .build(); + + final AtomicReference capturedRequest = new AtomicReference<>(); final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch callLatch = new CountDownLatch(1); - // In-process mock server for External Processor - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { @Override - @SuppressWarnings("unchecked") - public StreamObserver process( - StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarRequestHeadersLatch.countDown(); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - } - } + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + capturedRequest.set(request); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + sidecarLatch.countDown(); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } + } - @Override - public void onError(Throwable t) { - } + @Override + public void onError(Throwable t) { + } - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; + @Override + public void onCompleted() { + responseObserver.onCompleted(); } }; - + } + }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .executor(Executors.newSingleThreadExecutor()) .build().start()); - // Enable request headers and response headers - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .build()) - .build(); ExternalProcessorFilterConfig filterConfig = provider.parseFilterConfig(Any.pack(proto), filterContext).config; @@ -13225,151 +13170,124 @@ public void onCompleted() { .executor(Executors.newSingleThreadExecutor()) .build()); }); - - // Mock MetricRecorder to assert records - io.grpc.MetricRecorder mockMetricRecorder = Mockito.mock(io.grpc.MetricRecorder.class); - Filter.FilterContext customContext = Filter.FilterContext.create( - "envoy.ext_proc", - mockMetricRecorder); - - ScheduledExecutorService realScheduler = Executors.newSingleThreadScheduledExecutor(); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, realScheduler, customContext); + filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final CountDownLatch dataPlaneLatch = new CountDownLatch(1); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - new Thread(() -> { - try { - if (dataPlaneLatch.await(10, TimeUnit.SECONDS)) { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - } - } catch (InterruptedException e) { - responseObserver.onError(e); - } - }).start(); + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); })).build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .overrideAuthority("xds:///target-service-metric") + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .executor(Executors.newSingleThreadExecutor()) .build()); - final CountDownLatch appCloseLatch = new CountDownLatch(1); + Metadata headers = new Metadata(); + headers.put(Metadata.Key.of("referer", Metadata.ASCII_STRING_MARSHALLER), "http://google.com"); + headers.put(Metadata.Key.of("user-agent", Metadata.ASCII_STRING_MARSHALLER), "custom-ua"); + headers.put(Metadata.Key.of("x-request-id", Metadata.ASCII_STRING_MARSHALLER), "req-123"); + headers.put( + Metadata.Key.of("custom-header", Metadata.ASCII_STRING_MARSHALLER), "val"); + headers.put( + Metadata.Key.of("x-bin-key-bin", Metadata.BINARY_BYTE_MARSHALLER), new byte[]{1, 2}); + ClientCall proxyCall = interceptCall(interceptor, METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()) - .withOption(XdsNameResolver.CLUSTER_SELECTION_KEY, "backend-service-metric"), + DEFAULT_CALL_OPTIONS.withExecutor(Executors.newSingleThreadExecutor()), dataPlaneChannel); - + proxyCall.start(new ClientCall.Listener() { - @Override public void onClose(Status status, Metadata trailers) { - appCloseLatch.countDown(); + @Override + public void onClose(Status status, Metadata trailers) { + callLatch.countDown(); } - }, new Metadata()); - + }, headers); proxyCall.request(1); proxyCall.sendMessage("test"); proxyCall.halfClose(); - // 1. Wait for mock Ext Proc to receive and process client request headers - assertThat(sidecarRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); - - // 2. Release the data plane server to respond back to the client call - dataPlaneLatch.countDown(); - - // 3. Assert that all stages complete in sequence deterministically - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); - - // Clean up and close the Ext Proc stream to release in-process server/channel resources cleanly - proxyCall.cancel("Cleanup", null); - - // Verify that the 4 duration metrics were recorded with proper labels! - Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( - Mockito.eq(ExternalProcessorClientInterceptor.clientHeadersDuration), - Mockito.anyDouble(), - Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric")), - Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric"))); - - Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( - Mockito.eq(ExternalProcessorClientInterceptor.clientHalfCloseDuration), - Mockito.anyDouble(), - Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric")), - Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric"))); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(callLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + ProcessingRequest request = capturedRequest.get(); + java.util.Map attributes = request.getAttributesMap(); + assertThat(attributes.get("request.referer").getFieldsOrThrow("").getStringValue()) + .isEqualTo("http://google.com"); + assertThat(attributes.get("request.useragent").getFieldsOrThrow("").getStringValue()) + .isEqualTo("custom-ua"); + assertThat(attributes.get("request.id").getFieldsOrThrow("").getStringValue()) + .isEqualTo("req-123"); + + com.google.protobuf.Struct headersStruct = attributes.get("request.headers"); + assertThat(headersStruct.getFieldsOrThrow("x-bin-key-bin").getStringValue()) + .isEqualTo("AQI"); + + channelManager.close(); + } - Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( - Mockito.eq(ExternalProcessorClientInterceptor.serverHeadersDuration), - Mockito.anyDouble(), - Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric")), - Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric"))); - Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( - Mockito.eq(ExternalProcessorClientInterceptor.serverTrailersDuration), - Mockito.anyDouble(), - Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric")), - Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric"))); - channelManager.close(); - realScheduler.shutdown(); - } + // --- Category 24: Response Ordering Checks --- @Test - public void givenExtProcCall_whenExecutionFails_thenAll4MetricsAreRecorded() throws Exception { - final String uniqueExtProcServerName = - "ext-proc-server-metrics-fail-" + java.util.UUID.randomUUID(); - final CountDownLatch sidecarRequestHeadersLatch = new CountDownLatch(1); + public void givenOutOfOrderReqResponses_whenMessageArrivesBeforeHeaders_thenFails() + throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final AtomicReference extProcError = new AtomicReference<>(); - // In-process mock server for External Processor - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { @Override - @SuppressWarnings("unchecked") - public StreamObserver process( - StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarRequestHeadersLatch.countDown(); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - sidecarLatch.countDown(); - } - } + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + // Violate order: send RequestBody response before RequestHeaders response + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setEndOfStream(true) + .build()) + .build()) + .build()) + .build()) + .build()); + sidecarLatch.countDown(); + responseObserver.onCompleted(); // Complete stream to allow cleanup + } + } - @Override - public void onError(Throwable t) { - } + @Override + public void onError(Throwable t) { + extProcError.set(t); + } - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; + @Override + public void onCompleted() { + responseObserver.onCompleted(); } }; - + } + }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .executor(Executors.newSingleThreadExecutor()) .build().start()); - // Enable request headers and response headers ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) .build()) .build(); ExternalProcessorFilterConfig filterConfig = @@ -13380,58 +13298,27 @@ public void onCompleted() { .executor(Executors.newSingleThreadExecutor()) .build()); }); - - // Mock MetricRecorder to assert records - io.grpc.MetricRecorder mockMetricRecorder = Mockito.mock(io.grpc.MetricRecorder.class); - Filter.FilterContext customContext = Filter.FilterContext.create( - "envoy.ext_proc", - mockMetricRecorder); - - ScheduledExecutorService realScheduler = Executors.newSingleThreadScheduledExecutor(); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, realScheduler, customContext); - - final CountDownLatch dataPlaneLatch = new CountDownLatch(1); - dataPlaneServiceRegistry.addService(ServerInterceptors.intercept( - ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - new Thread(() -> { - try { - if (dataPlaneLatch.await(10, TimeUnit.SECONDS)) { - responseObserver.onError( - Status.UNAUTHENTICATED - .withDescription("authentication failed") - .asRuntimeException()); - } - } catch (InterruptedException e) { - responseObserver.onError(e); - } - }).start(); - })).build(), - new ServerInterceptor() { - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - call.sendHeaders(new Metadata()); - return next.startCall(call, headers); - } - })); + filterConfig, channelManager, scheduler, FAKE_CONTEXT); - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .overrideAuthority("xds:///target-service-metric-fail") - .executor(Executors.newSingleThreadExecutor()) - .build()); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); + })).build()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); - final AtomicReference appStatus = new AtomicReference<>(); final CountDownLatch appCloseLatch = new CountDownLatch(1); + final AtomicReference appStatus = new AtomicReference<>(); ClientCall proxyCall = interceptCall(interceptor, METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()) - .withOption(XdsNameResolver.CLUSTER_SELECTION_KEY, "backend-service-metric-fail"), + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { appStatus.set(status); @@ -13443,420 +13330,525 @@ public ServerCall.Listener interceptCall( proxyCall.sendMessage("test"); proxyCall.halfClose(); - // 1. Wait for mock Ext Proc to receive and process client request headers - assertThat(sidecarRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); - - // 2. Release the data plane server to respond back with error - dataPlaneLatch.countDown(); - - // 3. Assert that all stages complete - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); - - assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.UNAUTHENTICATED); - - // Clean up and close the Ext Proc stream - proxyCall.cancel("Cleanup", null); - - // Verify that the 4 duration metrics were recorded with proper labels! - Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( - Mockito.eq(ExternalProcessorClientInterceptor.clientHeadersDuration), - Mockito.anyDouble(), - Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric-fail")), - Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric-fail"))); - - Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( - Mockito.eq(ExternalProcessorClientInterceptor.clientHalfCloseDuration), - Mockito.anyDouble(), - Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric-fail")), - Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric-fail"))); - - Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( - Mockito.eq(ExternalProcessorClientInterceptor.serverHeadersDuration), - Mockito.anyDouble(), - Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric-fail")), - Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric-fail"))); - - Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( - Mockito.eq(ExternalProcessorClientInterceptor.serverTrailersDuration), - Mockito.anyDouble(), - Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric-fail")), - Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric-fail"))); - + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // The call should fail with INTERNAL status + // due to stream failure triggered by protocol error + assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(appStatus.get().getDescription()).contains("External processor stream failed"); + channelManager.close(); - realScheduler.shutdown(); } - // --- Category 26: Call activation with failure mode allow on and off --- @Test - public void - givenRequestHeaderModeSend_Fma_true_whenExtProcTerminates_thenCallIsActivated() + public void givenUnexpectedResponseHeaders_whenHeadersArriveBeforeServerHeaders_thenFails() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setFailureModeAllow(true) - .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - final AtomicReference> responseObserverRef = - new AtomicReference<>(); - final CountDownLatch streamActiveLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - StreamObserver responseObserver) { - responseObserverRef.set(responseObserver); - streamActiveLatch.countDown(); + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override - public void onNext(ProcessingRequest request) {} + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + // Violate order: send ResponseHeaders response instead of RequestHeaders response + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + sidecarLatch.countDown(); + responseObserver.onCompleted(); + } + } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + extProcError.set(t); + } @Override - public void onCompleted() {} + public void onCompleted() { + responseObserver.onCompleted(); + } }; } }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .directExecutor() + .executor(Executors.newSingleThreadExecutor()) .build().start()); + // Configure processing mode to SEND both request and response headers + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName) - .directExecutor() - .build()); + return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - MutableHandlerRegistry dataPlaneRegistry = new MutableHandlerRegistry(); - grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) - .fallbackHandlerRegistry(dataPlaneRegistry) - .directExecutor() - .build().start()); - - dataPlaneRegistry.addService(ServerServiceDefinition.builder("test.TestService") + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("response"); + responseObserver.onNext("Hello"); responseObserver.onCompleted(); })).build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .directExecutor() + InProcessChannelBuilder.forName(dataPlaneServerName) + .executor(Executors.newSingleThreadExecutor()) .build()); - ClientCall clientCall = interceptCall( - interceptor, METHOD_SAY_HELLO, + final CountDownLatch appCloseLatch = new CountDownLatch(1); + final AtomicReference appStatus = new AtomicReference<>(); + ClientCall proxyCall = interceptCall( + interceptor, + METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - final CountDownLatch callCompletedLatch = new CountDownLatch(1); - final AtomicReference closedStatus = new AtomicReference<>(); - clientCall.start(new ClientCall.Listener() { + proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - closedStatus.set(status); - callCompletedLatch.countDown(); + appStatus.set(status); + appCloseLatch.countDown(); } }, new Metadata()); - clientCall.request(1); - boolean active = streamActiveLatch.await(5, TimeUnit.SECONDS); - assertThat(active).isTrue(); + proxyCall.request(1); + proxyCall.sendMessage("test"); + proxyCall.halfClose(); - clientCall.sendMessage("app-msg"); - clientCall.halfClose(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - responseObserverRef.get().onError(new RuntimeException("Stream failure during start")); + // The call should fail with INTERNAL status due to protocol error + assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(appStatus.get().getDescription()).contains("External processor stream failed"); - boolean completed = callCompletedLatch.await(5, TimeUnit.SECONDS); - assertThat(completed).isTrue(); - // Verify call completed successfully due to FMA true (fail-open) - assertThat(closedStatus.get().isOk()).isTrue(); + // The data plane call should have the local cause set to the protocol violation + assertThat(appStatus.get().getCause()).isNotNull(); + assertThat(appStatus.get().getCause().getMessage()) + .contains("Protocol error: received response out of order"); channelManager.close(); } @Test - public void - givenRequestHeaderModeSend_Fma_false_whenExtProcTerminates_thenCallIsClosed() + public void givenUnexpectedResponseTrailers_whenTrailersArriveBeforeServerTrailers_thenFails() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setFailureModeAllow(false) - .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - final AtomicReference> responseObserverRef = - new AtomicReference<>(); - final CountDownLatch streamActiveLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - StreamObserver responseObserver) { - responseObserverRef.set(responseObserver); - streamActiveLatch.countDown(); + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override - public void onNext(ProcessingRequest request) {} + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + // Violate order: send ResponseTrailers response instead of RequestHeaders + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseTrailers(TrailersResponse.newBuilder().build()) + .build()); + sidecarLatch.countDown(); + responseObserver.onCompleted(); + } + } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + extProcError.set(t); + } @Override - public void onCompleted() {} + public void onCompleted() { + responseObserver.onCompleted(); + } }; } }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .directExecutor() + .executor(Executors.newSingleThreadExecutor()) .build().start()); + // Configure processing mode to SEND both request headers and response trailers + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName) - .directExecutor() - .build()); + return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - MutableHandlerRegistry dataPlaneRegistry = new MutableHandlerRegistry(); - grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) - .fallbackHandlerRegistry(dataPlaneRegistry) - .directExecutor() - .build().start()); - - dataPlaneRegistry.addService(ServerServiceDefinition.builder("test.TestService") + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("response"); + responseObserver.onNext("Hello"); responseObserver.onCompleted(); })).build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .directExecutor() + InProcessChannelBuilder.forName(dataPlaneServerName) + .executor(Executors.newSingleThreadExecutor()) .build()); - ClientCall clientCall = interceptCall( - interceptor, METHOD_SAY_HELLO, + final CountDownLatch appCloseLatch = new CountDownLatch(1); + final AtomicReference appStatus = new AtomicReference<>(); + ClientCall proxyCall = interceptCall( + interceptor, + METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - final CountDownLatch callCompletedLatch = new CountDownLatch(1); - final AtomicReference closedStatus = new AtomicReference<>(); - clientCall.start(new ClientCall.Listener() { + proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - closedStatus.set(status); - callCompletedLatch.countDown(); + appStatus.set(status); + appCloseLatch.countDown(); } }, new Metadata()); - clientCall.request(1); - boolean active = streamActiveLatch.await(5, TimeUnit.SECONDS); - assertThat(active).isTrue(); + proxyCall.request(1); + proxyCall.sendMessage("test"); + proxyCall.halfClose(); - // Terminate stream with error - responseObserverRef.get().onError(new RuntimeException("Stream failure during start")); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - boolean completed = callCompletedLatch.await(5, TimeUnit.SECONDS); - assertThat(completed).isTrue(); - // Verify call closed with INTERNAL status due to FMA false - assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(closedStatus.get().getDescription()).contains("External processor stream failed"); + // The call should fail with INTERNAL status due to protocol error + assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(appStatus.get().getDescription()).contains("External processor stream failed"); + + // The data plane call should have the local cause set to the protocol violation + assertThat(appStatus.get().getCause()).isNotNull(); + assertThat(appStatus.get().getCause().getMessage()) + .contains("Protocol error: received response out of order"); channelManager.close(); } @Test - public void givenFailureModeAllowTrue_whenExtProcStreamFailsAfterRequestBodySent_thenCallFails() + public void givenOutOfOrderRespResponses_whenResponseBodyArrivesBeforeResponseHeaders_thenFails() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setFailureModeAllow(true) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .build()) - .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - final AtomicReference> responseObserverRef = - new AtomicReference<>(); - final CountDownLatch streamActiveLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - StreamObserver responseObserver) { - responseObserverRef.set(responseObserver); - streamActiveLatch.countDown(); + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { + // Send valid RequestHeaders response first responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); + } else if (request.hasResponseHeaders()) { + // Violate order: send ResponseBody response instead of ResponseHeaders response + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder().build()) + .build()); + sidecarLatch.countDown(); + responseObserver.onCompleted(); } } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + extProcError.set(t); + } @Override - public void onCompleted() {} + public void onCompleted() { + responseObserver.onCompleted(); + } }; } }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .directExecutor() + .executor(Executors.newSingleThreadExecutor()) .build().start()); + // Configure processing mode to SEND request headers, response headers, response body, + // and response trailers + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName) - .directExecutor() - .build()); + return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - MutableHandlerRegistry dataPlaneRegistry = new MutableHandlerRegistry(); - grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) - .fallbackHandlerRegistry(dataPlaneRegistry) - .directExecutor() - .build().start()); - - dataPlaneRegistry.addService(ServerServiceDefinition.builder("test.TestService") + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("response"); - responseObserver.onCompleted(); + // The data plane server responds to trigger response headers on the client + responseObserver.onNext("Hello"); })).build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .directExecutor() + InProcessChannelBuilder.forName(dataPlaneServerName) + .executor(Executors.newSingleThreadExecutor()) .build()); - ClientCall clientCall = interceptCall( - interceptor, METHOD_SAY_HELLO, + final CountDownLatch appCloseLatch = new CountDownLatch(1); + final AtomicReference appStatus = new AtomicReference<>(); + ClientCall proxyCall = interceptCall( + interceptor, + METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - final AtomicReference closedStatus = new AtomicReference<>(); - final CountDownLatch callCompletedLatch = new CountDownLatch(1); - clientCall.start(new ClientCall.Listener() { + proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - closedStatus.set(status); - callCompletedLatch.countDown(); + appStatus.set(status); + appCloseLatch.countDown(); } }, new Metadata()); - clientCall.request(1); - boolean active = streamActiveLatch.await(5, TimeUnit.SECONDS); - assertThat(active).isTrue(); + proxyCall.request(1); + proxyCall.sendMessage("test"); + proxyCall.halfClose(); - clientCall.sendMessage("app-msg"); - clientCall.halfClose(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // Now abruptly fail the stream - responseObserverRef.get() - .onError(new RuntimeException("Stream failure after sending body/EOS")); + // The call should fail with INTERNAL status due to protocol error + assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(appStatus.get().getDescription()).contains("External processor stream failed"); - // Verify that the call failed with INTERNAL status instead of succeeding. - boolean completed = callCompletedLatch.await(5, TimeUnit.SECONDS); - assertThat(completed).isTrue(); - assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(closedStatus.get().getDescription()).contains("External processor stream failed"); + // The data plane call should have the local cause set to the protocol violation + assertThat(appStatus.get().getCause()).isNotNull(); + assertThat(appStatus.get().getCause().getMessage()) + .contains("Protocol error: received response_body before headers response."); channelManager.close(); } @Test - public void givenFailureModeAllowTrue_whenExtProcStreamFailsAfterResponseBodySent_thenCallFails() - throws Exception { + public void givenValidOrder_whenResponsesArriveInOrder_thenSucceeds() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + sidecarLatch.countDown(); + } + } + + @Override + public void onError(Throwable t) { + } + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .executor(Executors.newSingleThreadExecutor()) + .build().start()); + + // Configure processing mode to SEND request headers, but SKIP response headers ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setFailureModeAllow(true) .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); + }); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_SAY_HELLO, + ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); + })) + .build()); + + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); + + final CountDownLatch callLatch = new CountDownLatch(1); + final AtomicReference capturedStatus = new AtomicReference<>(); + + ClientCall proxyCall = + interceptCall(interceptor, + METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); + proxyCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + capturedStatus.set(status); + callLatch.countDown(); + } + }, new Metadata()); + + proxyCall.request(1); + proxyCall.sendMessage("test"); + proxyCall.halfClose(); + + // Verify that headers are processed correctly and the ordering check passes + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + + // Verify that the call completes successfully + assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(capturedStatus.get().isOk()).isTrue(); + + channelManager.close(); + } + + @Test + public void givenBidiStreamInterleavedEvents_whenExtProcRespondsOutOfLockstep_thenSucceeds() + throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final AtomicReference> responseObserverRef = - new AtomicReference<>(); - final CountDownLatch streamActiveLatch = new CountDownLatch(1); - final CountDownLatch streamFailedLatch = new CountDownLatch(1); + final CountDownLatch sidecarRequestBodyLatch = new CountDownLatch(1); + final CountDownLatch sidecarResponseHeadersLatch = new CountDownLatch(1); + final CountDownLatch allDoneLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - StreamObserver responseObserver) { - responseObserverRef.set(responseObserver); - streamActiveLatch.countDown(); + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + final AtomicReference> observerRef = + new AtomicReference<>(responseObserver); return new StreamObserver() { + private ProcessingRequest savedRequestBody; + @Override public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + if (request.hasRequestBody()) { + if (request.getRequestBody().getEndOfStream() + || request.getRequestBody().getEndOfStreamWithoutMessage()) { + // This is the half-close request! + observerRef.get().onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setEndOfStream(true) + .build()) + .build()) + .build()) + .build()) + .build()); + } else { + savedRequestBody = request; + sidecarRequestBodyLatch.countDown(); + } } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() + // When RESPONSE_HEADERS is received, we respond to it first! + // This is out-of-lockstep because REQUEST_BODY response is still outstanding. + observerRef.get().onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - } else if (request.hasResponseBody()) { - // Fail the stream once we see response body message - responseObserver.onError( - Status.INTERNAL.withDescription("Stream failure after response body") - .asRuntimeException()); - streamFailedLatch.countDown(); + sidecarResponseHeadersLatch.countDown(); + + // Now send response to REQUEST_BODY with streamed response containing the body + if (savedRequestBody != null) { + observerRef.get().onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(savedRequestBody.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } } } @@ -13864,302 +13856,358 @@ public void onNext(ProcessingRequest request) { public void onError(Throwable t) {} @Override - public void onCompleted() {} + public void onCompleted() { + observerRef.get().onCompleted(); + } }; } }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .directExecutor() + .executor(scheduler) .build().start()); - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName) - .directExecutor() - .build()); - }); + MutableHandlerRegistry uniqueBidiRegistry = new MutableHandlerRegistry(); + uniqueBidiRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + // Send headers immediately by sending a message when stream starts + responseObserver.onNext("Welcome"); + return new StreamObserver() { + @Override + public void onNext(String value) {} - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + })) + .build()); - MutableHandlerRegistry dataPlaneRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) - .fallbackHandlerRegistry(dataPlaneRegistry) - .directExecutor() + .fallbackHandlerRegistry(uniqueBidiRegistry) + .executor(scheduler) .build().start()); - dataPlaneRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("response-body-msg"); - responseObserver.onCompleted(); - })).build()); + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setProcessingMode(ProcessingMode.newBuilder() + // SKIP so data plane call starts immediately + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + // GRPC body mode to trigger REQUEST_BODY + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + // SEND to trigger RESPONSE_HEADERS + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(scheduler) + .build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(uniqueDataPlaneServerName) - .directExecutor() + .executor(scheduler) .build()); - ClientCall clientCall = interceptCall( - interceptor, METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + ClientCall clientCall = interceptCall(interceptor, + METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(scheduler), dataPlaneChannel); - final AtomicReference closedStatus = new AtomicReference<>(); - final CountDownLatch callCompletedLatch = new CountDownLatch(1); - clientCall.start(new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - closedStatus.set(status); - callCompletedLatch.countDown(); - } - }, new Metadata()); - clientCall.request(1); + StreamObserver bidiRequestObserver = ClientCalls.asyncBidiStreamingCall( + clientCall, + new StreamObserver() { + @Override + public void onNext(String value) {} - boolean active = streamActiveLatch.await(5, TimeUnit.SECONDS); - assertThat(active).isTrue(); + @Override + public void onError(Throwable t) {} - // Since request body mode is NONE, this sendMessage is NOT sent to ext_proc - clientCall.sendMessage("app-msg"); - clientCall.halfClose(); + @Override + public void onCompleted() { + allDoneLatch.countDown(); + } + }); - // Verify call completed and failed with INTERNAL status - boolean completed = callCompletedLatch.await(5, TimeUnit.SECONDS); - assertThat(completed).isTrue(); - assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(closedStatus.get().getDescription()).contains("External processor stream failed"); + // Send client message to trigger REQUEST_BODY to ext_proc + bidiRequestObserver.onNext("ClientMsg"); + + // Wait for ext_proc to process out-of-lockstep events + while (sidecarRequestBodyLatch.getCount() > 0 || sidecarResponseHeadersLatch.getCount() > 0) { + if (fakeClock.numPendingTasks() == 0) { + break; + } + fakeClock.runDueTasks(); + } + assertThat(sidecarRequestBodyLatch.getCount()).isEqualTo(0); + assertThat(sidecarResponseHeadersLatch.getCount()).isEqualTo(0); + + // Complete the bidi stream + bidiRequestObserver.onCompleted(); + while (allDoneLatch.getCount() > 0) { + if (fakeClock.numPendingTasks() == 0) { + break; + } + fakeClock.runDueTasks(); + } + assertThat(allDoneLatch.getCount()).isEqualTo(0); + + // Clean up by cancelling the call explicitly + clientCall.cancel("Test finished", null); channelManager.close(); } + // --- Category 25: Header Response Status Checks --- + @Test - public void givenObservabilityTrue_whenExtProcStreamFails_thenCallContinues() + public void givenRequestHeadersResponse_whenStatusIsContinueAndReplace_thenFails() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) - .setFailureModeAllow(false) - .setObservabilityMode(true) - .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - - final CountDownLatch streamActiveLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch sidecarFinishedLatch = new CountDownLatch(1); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { @Override - public StreamObserver process( - StreamObserver responseObserver) { - streamActiveLatch.countDown(); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - // Fail the stream immediately on receiving headers - responseObserver.onError( - Status.INTERNAL.withDescription("Simulated sidecar failure") - .asRuntimeException()); - } - } - - @Override - public void onError(Throwable t) {} + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setStatus(CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) + .build()) + .build()) + .build()); + sidecarLatch.countDown(); + responseObserver.onCompleted(); + } + } - @Override - public void onCompleted() {} - }; + @Override + public void onError(Throwable t) { + sidecarFinishedLatch.countDown(); } - }; + @Override + public void onCompleted() { + sidecarFinishedLatch.countDown(); + responseObserver.onCompleted(); + } + }; + } + }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .directExecutor() + .executor(Executors.newSingleThreadExecutor()) .build().start()); + // Enable fail-open + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setFailureModeAllow(true) + .build(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; + CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName) - .directExecutor() - .build()); + return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final CountDownLatch dataPlaneLatch = new CountDownLatch(1); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello " + request); - responseObserver.onCompleted(); - dataPlaneLatch.countDown(); - })) - .build()); + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); + })).build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - - final CountDownLatch closedLatch = new CountDownLatch(1); - final AtomicReference closedStatus = new AtomicReference<>(); - ClientCall.Listener appListener = new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - closedStatus.set(status); - closedLatch.countDown(); - } - }; + InProcessChannelBuilder.forName(dataPlaneServerName) + .executor(Executors.newSingleThreadExecutor()) + .build()); - CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + final CountDownLatch appCloseLatch = new CountDownLatch(1); + final AtomicReference appStatus = new AtomicReference<>(); ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); - proxyCall.start(appListener, new Metadata()); + interceptCall(interceptor, + METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); + proxyCall.start(new ClientCall.Listener() { + @Override public void onClose(Status status, Metadata trailers) { + appStatus.set(status); + appCloseLatch.countDown(); + } + }, new Metadata()); proxyCall.request(1); proxyCall.sendMessage("test"); - proxyCall.halfClose(); + try { + proxyCall.halfClose(); + } catch (IllegalStateException ignored) { + // ignore + } - // Verify stream failed - assertThat(streamActiveLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(30, TimeUnit.SECONDS)).isTrue(); - // Verify data plane call still succeeded (observability mode ignores ext_proc failure) - assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(closedStatus.get().isOk()).isTrue(); + // Call should succeed due to fail-open + assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.OK); - proxyCall.cancel("Cleanup", null); channelManager.close(); } - // --- Category 27: Request-Scoped Context Propagation --- - @Test - public void clientInterceptor_contextPropagatedToStartCall() throws Exception { + public void givenResponseHeadersResponse_whenStatusIsContinueAndReplace_thenFails() + throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { + + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch sidecarFinishedLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { @Override - public StreamObserver process( - StreamObserver responseObserver) { - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - } + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setStatus(CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) + .build()) + .build()) + .build()); + sidecarLatch.countDown(); + responseObserver.onCompleted(); + } + } - @Override - public void onError(Throwable t) {} + @Override + public void onError(Throwable t) { + sidecarFinishedLatch.countDown(); + } - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; + @Override + public void onCompleted() { + sidecarFinishedLatch.countDown(); + responseObserver.onCompleted(); } }; - - ExecutorService extProcServerExecutor = Executors.newSingleThreadExecutor(); + } + }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .executor(extProcServerExecutor) + .executor(Executors.newSingleThreadExecutor()) .build().start()); + // Enable response headers and fail-open ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setFailureModeAllow(true) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); ExternalProcessorFilterConfig filterConfig = provider.parseFilterConfig(Any.pack(proto), filterContext).config; - ExecutorService extProcChannelExecutor = Executors.newSingleThreadExecutor(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(extProcChannelExecutor) + .executor(Executors.newSingleThreadExecutor()) .build()); }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final Context.Key testKey = Context.key("test-key"); - Context testContext = Context.current().withValue(testKey, "test-value"); - final AtomicReference contextValueAtDownstreamStart = new AtomicReference<>(); - final CountDownLatch downstreamStartLatch = new CountDownLatch(1); - - ClientInterceptor assertInterceptor = new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, CallOptions callOptions, Channel next) { - return new SimpleForwardingClientCall(next.newCall(method, callOptions)) { - @Override - public void start(ClientCall.Listener responseListener, Metadata headers) { - contextValueAtDownstreamStart.set(testKey.get()); - super.start(responseListener, headers); - downstreamStartLatch.countDown(); - } - }; - } - }; - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("response-msg"); + responseObserver.onNext("Hello"); responseObserver.onCompleted(); })).build()); - ExecutorService dataPlaneChannelExecutor = Executors.newSingleThreadExecutor(); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName) - .intercept(assertInterceptor) - .executor(dataPlaneChannelExecutor) + .executor(Executors.newSingleThreadExecutor()) .build()); - final AtomicReference> proxyCallRef = new AtomicReference<>(); - ExecutorService callExecutor = Executors.newSingleThreadExecutor(); - try { - testContext.run(() -> { - ClientCall proxyCall = interceptCall( - interceptor, + final CountDownLatch appCloseLatch = new CountDownLatch(1); + final AtomicReference appStatus = new AtomicReference<>(); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(callExecutor), + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - proxyCallRef.set(proxyCall); - proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - }); - - ClientCall proxyCall = proxyCallRef.get(); + proxyCall.start(new ClientCall.Listener() { + @Override public void onClose(Status status, Metadata trailers) { + appStatus.set(status); + appCloseLatch.countDown(); + } + }, new Metadata()); - proxyCall.request(1); - proxyCall.sendMessage("hello"); + proxyCall.request(1); + proxyCall.sendMessage("test"); + try { proxyCall.halfClose(); + } catch (IllegalStateException ignored) { + // ignore + } - assertThat(downstreamStartLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(contextValueAtDownstreamStart.get()).isEqualTo("test-value"); + assertThat(sidecarLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(30, TimeUnit.SECONDS)).isTrue(); - proxyCall.cancel("cleanup", null); - } finally { - channelManager.close(); - shutdownAndAwaitTermination(extProcServerExecutor); - shutdownAndAwaitTermination(extProcChannelExecutor); - shutdownAndAwaitTermination(dataPlaneChannelExecutor); - shutdownAndAwaitTermination(callExecutor); - } + // The call should succeed due to fail-open + assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.OK); + + channelManager.close(); } @Test - public void clientInterceptor_contextPropagatedToListenerCallbacks() throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + public void givenExtProcCall_whenExecutionSucceeds_thenAll4MetricsAreRecorded() throws Exception { + final String uniqueExtProcServerName = "ext-proc-server-metrics-" + java.util.UUID.randomUUID(); + final CountDownLatch sidecarRequestHeadersLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + + // In-process mock server for External Processor + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override + @SuppressWarnings("unchecked") public StreamObserver process( StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { @@ -14167,15 +14215,18 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); + sidecarRequestHeadersLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); + sidecarLatch.countDown(); } } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override public void onCompleted() { @@ -14185,12 +14236,12 @@ public void onCompleted() { } }; - ExecutorService extProcServerExecutor = Executors.newSingleThreadExecutor(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .executor(extProcServerExecutor) + .executor(Executors.newSingleThreadExecutor()) .build().start()); + // Enable request headers and response headers ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) @@ -14200,235 +14251,313 @@ public void onCompleted() { ExternalProcessorFilterConfig filterConfig = provider.parseFilterConfig(Any.pack(proto), filterContext).config; - ExecutorService extProcChannelExecutor = Executors.newSingleThreadExecutor(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .executor(extProcChannelExecutor) + .executor(Executors.newSingleThreadExecutor()) .build()); }); + // Mock MetricRecorder to assert records + io.grpc.MetricRecorder mockMetricRecorder = Mockito.mock(io.grpc.MetricRecorder.class); + Filter.FilterContext customContext = Filter.FilterContext.create( + "envoy.ext_proc", + mockMetricRecorder); + + ScheduledExecutorService realScheduler = Executors.newSingleThreadScheduledExecutor(); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); + filterConfig, channelManager, realScheduler, customContext); + final CountDownLatch dataPlaneLatch = new CountDownLatch(1); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { - responseObserver.onNext("response-msg"); - responseObserver.onCompleted(); + new Thread(() -> { + try { + if (dataPlaneLatch.await(10, TimeUnit.SECONDS)) { + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); + } + } catch (InterruptedException e) { + responseObserver.onError(e); + } + }).start(); })).build()); - ExecutorService dataPlaneChannelExecutor = Executors.newSingleThreadExecutor(); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName) - .executor(dataPlaneChannelExecutor) + .overrideAuthority("xds:///target-service-metric") + .executor(Executors.newSingleThreadExecutor()) .build()); - final Context.Key testKey = Context.key("test-key"); - Context testContext = Context.current().withValue(testKey, "test-value"); - - final AtomicReference onHeadersContext = new AtomicReference<>(); - final AtomicReference onMessageContext = new AtomicReference<>(); - final AtomicReference onCloseContext = new AtomicReference<>(); - final AtomicReference onReadyContext = new AtomicReference<>(); - final CountDownLatch latch = new CountDownLatch(1); + final CountDownLatch appCloseLatch = new CountDownLatch(1); + ClientCall proxyCall = + interceptCall(interceptor, + METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()) + .withOption(XdsNameResolver.CLUSTER_SELECTION_KEY, "backend-service-metric"), + dataPlaneChannel); - ClientCall.Listener appListener = new ClientCall.Listener() { - @Override - public void onHeaders(Metadata headers) { - onHeadersContext.set(testKey.get()); + proxyCall.start(new ClientCall.Listener() { + @Override public void onClose(Status status, Metadata trailers) { + appCloseLatch.countDown(); } + }, new Metadata()); - @Override - public void onMessage(String message) { - onMessageContext.set(testKey.get()); - } + proxyCall.request(1); + proxyCall.sendMessage("test"); + proxyCall.halfClose(); - @Override - public void onClose(Status status, Metadata trailers) { - onCloseContext.set(testKey.get()); - latch.countDown(); - } + // 1. Wait for mock Ext Proc to receive and process client request headers + assertThat(sidecarRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); - @Override - public void onReady() { - onReadyContext.set(testKey.get()); - } - }; + // 2. Release the data plane server to respond back to the client call + dataPlaneLatch.countDown(); - final AtomicReference> proxyCallRef = new AtomicReference<>(); - ExecutorService callExecutor = Executors.newSingleThreadExecutor(); - try { - testContext.run(() -> { - ClientCall proxyCall = interceptCall( - interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(callExecutor), - dataPlaneChannel); - proxyCallRef.set(proxyCall); - proxyCall.start(appListener, new Metadata()); - }); + // 3. Assert that all stages complete in sequence deterministically + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); - ClientCall proxyCall = proxyCallRef.get(); + // Clean up and close the Ext Proc stream to release in-process server/channel resources cleanly + proxyCall.cancel("Cleanup", null); - proxyCall.request(1); - proxyCall.sendMessage("hello"); - proxyCall.halfClose(); + // Verify that the 4 duration metrics were recorded with proper labels! + Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( + Mockito.eq(ExternalProcessorClientInterceptor.clientHeadersDuration), + Mockito.anyDouble(), + Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric")), + Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric"))); - assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); + Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( + Mockito.eq(ExternalProcessorClientInterceptor.clientHalfCloseDuration), + Mockito.anyDouble(), + Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric")), + Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric"))); - assertThat(onHeadersContext.get()).isEqualTo("test-value"); - assertThat(onMessageContext.get()).isEqualTo("test-value"); - assertThat(onCloseContext.get()).isEqualTo("test-value"); - assertThat(onReadyContext.get()).isEqualTo("test-value"); + Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( + Mockito.eq(ExternalProcessorClientInterceptor.serverHeadersDuration), + Mockito.anyDouble(), + Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric")), + Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric"))); - proxyCall.cancel("cleanup", null); - } finally { - channelManager.close(); - shutdownAndAwaitTermination(extProcServerExecutor); - shutdownAndAwaitTermination(extProcChannelExecutor); - shutdownAndAwaitTermination(dataPlaneChannelExecutor); - shutdownAndAwaitTermination(callExecutor); - } + Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( + Mockito.eq(ExternalProcessorClientInterceptor.serverTrailersDuration), + Mockito.anyDouble(), + Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric")), + Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric"))); + + channelManager.close(); + realScheduler.shutdown(); } @Test - public void clientInterceptor_contextPropagatedToExtProcStub() throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + public void givenExtProcCall_whenExecutionFails_thenAll4MetricsAreRecorded() throws Exception { + final String uniqueExtProcServerName = + "ext-proc-server-metrics-fail-" + java.util.UUID.randomUUID(); + final CountDownLatch sidecarRequestHeadersLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + + // In-process mock server for External Processor + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override + @SuppressWarnings("unchecked") public StreamObserver process( StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override - public void onNext(ProcessingRequest request) {} + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + sidecarRequestHeadersLatch.countDown(); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + sidecarLatch.countDown(); + } + } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override - public void onCompleted() {} + public void onCompleted() { + responseObserver.onCompleted(); + } }; } }; - ExecutorService extProcServerExecutor = Executors.newSingleThreadExecutor(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .executor(extProcServerExecutor) + .executor(Executors.newSingleThreadExecutor()) .build().start()); - ExternalProcessor proto = createBaseProto(uniqueExtProcServerName).build(); + // Enable request headers and response headers + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); ExternalProcessorFilterConfig filterConfig = provider.parseFilterConfig(Any.pack(proto), filterContext).config; - final Context.Key testKey = Context.key("test-key"); - Context testContext = Context.current().withValue(testKey, "test-value"); - final AtomicReference contextAtExtProcCall = new AtomicReference<>(); - final CountDownLatch extProcCallLatch = new CountDownLatch(1); - - ExecutorService extProcChannelExecutor = Executors.newSingleThreadExecutor(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) - .intercept(new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, CallOptions callOptions, Channel next) { - if (method.equals(ExternalProcessorGrpc.getProcessMethod())) { - contextAtExtProcCall.set(testKey.get()); - extProcCallLatch.countDown(); - } - return next.newCall(method, callOptions); - } - }) - .executor(extProcChannelExecutor) + .executor(Executors.newSingleThreadExecutor()) .build()); }); - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); + // Mock MetricRecorder to assert records + io.grpc.MetricRecorder mockMetricRecorder = Mockito.mock(io.grpc.MetricRecorder.class); + Filter.FilterContext customContext = Filter.FilterContext.create( + "envoy.ext_proc", + mockMetricRecorder); + + ScheduledExecutorService realScheduler = Executors.newSingleThreadScheduledExecutor(); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, realScheduler, customContext); + + final CountDownLatch dataPlaneLatch = new CountDownLatch(1); + dataPlaneServiceRegistry.addService(ServerInterceptors.intercept( + ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + new Thread(() -> { + try { + if (dataPlaneLatch.await(10, TimeUnit.SECONDS)) { + responseObserver.onError( + Status.UNAUTHENTICATED + .withDescription("authentication failed") + .asRuntimeException()); + } + } catch (InterruptedException e) { + responseObserver.onError(e); + } + }).start(); + })).build(), + new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + call.sendHeaders(new Metadata()); + return next.startCall(call, headers); + } + })); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .overrideAuthority("xds:///target-service-metric-fail") + .executor(Executors.newSingleThreadExecutor()) + .build()); + + final AtomicReference appStatus = new AtomicReference<>(); + final CountDownLatch appCloseLatch = new CountDownLatch(1); + ClientCall proxyCall = + interceptCall(interceptor, + METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()) + .withOption(XdsNameResolver.CLUSTER_SELECTION_KEY, "backend-service-metric-fail"), + dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() { + @Override public void onClose(Status status, Metadata trailers) { + appStatus.set(status); + appCloseLatch.countDown(); + } + }, new Metadata()); + + proxyCall.request(1); + proxyCall.sendMessage("test"); + proxyCall.halfClose(); - ExecutorService dataPlaneChannelExecutor = Executors.newSingleThreadExecutor(); - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .executor(dataPlaneChannelExecutor) - .build()); + // 1. Wait for mock Ext Proc to receive and process client request headers + assertThat(sidecarRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); - final AtomicReference> proxyCallRef = new AtomicReference<>(); - ExecutorService callExecutor = Executors.newSingleThreadExecutor(); - try { - testContext.run(() -> { - ClientCall proxyCall = interceptCall( - interceptor, - METHOD_SAY_HELLO, - DEFAULT_CALL_OPTIONS.withExecutor(callExecutor), - dataPlaneChannel); - proxyCallRef.set(proxyCall); - proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - }); + // 2. Release the data plane server to respond back with error + dataPlaneLatch.countDown(); - ClientCall proxyCall = proxyCallRef.get(); + // 3. Assert that all stages complete + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcCallLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(contextAtExtProcCall.get()).isEqualTo("test-value"); + assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.UNAUTHENTICATED); - proxyCall.cancel("cleanup", null); - } finally { - channelManager.close(); - shutdownAndAwaitTermination(extProcServerExecutor); - shutdownAndAwaitTermination(extProcChannelExecutor); - shutdownAndAwaitTermination(dataPlaneChannelExecutor); - shutdownAndAwaitTermination(callExecutor); - } - } + // Clean up and close the Ext Proc stream + proxyCall.cancel("Cleanup", null); - // --- Category 28: Header Option Value Spec Compliance and Validation --- + // Verify that the 4 duration metrics were recorded with proper labels! + Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( + Mockito.eq(ExternalProcessorClientInterceptor.clientHeadersDuration), + Mockito.anyDouble(), + Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric-fail")), + Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric-fail"))); + + Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( + Mockito.eq(ExternalProcessorClientInterceptor.clientHalfCloseDuration), + Mockito.anyDouble(), + Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric-fail")), + Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric-fail"))); + + Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( + Mockito.eq(ExternalProcessorClientInterceptor.serverHeadersDuration), + Mockito.anyDouble(), + Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric-fail")), + Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric-fail"))); + + Mockito.verify(mockMetricRecorder, Mockito.times(1)).recordDoubleHistogram( + Mockito.eq(ExternalProcessorClientInterceptor.serverTrailersDuration), + Mockito.anyDouble(), + Mockito.eq(com.google.common.collect.ImmutableList.of("xds:///target-service-metric-fail")), + Mockito.eq(com.google.common.collect.ImmutableList.of("backend-service-metric-fail"))); + + channelManager.close(); + realScheduler.shutdown(); + } + // --- Category 26: Call activation with failure mode allow on and off --- @Test - @SuppressWarnings("unchecked") - public void serialization_specCompliance() throws Exception { + public void + givenRequestHeaderModeSend_Fma_true_whenExtProcTerminates_thenCallIsActivated() + throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + uniqueExtProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl( - "type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") - .build()) - .build()) - .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setFailureModeAllow(true) .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final AtomicReference> responseObserverRef = + new AtomicReference<>(); + final CountDownLatch streamActiveLatch = new CountDownLatch(1); - final CountDownLatch requestSentLatch = new CountDownLatch(1); - final AtomicReference capturedRequest = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); + responseObserverRef.set(responseObserver); + streamActiveLatch.countDown(); return new StreamObserver() { @Override - public void onNext(ProcessingRequest request) { - capturedRequest.set(request); - requestSentLatch.countDown(); - } + public void onNext(ProcessingRequest request) {} @Override public void onError(Throwable t) {} @Override - public void onCompleted() { - responseObserver.onCompleted(); - } + public void onCompleted() {} }; } }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -14436,123 +14565,102 @@ public void onCompleted() { CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + MutableHandlerRegistry dataPlaneRegistry = new MutableHandlerRegistry(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) + .fallbackHandlerRegistry(dataPlaneRegistry) + .directExecutor() + .build().start()); - CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); - ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + dataPlaneRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + responseObserver.onNext("response"); + responseObserver.onCompleted(); + })).build()); - Metadata headers = new Metadata(); - headers.put(Metadata.Key.of("custom-ascii", Metadata.ASCII_STRING_MARSHALLER), "hello-world"); - headers.put( - Metadata.Key.of("custom-bin", Metadata.BINARY_BYTE_MARSHALLER), - new byte[]{0x00, 0x01, 0x02}); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) + .directExecutor() + .build()); - proxyCall.start(new ClientCall.Listener() {}, headers); + ClientCall clientCall = interceptCall( + interceptor, METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); - assertThat(requestSentLatch.await(5, TimeUnit.SECONDS)).isTrue(); - ProcessingRequest req = capturedRequest.get(); - assertThat(req.hasRequestHeaders()).isTrue(); - - // Find our headers in the captured request - io.envoyproxy.envoy.config.core.v3.HeaderMap headerMap = req.getRequestHeaders().getHeaders(); - io.envoyproxy.envoy.config.core.v3.HeaderValue customAsciiProto = null; - io.envoyproxy.envoy.config.core.v3.HeaderValue customBinProto = null; - for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : headerMap.getHeadersList()) { - if (hv.getKey().equals("custom-ascii")) { - customAsciiProto = hv; - } else if (hv.getKey().equals("custom-bin")) { - customBinProto = hv; + final CountDownLatch callCompletedLatch = new CountDownLatch(1); + final AtomicReference closedStatus = new AtomicReference<>(); + clientCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + closedStatus.set(status); + callCompletedLatch.countDown(); } - } + }, new Metadata()); + clientCall.request(1); - assertThat(customAsciiProto).isNotNull(); - // ASCII: value is not set, raw_value is set to the ASCII string bytes - assertThat(customAsciiProto.getValue()).isEmpty(); - assertThat(customAsciiProto.getRawValue().toStringUtf8()).isEqualTo("hello-world"); + boolean active = streamActiveLatch.await(5, TimeUnit.SECONDS); + assertThat(active).isTrue(); - assertThat(customBinProto).isNotNull(); - // Binary: value is not set, raw_value is set to base64-encoded bytes - assertThat(customBinProto.getValue()).isEmpty(); - String expectedBase64 = BaseEncoding.base64().encode(new byte[]{0x00, 0x01, 0x02}); - assertThat(customBinProto.getRawValue().toStringUtf8()).isEqualTo(expectedBase64); + clientCall.sendMessage("app-msg"); + clientCall.halfClose(); + + responseObserverRef.get().onError(new RuntimeException("Stream failure during start")); + + boolean completed = callCompletedLatch.await(5, TimeUnit.SECONDS); + assertThat(completed).isTrue(); + // Verify call completed successfully due to FMA true (fail-open) + assertThat(closedStatus.get().isOk()).isTrue(); - proxyCall.cancel("Cleanup", null); channelManager.close(); } @Test - @SuppressWarnings("unchecked") - public void deserialization_preferRawValue() throws Exception { + public void + givenRequestHeaderModeSend_Fma_false_whenExtProcTerminates_thenCallIsClosed() + throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + uniqueExtProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl( - "type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") - .build()) - .build()) - .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) - .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setHeaderMutation(HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("custom-ascii") - .setValue("legacy-val") - .setRawValue(ByteString.copyFromUtf8("raw-val")) - .build()) - .build()) - .build()) - .build()) - .build()) - .build()); - } - } + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setFailureModeAllow(false) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final AtomicReference> responseObserverRef = + new AtomicReference<>(); + final CountDownLatch streamActiveLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + streamActiveLatch.countDown(); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) {} @Override public void onError(Throwable t) {} @Override - public void onCompleted() { - responseObserver.onCompleted(); - } + public void onCompleted() {} }; } }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -14560,108 +14668,96 @@ public void onCompleted() { CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch dataPlaneLatch = new CountDownLatch(1); - MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); + MutableHandlerRegistry dataPlaneRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) - .fallbackHandlerRegistry(uniqueRegistry) + .fallbackHandlerRegistry(dataPlaneRegistry) .directExecutor() .build().start()); - uniqueRegistry.addService(ServerInterceptors.intercept( - ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello " + request); - responseObserver.onCompleted(); - })) - .build(), - new ServerInterceptor() { - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - capturedHeaders.set(headers); - dataPlaneLatch.countDown(); - return next.startCall(call, headers); - } - })); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + dataPlaneRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + responseObserver.onNext("response"); + responseObserver.onCompleted(); + })).build()); - CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); - ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); - proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) + .directExecutor() + .build()); - assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); - Metadata headersApplied = capturedHeaders.get(); - // It should have chosen raw_value ("raw-val") and ignored value ("legacy-val") - assertThat( - headersApplied.get( - Metadata.Key.of("custom-ascii", Metadata.ASCII_STRING_MARSHALLER))) - .isEqualTo("raw-val"); + ClientCall clientCall = interceptCall( + interceptor, METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); + + final CountDownLatch callCompletedLatch = new CountDownLatch(1); + final AtomicReference closedStatus = new AtomicReference<>(); + clientCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + closedStatus.set(status); + callCompletedLatch.countDown(); + } + }, new Metadata()); + clientCall.request(1); + + boolean active = streamActiveLatch.await(5, TimeUnit.SECONDS); + assertThat(active).isTrue(); + + // Terminate stream with error + responseObserverRef.get().onError(new RuntimeException("Stream failure during start")); + + boolean completed = callCompletedLatch.await(5, TimeUnit.SECONDS); + assertThat(completed).isTrue(); + // Verify call closed with INTERNAL status due to FMA false + assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(closedStatus.get().getDescription()).contains("External processor stream failed"); - proxyCall.cancel("Cleanup", null); channelManager.close(); } @Test - @SuppressWarnings("unchecked") - public void deserialization_binaryHeader_validBase64() throws Exception { + public void givenFailureModeAllowTrue_whenExtProcStreamFailsAfterRequestBodySent_thenCallFails() + throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + uniqueExtProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl( - "type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") - .build()) - .build()) - .build()) + + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setFailureModeAllow(true) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .build()) .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final AtomicReference> responseObserverRef = + new AtomicReference<>(); + final CountDownLatch streamActiveLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); + StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + streamActiveLatch.countDown(); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setHeaderMutation(HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("custom-bin") - .setRawValue( - ByteString.copyFromUtf8("YmFy")) - .build()) - .build()) - .build()) - .build()) - .build()) + .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } } @@ -14670,12 +14766,11 @@ public void onNext(ProcessingRequest request) { public void onError(Throwable t) {} @Override - public void onCompleted() { - responseObserver.onCompleted(); - } + public void onCompleted() {} }; } }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -14683,108 +14778,116 @@ public void onCompleted() { CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch dataPlaneLatch = new CountDownLatch(1); - MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); + MutableHandlerRegistry dataPlaneRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) - .fallbackHandlerRegistry(uniqueRegistry) + .fallbackHandlerRegistry(dataPlaneRegistry) .directExecutor() .build().start()); - uniqueRegistry.addService(ServerInterceptors.intercept( - ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello " + request); - responseObserver.onCompleted(); - })) - .build(), - new ServerInterceptor() { - @Override - public ServerCall.Listener interceptCall( - ServerCall call, Metadata headers, ServerCallHandler next) { - capturedHeaders.set(headers); - dataPlaneLatch.countDown(); - return next.startCall(call, headers); - } - })); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + dataPlaneRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + responseObserver.onNext("response"); + responseObserver.onCompleted(); + })).build()); - CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); - ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); - proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) + .directExecutor() + .build()); - assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); - Metadata headersApplied = capturedHeaders.get(); - // It should have base64 decoded "YmFy" to "bar" - byte[] binValue = - headersApplied.get(Metadata.Key.of("custom-bin", Metadata.BINARY_BYTE_MARSHALLER)); - assertThat(binValue).isEqualTo(new byte[]{'b', 'a', 'r'}); + ClientCall clientCall = interceptCall( + interceptor, METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); + + final AtomicReference closedStatus = new AtomicReference<>(); + final CountDownLatch callCompletedLatch = new CountDownLatch(1); + clientCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + closedStatus.set(status); + callCompletedLatch.countDown(); + } + }, new Metadata()); + clientCall.request(1); + + boolean active = streamActiveLatch.await(5, TimeUnit.SECONDS); + assertThat(active).isTrue(); + + clientCall.sendMessage("app-msg"); + clientCall.halfClose(); + + // Now abruptly fail the stream + responseObserverRef.get() + .onError(new RuntimeException("Stream failure after sending body/EOS")); + + // Verify that the call failed with INTERNAL status instead of succeeding. + boolean completed = callCompletedLatch.await(5, TimeUnit.SECONDS); + assertThat(completed).isTrue(); + assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(closedStatus.get().getDescription()).contains("External processor stream failed"); - proxyCall.cancel("Cleanup", null); channelManager.close(); } @Test - @SuppressWarnings("unchecked") - public void deserialization_binaryHeader_invalidBase64_noError_fails() throws Exception { + public void givenFailureModeAllowTrue_whenExtProcStreamFailsAfterResponseBodySent_thenCallFails() + throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + uniqueExtProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl( - "type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") - .build()) - .build()) - .build()) + + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setFailureModeAllow(true) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final AtomicReference> responseObserverRef = + new AtomicReference<>(); + final CountDownLatch streamActiveLatch = new CountDownLatch(1); + final CountDownLatch streamFailedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); + StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + streamActiveLatch.countDown(); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setHeaderMutation(HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("custom-bin") - .setRawValue( - ByteString.copyFromUtf8("invalid_base64!")) - .build()) - .build()) - .build()) - .build()) - .build()) + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); + } else if (request.hasResponseBody()) { + // Fail the stream once we see response body message + responseObserver.onError( + Status.INTERNAL.withDescription("Stream failure after response body") + .asRuntimeException()); + streamFailedLatch.countDown(); } } @@ -14792,12 +14895,11 @@ public void onNext(ProcessingRequest request) { public void onError(Throwable t) {} @Override - public void onCompleted() { - responseObserver.onCompleted(); - } + public void onCompleted() {} }; } }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -14805,93 +14907,93 @@ public void onCompleted() { CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + MutableHandlerRegistry dataPlaneRegistry = new MutableHandlerRegistry(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) + .fallbackHandlerRegistry(dataPlaneRegistry) + .directExecutor() + .build().start()); - CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); - ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + dataPlaneRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + responseObserver.onNext("response-body-msg"); + responseObserver.onCompleted(); + })).build()); - final AtomicReference capturedStatus = new AtomicReference<>(); - final CountDownLatch callClosedLatch = new CountDownLatch(1); - proxyCall.start(new ClientCall.Listener() { + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName) + .directExecutor() + .build()); + + ClientCall clientCall = interceptCall( + interceptor, METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); + + final AtomicReference closedStatus = new AtomicReference<>(); + final CountDownLatch callCompletedLatch = new CountDownLatch(1); + clientCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - capturedStatus.set(status); - callClosedLatch.countDown(); + closedStatus.set(status); + callCompletedLatch.countDown(); } }, new Metadata()); + clientCall.request(1); - assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); + boolean active = streamActiveLatch.await(5, TimeUnit.SECONDS); + assertThat(active).isTrue(); + + // Since request body mode is NONE, this sendMessage is NOT sent to ext_proc + clientCall.sendMessage("app-msg"); + clientCall.halfClose(); + + // Verify call completed and failed with INTERNAL status + boolean completed = callCompletedLatch.await(5, TimeUnit.SECONDS); + assertThat(completed).isTrue(); + assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(closedStatus.get().getDescription()).contains("External processor stream failed"); channelManager.close(); } @Test - @SuppressWarnings("unchecked") - public void deserialization_binaryHeader_invalidBase64_failsCall() throws Exception { + public void givenObservabilityTrue_whenExtProcStreamFails_thenCallContinues() + throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + uniqueExtProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl( - "type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") - .build()) - .build()) - .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) - .setMutationRules( - io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules - .newBuilder() - .setDisallowIsError(com.google.protobuf.BoolValue.of(true)) - .build()) + + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setFailureModeAllow(false) + .setObservabilityMode(true) .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final CountDownLatch streamActiveLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); + StreamObserver responseObserver) { + streamActiveLatch.countDown(); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setHeaderMutation(HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("custom-bin") - .setRawValue( - ByteString.copyFromUtf8("invalid_base64!")) - .build()) - .build()) - .build()) - .build()) - .build()) - .build()); + // Fail the stream immediately on receiving headers + responseObserver.onError( + Status.INTERNAL.withDescription("Simulated sidecar failure") + .asRuntimeException()); } } @@ -14899,12 +15001,11 @@ public void onNext(ProcessingRequest request) { public void onError(Throwable t) {} @Override - public void onCompleted() { - responseObserver.onCompleted(); - } + public void onCompleted() {} }; } }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -14912,90 +15013,74 @@ public void onCompleted() { CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + final CountDownLatch dataPlaneLatch = new CountDownLatch(1); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + dataPlaneLatch.countDown(); + })) + .build()); - CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); - ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - final AtomicReference capturedStatus = new AtomicReference<>(); - final CountDownLatch callClosedLatch = new CountDownLatch(1); - proxyCall.start(new ClientCall.Listener() { + final CountDownLatch closedLatch = new CountDownLatch(1); + final AtomicReference closedStatus = new AtomicReference<>(); + ClientCall.Listener appListener = new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { - capturedStatus.set(status); - callClosedLatch.countDown(); + closedStatus.set(status); + closedLatch.countDown(); } - }, new Metadata()); + }; - assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + proxyCall.start(appListener, new Metadata()); - channelManager.close(); - } + proxyCall.request(1); + proxyCall.sendMessage("test"); + proxyCall.halfClose(); - @Test - @SuppressWarnings("unchecked") - public void deserialization_asciiHeader_invalidChars_noError_fails() throws Exception { - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + uniqueExtProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl( - "type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") - .build()) - .build()) - .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) - .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; + // Verify stream failed + assertThat(streamActiveLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // Verify data plane call still succeeded (observability mode ignores ext_proc failure) + assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(closedStatus.get().isOk()).isTrue(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + // --- Category 27: Request-Scoped Context Propagation --- + + @Test + public void clientInterceptor_contextPropagatedToStartCall() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); + StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setHeaderMutation(HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("custom-ascii") - .setRawValue( - ByteString.copyFromUtf8( - "value_with_newline\n")) - .build()) - .build()) - .build()) - .build()) - .build()) - .build()); - } + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); } @Override @@ -15008,100 +15093,114 @@ public void onCompleted() { }; } }; + + ExecutorService extProcServerExecutor = Executors.newSingleThreadExecutor(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .directExecutor() + .executor(extProcServerExecutor) .build().start()); + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; + + ExecutorService extProcChannelExecutor = Executors.newSingleThreadExecutor(); CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(extProcChannelExecutor) + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); - - CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); - ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + final Context.Key testKey = Context.key("test-key"); + Context testContext = Context.current().withValue(testKey, "test-value"); + final AtomicReference contextValueAtDownstreamStart = new AtomicReference<>(); + final CountDownLatch downstreamStartLatch = new CountDownLatch(1); - final AtomicReference capturedStatus = new AtomicReference<>(); - final CountDownLatch callClosedLatch = new CountDownLatch(1); - proxyCall.start(new ClientCall.Listener() { + ClientInterceptor assertInterceptor = new ClientInterceptor() { @Override - public void onClose(Status status, Metadata trailers) { - capturedStatus.set(status); - callClosedLatch.countDown(); + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new SimpleForwardingClientCall(next.newCall(method, callOptions)) { + @Override + public void start(ClientCall.Listener responseListener, Metadata headers) { + contextValueAtDownstreamStart.set(testKey.get()); + super.start(responseListener, headers); + downstreamStartLatch.countDown(); + } + }; } - }, new Metadata()); + }; - assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + responseObserver.onNext("response-msg"); + responseObserver.onCompleted(); + })).build()); - channelManager.close(); + ExecutorService dataPlaneChannelExecutor = Executors.newSingleThreadExecutor(); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept(assertInterceptor) + .executor(dataPlaneChannelExecutor) + .build()); + + final AtomicReference> proxyCallRef = new AtomicReference<>(); + ExecutorService callExecutor = Executors.newSingleThreadExecutor(); + try { + testContext.run(() -> { + ClientCall proxyCall = interceptCall( + interceptor, + METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(callExecutor), + dataPlaneChannel); + proxyCallRef.set(proxyCall); + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + }); + + ClientCall proxyCall = proxyCallRef.get(); + + proxyCall.request(1); + proxyCall.sendMessage("hello"); + proxyCall.halfClose(); + + assertThat(downstreamStartLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(contextValueAtDownstreamStart.get()).isEqualTo("test-value"); + + proxyCall.cancel("cleanup", null); + } finally { + channelManager.close(); + shutdownAndAwaitTermination(extProcServerExecutor); + shutdownAndAwaitTermination(extProcChannelExecutor); + shutdownAndAwaitTermination(dataPlaneChannelExecutor); + shutdownAndAwaitTermination(callExecutor); + } } @Test - @SuppressWarnings("unchecked") - public void deserialization_asciiHeader_invalidCharacters_failsCall() throws Exception { + public void clientInterceptor_contextPropagatedToListenerCallbacks() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + uniqueExtProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl( - "type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") - .build()) - .build()) - .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) - .setMutationRules( - io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules - .newBuilder() - .setDisallowIsError(com.google.protobuf.BoolValue.of(true)) - .build()) - .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; - - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); + StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setHeaderMutation(HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("custom-ascii") - .setRawValue( - ByteString.copyFromUtf8( - "value_with_newline\n")) - .build()) - .build()) - .build()) - .build()) - .build()) + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } } @@ -15116,153 +15215,209 @@ public void onCompleted() { }; } }; + + ExecutorService extProcServerExecutor = Executors.newSingleThreadExecutor(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .directExecutor() + .executor(extProcServerExecutor) .build().start()); + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; + + ExecutorService extProcChannelExecutor = Executors.newSingleThreadExecutor(); CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) + .executor(extProcChannelExecutor) + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall((request, responseObserver) -> { + responseObserver.onNext("response-msg"); + responseObserver.onCompleted(); + })).build()); + + ExecutorService dataPlaneChannelExecutor = Executors.newSingleThreadExecutor(); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .executor(dataPlaneChannelExecutor) + .build()); + + final Context.Key testKey = Context.key("test-key"); + Context testContext = Context.current().withValue(testKey, "test-value"); + + final AtomicReference onHeadersContext = new AtomicReference<>(); + final AtomicReference onMessageContext = new AtomicReference<>(); + final AtomicReference onCloseContext = new AtomicReference<>(); + final AtomicReference onReadyContext = new AtomicReference<>(); + final CountDownLatch latch = new CountDownLatch(1); + + ClientCall.Listener appListener = new ClientCall.Listener() { + @Override + public void onHeaders(Metadata headers) { + onHeadersContext.set(testKey.get()); + } + + @Override + public void onMessage(String message) { + onMessageContext.set(testKey.get()); + } + + @Override + public void onClose(Status status, Metadata trailers) { + onCloseContext.set(testKey.get()); + latch.countDown(); + } + + @Override + public void onReady() { + onReadyContext.set(testKey.get()); + } + }; + + final AtomicReference> proxyCallRef = new AtomicReference<>(); + ExecutorService callExecutor = Executors.newSingleThreadExecutor(); + try { + testContext.run(() -> { + ClientCall proxyCall = interceptCall( + interceptor, + METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(callExecutor), + dataPlaneChannel); + proxyCallRef.set(proxyCall); + proxyCall.start(appListener, new Metadata()); + }); + + ClientCall proxyCall = proxyCallRef.get(); - CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); - ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + proxyCall.request(1); + proxyCall.sendMessage("hello"); + proxyCall.halfClose(); - final AtomicReference capturedStatus = new AtomicReference<>(); - final CountDownLatch callClosedLatch = new CountDownLatch(1); - proxyCall.start(new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - capturedStatus.set(status); - callClosedLatch.countDown(); - } - }, new Metadata()); + assertThat(latch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); + assertThat(onHeadersContext.get()).isEqualTo("test-value"); + assertThat(onMessageContext.get()).isEqualTo("test-value"); + assertThat(onCloseContext.get()).isEqualTo("test-value"); + assertThat(onReadyContext.get()).isEqualTo("test-value"); - channelManager.close(); + proxyCall.cancel("cleanup", null); + } finally { + channelManager.close(); + shutdownAndAwaitTermination(extProcServerExecutor); + shutdownAndAwaitTermination(extProcChannelExecutor); + shutdownAndAwaitTermination(dataPlaneChannelExecutor); + shutdownAndAwaitTermination(callExecutor); + } } @Test - @SuppressWarnings("unchecked") - public void deserialization_headerValue_tooLong_noError_fails() throws Exception { + public void clientInterceptor_contextPropagatedToExtProcStub() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + uniqueExtProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl( - "type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") - .build()) - .build()) - .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) - .build(); - ExternalProcessorFilterConfig filterConfig = - provider.parseFilterConfig(Any.pack(proto), filterContext).config; - - String longValue = new String(new char[16385]).replace('\0', 'v'); - - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); + StreamObserver responseObserver) { return new StreamObserver() { @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setHeaderMutation(HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("custom-ascii") - .setRawValue(ByteString.copyFromUtf8(longValue)) - .build()) - .build()) - .build()) - .build()) - .build()) - .build()); - } - } + public void onNext(ProcessingRequest request) {} @Override public void onError(Throwable t) {} @Override - public void onCompleted() { - responseObserver.onCompleted(); - } + public void onCompleted() {} }; } }; + + ExecutorService extProcServerExecutor = Executors.newSingleThreadExecutor(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) - .directExecutor() + .executor(extProcServerExecutor) .build().start()); + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName).build(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; + + final Context.Key testKey = Context.key("test-key"); + Context testContext = Context.current().withValue(testKey, "test-value"); + final AtomicReference contextAtExtProcCall = new AtomicReference<>(); + final CountDownLatch extProcCallLatch = new CountDownLatch(1); + + ExecutorService extProcChannelExecutor = Executors.newSingleThreadExecutor(); CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + return grpcCleanup.register(InProcessChannelBuilder.forName(uniqueExtProcServerName) + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + if (method.equals(ExternalProcessorGrpc.getProcessMethod())) { + contextAtExtProcCall.set(testKey.get()); + extProcCallLatch.countDown(); + } + return next.newCall(method, callOptions); + } + }) + .executor(extProcChannelExecutor) + .build()); }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - ManagedChannel dataPlaneChannel = - grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + ExecutorService dataPlaneChannelExecutor = Executors.newSingleThreadExecutor(); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .executor(dataPlaneChannelExecutor) + .build()); - CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); - ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + final AtomicReference> proxyCallRef = new AtomicReference<>(); + ExecutorService callExecutor = Executors.newSingleThreadExecutor(); + try { + testContext.run(() -> { + ClientCall proxyCall = interceptCall( + interceptor, + METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(callExecutor), + dataPlaneChannel); + proxyCallRef.set(proxyCall); + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + }); - final AtomicReference capturedStatus = new AtomicReference<>(); - final CountDownLatch callClosedLatch = new CountDownLatch(1); - proxyCall.start(new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - capturedStatus.set(status); - callClosedLatch.countDown(); - } - }, new Metadata()); + ClientCall proxyCall = proxyCallRef.get(); - assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); + assertThat(extProcCallLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(contextAtExtProcCall.get()).isEqualTo("test-value"); - channelManager.close(); + proxyCall.cancel("cleanup", null); + } finally { + channelManager.close(); + shutdownAndAwaitTermination(extProcServerExecutor); + shutdownAndAwaitTermination(extProcChannelExecutor); + shutdownAndAwaitTermination(dataPlaneChannelExecutor); + shutdownAndAwaitTermination(callExecutor); + } } + // --- Category 28: Header Option Value Spec Compliance and Validation --- + @Test @SuppressWarnings("unchecked") - public void deserialization_headerValue_tooLong_failsCall() throws Exception { + public void serialization_specCompliance() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - // Enable disallowIsError = true in mutation rules ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() @@ -15276,47 +15431,23 @@ public void deserialization_headerValue_tooLong_failsCall() throws Exception { .build()) .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) - .setMutationRules( - io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules - .newBuilder() - .setDisallowIsError(com.google.protobuf.BoolValue.of(false)) - .build()) .build(); ExternalProcessorFilterConfig filterConfig = provider.parseFilterConfig(Any.pack(proto), filterContext).config; - // Create a value that is 16385 characters long (exceeding 16384 limit) - String longValue = new String(new char[16385]).replace('\0', 'v'); - + final CountDownLatch requestSentLatch = new CountDownLatch(1); + final AtomicReference capturedRequest = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( - final StreamObserver responseObserver) { + StreamObserver responseObserver) { ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setHeaderMutation(HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("custom-ascii") - .setRawValue(ByteString.copyFromUtf8(longValue)) - .build()) - .build()) - .build()) - .build()) - .build()) - .build()); - } + capturedRequest.set(request); + requestSentLatch.countDown(); } @Override @@ -15344,56 +15475,72 @@ public void onCompleted() { ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); - final AtomicReference capturedStatus = new AtomicReference<>(); - final CountDownLatch callClosedLatch = new CountDownLatch(1); - proxyCall.start(new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - capturedStatus.set(status); - callClosedLatch.countDown(); + Metadata headers = new Metadata(); + headers.put(Metadata.Key.of("custom-ascii", Metadata.ASCII_STRING_MARSHALLER), "hello-world"); + headers.put( + Metadata.Key.of("custom-bin", Metadata.BINARY_BYTE_MARSHALLER), + new byte[]{0x00, 0x01, 0x02}); + + proxyCall.start(new ClientCall.Listener() {}, headers); + + assertThat(requestSentLatch.await(5, TimeUnit.SECONDS)).isTrue(); + ProcessingRequest req = capturedRequest.get(); + assertThat(req.hasRequestHeaders()).isTrue(); + + // Find our headers in the captured request + io.envoyproxy.envoy.config.core.v3.HeaderMap headerMap = req.getRequestHeaders().getHeaders(); + io.envoyproxy.envoy.config.core.v3.HeaderValue customAsciiProto = null; + io.envoyproxy.envoy.config.core.v3.HeaderValue customBinProto = null; + for (io.envoyproxy.envoy.config.core.v3.HeaderValue hv : headerMap.getHeadersList()) { + if (hv.getKey().equals("custom-ascii")) { + customAsciiProto = hv; + } else if (hv.getKey().equals("custom-bin")) { + customBinProto = hv; } - }, new Metadata()); + } - assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // The call should fail unconditionally due to IllegalArgumentException - assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); + assertThat(customAsciiProto).isNotNull(); + // ASCII: value is not set, raw_value is set to the ASCII string bytes + assertThat(customAsciiProto.getValue()).isEmpty(); + assertThat(customAsciiProto.getRawValue().toStringUtf8()).isEqualTo("hello-world"); + + assertThat(customBinProto).isNotNull(); + // Binary: value is not set, raw_value is set to base64-encoded bytes + assertThat(customBinProto.getValue()).isEmpty(); + String expectedBase64 = BaseEncoding.base64().encode(new byte[]{0x00, 0x01, 0x02}); + assertThat(customBinProto.getRawValue().toStringUtf8()).isEqualTo(expectedBase64); + proxyCall.cancel("Cleanup", null); channelManager.close(); } @Test @SuppressWarnings("unchecked") - public void testFlowControlStateInitialization() throws Exception { + public void deserialization_preferRawValue() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) + .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl( + "type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(2); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -15404,20 +15551,22 @@ public StreamObserver process( return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - receivedRequests.add(request); - extProcLatch.countDown(); if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - } else if (request.hasRequestBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getRequestBody().getBody()) - .build()) + .setHeaderMutation(HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("custom-ascii") + .setValue("legacy-val") + .setRawValue(ByteString.copyFromUtf8("raw-val")) + .build()) + .build()) .build()) .build()) .build()) @@ -15435,8 +15584,6 @@ public void onCompleted() { }; } }; - - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -15450,36 +15597,47 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello " + request); - responseObserver.onCompleted(); - })) - .build()); + final AtomicReference capturedHeaders = new AtomicReference<>(); + final CountDownLatch dataPlaneLatch = new CountDownLatch(1); + MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) + .fallbackHandlerRegistry(uniqueRegistry) + .directExecutor() + .build().start()); + uniqueRegistry.addService(ServerInterceptors.intercept( + ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + })) + .build(), + new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + capturedHeaders.set(headers); + dataPlaneLatch.countDown(); + return next.startCall(call, headers); + } + })); - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - proxyCall.sendMessage("Message 1"); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); - - assertThat(receivedRequests).hasSize(2); - ProcessingRequest firstRequest = receivedRequests.get(0); - ProcessingRequest secondRequest = receivedRequests.get(1); - - assertThat(firstRequest.hasRequestHeaders()).isTrue(); - assertThat(firstRequest.hasFlowControlInit()).isTrue(); - assertThat(firstRequest.getFlowControlInit().getInitialWindowDownstreamToSidestream()).isEqualTo(65536); - assertThat(firstRequest.getFlowControlInit().getInitialWindowSidestreamToUpstream()).isEqualTo(65536); - - assertThat(secondRequest.hasRequestBody()).isTrue(); - assertThat(secondRequest.hasFlowControlInit()).isFalse(); + assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); + Metadata headersApplied = capturedHeaders.get(); + // It should have chosen raw_value ("raw-val") and ignored value ("legacy-val") + assertThat( + headersApplied.get( + Metadata.Key.of("custom-ascii", Metadata.ASCII_STRING_MARSHALLER))) + .isEqualTo("raw-val"); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -15487,177 +15645,121 @@ public void onCompleted() { @Test @SuppressWarnings("unchecked") - public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Exception { + public void deserialization_binaryHeader_validBase64() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) + .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl( + "type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); - final CountDownLatch firstBodyLatch = new CountDownLatch(2); // Headers + First Body - final CountDownLatch secondBodyLatch = new CountDownLatch(1); - final AtomicReference> responseObserverRef = new AtomicReference<>(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( final StreamObserver responseObserver) { - responseObserverRef.set(responseObserver); ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - receivedRequests.add(request); if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) + .setRequestHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setHeaderMutation(HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("custom-bin") + .setRawValue( + ByteString.copyFromUtf8("YmFy")) + .build()) + .build()) + .build()) + .build()) + .build()) .build()); - firstBodyLatch.countDown(); - } else if (request.hasRequestBody()) { - if (firstBodyLatch.getCount() > 0) { - // This is the first body (40000 bytes) - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getRequestBody().getBody()) - .build()) - .build()) - .build()) - .build()) - .build()); - firstBodyLatch.countDown(); - } else { - // This is the second body (30000 bytes) - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getRequestBody().getBody()) - .build()) - .build()) - .build()) - .build()) - .build()); - secondBodyLatch.countDown(); - } } } - @Override - public void onError(Throwable t) {} - - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; - } - }; - - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) - .addService(extProcImpl) - .directExecutor() - .build().start()); - - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); - }); - - ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); - - final List dataPlaneReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( - new ServerCalls.ClientStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - return new StreamObserver() { - @Override - public void onNext(String value) { - dataPlaneReceivedMessages.add(value); - } - - @Override - public void onError(Throwable t) {} - - @Override - public void onCompleted() { - responseObserver.onNext("Response"); - responseObserver.onCompleted(); - } - }; - } - })) - .build()); - - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - - ClientCall proxyCall = - interceptCall(interceptor, METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - - proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - - // Generate large messages - String largeMessage40k = new String(new char[40000]).replace('\0', 'a'); - String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); - - // Send first message (40000 bytes) - fits in 65536 window - proxyCall.sendMessage(largeMessage40k); - assertThat(firstBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(proxyCall.isReady()).isTrue(); + @Override + public void onError(Throwable t) {} - // Send second message (30000 bytes) - total 70000 > 65536, should buffer - proxyCall.sendMessage(largeMessage30k); + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); - // Wait a brief moment to confirm it is NOT delivered to ext_proc (latch shouldn't tick) - assertThat(receivedRequests).hasSize(3); // Headers + First Body + Client Window Update (Path 2 replenishment) - assertThat(proxyCall.isReady()).isFalse(); + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); - // Now send ServerWindowUpdate from ext_proc to interceptor to increment window by 40000 - responseObserverRef.get().onNext(ProcessingResponse.newBuilder() - .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder() - .setWindowIncrementDownstreamToSidestream(40000) - .build()) - .build()); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); - // The second body should now be flushed and received by ext_proc - assertThat(secondBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(receivedRequests.size()).isAtLeast(4); + final AtomicReference capturedHeaders = new AtomicReference<>(); + final CountDownLatch dataPlaneLatch = new CountDownLatch(1); + MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) + .fallbackHandlerRegistry(uniqueRegistry) + .directExecutor() + .build().start()); + uniqueRegistry.addService(ServerInterceptors.intercept( + ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + })) + .build(), + new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall( + ServerCall call, Metadata headers, ServerCallHandler next) { + capturedHeaders.set(headers); + dataPlaneLatch.countDown(); + return next.startCall(call, headers); + } + })); - assertThat(receivedRequests.get(0).hasRequestHeaders()).isTrue(); - assertThat(receivedRequests.get(1).hasRequestBody()).isTrue(); - assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); - assertThat(receivedRequests.get(3).hasRequestBody()).isTrue(); - assertThat(receivedRequests.get(3).getRequestBody().getBody().size()).isEqualTo(30000); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); - assertThat(proxyCall.isReady()).isTrue(); + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - // Verify both messages reached the backend service - assertThat(dataPlaneReceivedMessages).containsExactly(largeMessage40k, largeMessage30k).inOrder(); + assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); + Metadata headersApplied = capturedHeaders.get(); + // It should have base64 decoded "YmFy" to "bar" + byte[] binValue = + headersApplied.get(Metadata.Key.of("custom-bin", Metadata.BINARY_BYTE_MARSHALLER)); + assertThat(binValue).isEqualTo(new byte[]{'b', 'a', 'r'}); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -15665,80 +15767,55 @@ public void onCompleted() { @Test @SuppressWarnings("unchecked") - public void testUpstreamToSidestreamFlowControl_EnforcesWindow() throws Exception { + public void deserialization_binaryHeader_invalidBase64_noError_fails() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) + .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl( + "type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(4); // Headers, Request Body, Response Headers, Response Body 1 - final CountDownLatch secondResponseBodyLatch = new CountDownLatch(1); - final AtomicReference> responseObserverRef = new AtomicReference<>(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( final StreamObserver responseObserver) { - responseObserverRef.set(responseObserver); ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - receivedRequests.add(request); - extProcLatch.countDown(); if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - } else if (request.hasRequestBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getRequestBody().getBody()) - .build()) - .build()) - .build()) - .build()) - .build()); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - } else if (request.hasResponseBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseBody(BodyResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getResponseBody().getBody()) - .build()) + .setHeaderMutation(HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("custom-bin") + .setRawValue( + ByteString.copyFromUtf8("invalid_base64!")) + .build()) + .build()) .build()) .build()) .build()) .build()); - if (request.getResponseBody().getBody().size() == 30000) { - secondResponseBodyLatch.countDown(); - } } } @@ -15752,8 +15829,6 @@ public void onCompleted() { }; } }; - - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -15767,111 +15842,159 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( - new ServerCalls.BidiStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - dataPlaneResponseObserverRef.set(responseObserver); - return new StreamObserver() { - @Override - public void onNext(String value) {} - @Override - public void onError(Throwable t) {} - @Override - public void onCompleted() {} - }; - } - })) - .build()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); - final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); - final CountDownLatch messagesLatch2 = new CountDownLatch(2); - final CountDownLatch messagesLatch3 = new CountDownLatch(3); - ClientCall.Listener appListener = new ClientCall.Listener() { + final AtomicReference capturedStatus = new AtomicReference<>(); + final CountDownLatch callClosedLatch = new CountDownLatch(1); + proxyCall.start(new ClientCall.Listener() { @Override - public void onMessage(String message) { - appReceivedMessages.add(message); - messagesLatch2.countDown(); - messagesLatch3.countDown(); + public void onClose(Status status, Metadata trailers) { + capturedStatus.set(status); + callClosedLatch.countDown(); } - }; - - ClientCall proxyCall = - interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + }, new Metadata()); - proxyCall.start(appListener, new Metadata()); - proxyCall.request(10); + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); - proxyCall.sendMessage("Client Msg"); + channelManager.close(); + } - StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); - upstreamResponseObserver.onNext("Dummy for headers"); + @Test + @SuppressWarnings("unchecked") + public void deserialization_binaryHeader_invalidBase64_failsCall() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + uniqueExtProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl( + "type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) + .setMutationRules( + io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules + .newBuilder() + .setDisallowIsError(com.google.protobuf.BoolValue.of(true)) + .build()) + .build(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; - String largeMessage40k = new String(new char[40000]).replace('\0', 'a'); - String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setHeaderMutation(HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("custom-bin") + .setRawValue( + ByteString.copyFromUtf8("invalid_base64!")) + .build()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + @Override + public void onError(Throwable t) {} - upstreamResponseObserver.onNext(largeMessage40k); + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); - assertThat(messagesLatch2.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(appReceivedMessages).hasSize(2); + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); - upstreamResponseObserver.onNext(largeMessage30k); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); - Thread.sleep(200); - assertThat(secondResponseBodyLatch.getCount()).isEqualTo(1); - assertThat(appReceivedMessages).hasSize(2); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); - responseObserverRef.get().onNext(ProcessingResponse.newBuilder() - .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder() - .setWindowIncrementUpstreamToSidestream(40000) - .build()) - .build()); + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); - assertThat(secondResponseBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); + final AtomicReference capturedStatus = new AtomicReference<>(); + final CountDownLatch callClosedLatch = new CountDownLatch(1); + proxyCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + capturedStatus.set(status); + callClosedLatch.countDown(); + } + }, new Metadata()); - assertThat(messagesLatch3.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(appReceivedMessages).hasSize(3); - assertThat(appReceivedMessages.get(2)).isEqualTo(largeMessage30k); - assertThat(receivedRequests).isNotEmpty(); + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); - proxyCall.cancel("Cleanup", null); channelManager.close(); } @Test @SuppressWarnings("unchecked") - public void testSidestreamToUpstreamFlowControl_Violations() throws Exception { + public void deserialization_asciiHeader_invalidChars_noError_fails() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) + .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl( + "type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - - final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'c'); - final CountDownLatch callClosedLatch = new CountDownLatch(1); - final AtomicReference capturedStatus = new AtomicReference<>(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -15884,17 +16007,21 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - } else if (request.hasRequestBody()) { - // Respond with a mutated body that is larger than the initial window size of 65536 - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) - .build()) + .setHeaderMutation(HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("custom-ascii") + .setRawValue( + ByteString.copyFromUtf8( + "value_with_newline\n")) + .build()) + .build()) .build()) .build()) .build()) @@ -15912,8 +16039,6 @@ public void onCompleted() { }; } }; - - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -15927,20 +16052,16 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello " + request); - responseObserver.onCompleted(); - })) - .build()); - - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + final AtomicReference capturedStatus = new AtomicReference<>(); + final CountDownLatch callClosedLatch = new CountDownLatch(1); proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { @@ -15949,45 +16070,39 @@ public void onClose(Status status, Metadata trailers) { } }, new Metadata()); - proxyCall.sendMessage("Message 1"); - assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // The call should fail immediately with INTERNAL error code due to flow control violation assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); - assertThat(capturedStatus.get().getCause()).isInstanceOf(io.grpc.StatusRuntimeException.class); - assertThat(capturedStatus.get().getCause().getMessage()).contains("Flow control violation: received client body from ext_proc when window is closed"); + assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); channelManager.close(); } @Test @SuppressWarnings("unchecked") - public void testSidestreamToDownstreamFlowControl_Violations() throws Exception { + public void deserialization_asciiHeader_invalidCharacters_failsCall() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) + .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl( + "type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) + .setMutationRules( + io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules + .newBuilder() + .setDisallowIsError(com.google.protobuf.BoolValue.of(true)) + .build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; - - final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'd'); - final CountDownLatch callClosedLatch = new CountDownLatch(1); - final AtomicReference capturedStatus = new AtomicReference<>(); + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -16000,33 +16115,21 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - } else if (request.hasRequestBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getRequestBody().getBody()) - .build()) - .build()) - .build()) - .build()) - .build()); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); - } else if (request.hasResponseBody()) { - // Respond with a mutated body that is larger than the initial window size of 65536 - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseBody(BodyResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) - .build()) + .setHeaderMutation(HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("custom-ascii") + .setRawValue( + ByteString.copyFromUtf8( + "value_with_newline\n")) + .build()) + .build()) .build()) .build()) .build()) @@ -16044,8 +16147,6 @@ public void onCompleted() { }; } }; - - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -16059,31 +16160,16 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( - new ServerCalls.BidiStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - dataPlaneResponseObserverRef.set(responseObserver); - return new StreamObserver() { - @Override - public void onNext(String value) {} - @Override - public void onError(Throwable t) {} - @Override - public void onCompleted() {} - }; - } - })) - .build()); - - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + final AtomicReference capturedStatus = new AtomicReference<>(); + final CountDownLatch callClosedLatch = new CountDownLatch(1); proxyCall.start(new ClientCall.Listener() { @Override public void onClose(Status status, Metadata trailers) { @@ -16091,76 +16177,62 @@ public void onClose(Status status, Metadata trailers) { callClosedLatch.countDown(); } }, new Metadata()); - proxyCall.request(10); - - proxyCall.sendMessage("Client Msg"); - - // Send a response from upstream to trigger headers and then the body response - StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); - upstreamResponseObserver.onNext("Response Msg"); assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // The call should fail immediately with INTERNAL error code due to flow control violation assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); - assertThat(capturedStatus.get().getCause()).isInstanceOf(io.grpc.StatusRuntimeException.class); - assertThat(capturedStatus.get().getCause().getMessage()).contains("Flow control violation: received server body from ext_proc when window is closed"); + assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); channelManager.close(); } @Test @SuppressWarnings("unchecked") - public void testThresholdBasedWindowUpdates() throws Exception { + public void deserialization_headerValue_tooLong_noError_fails() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) + .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl( + "type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); - final List> observers = new ArrayList<>(); + String longValue = new String(new char[16385]).replace('\0', 'v'); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( final StreamObserver responseObserver) { - observers.add(responseObserver); ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - receivedRequests.add(request); if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - extProcLatch.countDown(); - } else if (request.hasRequestBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getRequestBody().getBody()) - .build()) + .setHeaderMutation(HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("custom-ascii") + .setRawValue(ByteString.copyFromUtf8(longValue)) + .build()) + .build()) .build()) .build()) .build()) @@ -16178,8 +16250,6 @@ public void onCompleted() { }; } }; - - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -16193,108 +16263,61 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( - new ServerCalls.BidiStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - return new StreamObserver() { - @Override - public void onNext(String value) {} - @Override - public void onError(Throwable t) {} - @Override - public void onCompleted() {} - }; - } - })) - .build()); - - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); - - proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(filterClientRequests(receivedRequests)).hasSize(1); - assertThat(filterClientRequests(receivedRequests).get(0).hasRequestHeaders()).isTrue(); - - // 1. Send body message. It should be sent immediately. - proxyCall.sendMessage("Msg 1"); // size = 5 bytes - - assertThat(filterClientRequests(receivedRequests)).hasSize(2); - assertThat(filterClientRequests(receivedRequests).get(1).hasRequestBody()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(1).getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 1"); - // No window updates were accumulated yet. - assertThat(filterClientRequests(receivedRequests).get(1).hasClientWindowUpdate()).isFalse(); - - // 2. Trigger window replenishment below threshold (e.g. 5 bytes from Msg 1 response). - // The interceptor processes the response, forwards it upstream, and increments - // accumulatedWindowUpdateSidestreamToUpstream. Since 5 < 32768, it won't send standalone updates. - // We send another message "Msg 2" to trigger piggybacking. - proxyCall.sendMessage("Msg 2"); - - assertThat(filterClientRequests(receivedRequests)).hasSize(3); - assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2).getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 2"); - // Verify accumulated 5 bytes update is piggybacked. - assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(5); + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); - // 3. Accumulate past threshold (e.g. 35,000 bytes) without sending body messages. - // This should trigger an immediate standalone ClientWindowUpdate. - StreamObserver responseObserver = observers.get(0); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFrom(new byte[35000])) - .build()) - .build()) - .build()) - .build()) - .build()); + final AtomicReference capturedStatus = new AtomicReference<>(); + final CountDownLatch callClosedLatch = new CountDownLatch(1); + proxyCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + capturedStatus.set(status); + callClosedLatch.countDown(); + } + }, new Metadata()); - // standalone client window update received. - assertThat(filterClientRequests(receivedRequests)).hasSize(4); - assertThat(filterClientRequests(receivedRequests).get(3).hasClientWindowUpdate()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(3).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(35005); - assertThat(filterClientRequests(receivedRequests).get(3).hasRequestBody()).isFalse(); + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); - proxyCall.cancel("Cleanup", null); channelManager.close(); } @Test @SuppressWarnings("unchecked") - public void testHalfClosePiggybacking() throws Exception { + public void deserialization_headerValue_tooLong_failsCall() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); + // Enable disallowIsError = true in mutation rules ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) + .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl( + "type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) + .setMutationRules( + io.envoyproxy.envoy.config.common.mutation_rules.v3.HeaderMutationRules + .newBuilder() + .setDisallowIsError(com.google.protobuf.BoolValue.of(false)) + .build()) .build(); - ConfigOrError configOrError = - provider.parseFilterConfig(Any.pack(proto), filterContext); - assertThat(configOrError.errorDetail).isNull(); - ExternalProcessorFilterConfig filterConfig = configOrError.config; + ExternalProcessorFilterConfig filterConfig = + provider.parseFilterConfig(Any.pack(proto), filterContext).config; - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + // Create a value that is 16385 characters long (exceeding 16384 limit) + String longValue = new String(new char[16385]).replace('\0', 'v'); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -16305,20 +16328,21 @@ public StreamObserver process( return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - receivedRequests.add(request); if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - extProcLatch.countDown(); - } else if (request.hasRequestBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getRequestBody().getBody()) - .build()) + .setHeaderMutation(HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("custom-ascii") + .setRawValue(ByteString.copyFromUtf8(longValue)) + .build()) + .build()) .build()) .build()) .build()) @@ -16336,8 +16360,6 @@ public void onCompleted() { }; } }; - - String uniqueExtProcServerName = InProcessServerBuilder.generateName(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -16351,51 +16373,29 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( - new ServerCalls.BidiStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - return new StreamObserver() { - @Override - public void onNext(String value) {} - @Override - public void onError(Throwable t) {} - @Override - public void onCompleted() {} - }; - } - })) - .build()); - - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueDataPlaneServerName).directExecutor().build()); + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); - - proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(filterClientRequests(receivedRequests)).hasSize(1); - - proxyCall.sendMessage("Last Message"); - - - - // Verify 12 bytes are accumulated but no standalone update is sent. - assertThat(filterClientRequests(receivedRequests)).hasSize(2); + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); - proxyCall.halfClose(); + final AtomicReference capturedStatus = new AtomicReference<>(); + final CountDownLatch callClosedLatch = new CountDownLatch(1); + proxyCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + capturedStatus.set(status); + callClosedLatch.countDown(); + } + }, new Metadata()); - // Verify halfClose sends EOF request piggybacking the accumulated 12 bytes update. - assertThat(filterClientRequests(receivedRequests)).hasSize(3); - assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2).getRequestBody().getEndOfStreamWithoutMessage()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(12); + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // The call should fail unconditionally due to IllegalArgumentException + assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); + assertThat(capturedStatus.get().getCause()).isInstanceOf(IllegalArgumentException.class); - proxyCall.cancel("Cleanup", null); channelManager.close(); } From 5855bc88c6babc31bcaa1ee01c261e54e6d583f5 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Mon, 20 Jul 2026 13:01:20 +0000 Subject: [PATCH 06/13] Add more tests. --- .../ExternalProcessorClientInterceptor.java | 4 +- ...xternalProcessorClientInterceptorTest.java | 855 ++++++++++++++++-- 2 files changed, 805 insertions(+), 54 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java index f03d07ed0e0..9f7da1d201e 100644 --- a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java +++ b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java @@ -970,7 +970,7 @@ public void sendMessage(InputStream message) { bodyMessageSentToExtProc.set(true); super.sendMessage(new KnownLengthInputStream(bodyByteString)); } else { - if (bodyByteString.size() > downstreamToSidestreamWindow || !pendingRequestBodyMessages.isEmpty()) { + if (downstreamToSidestreamWindow <= 0 || !pendingRequestBodyMessages.isEmpty()) { pendingRequestBodyMessages.add(bodyByteString); } else { sendRequestBodyToExtProc(bodyByteString); @@ -1376,7 +1376,7 @@ public void onMessage(InputStream message) { dataPlaneClientCall.getCallContext().run( () -> delegate().onMessage(bodyByteString.newInput())); } else { - if (bodyByteString.size() > dataPlaneClientCall.upstreamToSidestreamWindow || !savedMessages.isEmpty()) { + if (dataPlaneClientCall.upstreamToSidestreamWindow <= 0 || !savedMessages.isEmpty()) { savedMessages.add(new KnownLengthInputStream(bodyByteString)); } else { dataPlaneClientCall.upstreamToSidestreamWindow -= bodyByteString.size(); diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index 864850e91e1..fc0a8d4fa33 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -91,11 +91,13 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Executor; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -3183,10 +3185,10 @@ public void givenResponseHeaderModeSkip_responseHeadersSentDirectlyUpstream() Metadata.Key.of("custom-response-header", Metadata.ASCII_STRING_MARSHALLER); // External Processor Server - final java.util.concurrent.atomic.AtomicBoolean responseHeadersReceived = - new java.util.concurrent.atomic.AtomicBoolean(false); - final java.util.concurrent.CountDownLatch requestHeadersLatch = - new java.util.concurrent.CountDownLatch(1); + final AtomicBoolean responseHeadersReceived = + new AtomicBoolean(false); + final CountDownLatch requestHeadersLatch = + new CountDownLatch(1); final CountDownLatch extProcCompletedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -4884,7 +4886,7 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final List serverReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List serverReceivedMessages = new CopyOnWriteArrayList<>(); MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) .fallbackHandlerRegistry(uniqueRegistry) @@ -4915,8 +4917,8 @@ public void onCompleted() { })) .build()); - final java.util.concurrent.CountDownLatch dataPlaneHalfClosedLatch = - new java.util.concurrent.CountDownLatch(1); + final CountDownLatch dataPlaneHalfClosedLatch = + new CountDownLatch(1); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .intercept(new ClientInterceptor() { @@ -4944,7 +4946,7 @@ public void halfClose() { proxyCall.request(1); proxyCall.halfClose(); - assertThat(dataPlaneHalfClosedLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); + assertThat(dataPlaneHalfClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(serverReceivedMessages).containsExactly("mutated1", "mutated2"); proxyCall.cancel("Cleanup", null); @@ -5040,7 +5042,7 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final List serverReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List serverReceivedMessages = new CopyOnWriteArrayList<>(); MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) .fallbackHandlerRegistry(uniqueRegistry) @@ -5071,8 +5073,8 @@ public void onCompleted() { })) .build()); - final java.util.concurrent.CountDownLatch dataPlaneHalfClosedLatch = - new java.util.concurrent.CountDownLatch(1); + final CountDownLatch dataPlaneHalfClosedLatch = + new CountDownLatch(1); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .intercept(new ClientInterceptor() { @@ -5100,7 +5102,7 @@ public void halfClose() { proxyCall.request(1); proxyCall.halfClose(); - assertThat(dataPlaneHalfClosedLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); + assertThat(dataPlaneHalfClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(serverReceivedMessages).containsExactly("mutated1", "mutated2"); proxyCall.cancel("Cleanup", null); @@ -5134,7 +5136,7 @@ public void halfClose() { ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new java.util.concurrent.CopyOnWriteArrayList<>(); + new CopyOnWriteArrayList<>(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -5208,7 +5210,7 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final List serverReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List serverReceivedMessages = new CopyOnWriteArrayList<>(); MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) .fallbackHandlerRegistry(uniqueRegistry) @@ -5239,8 +5241,8 @@ public void onCompleted() { })) .build()); - final java.util.concurrent.CountDownLatch dataPlaneHalfClosedLatch = - new java.util.concurrent.CountDownLatch(1); + final CountDownLatch dataPlaneHalfClosedLatch = + new CountDownLatch(1); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .intercept(new ClientInterceptor() { @@ -5268,7 +5270,7 @@ public void halfClose() { proxyCall.request(1); proxyCall.sendMessage("req1"); - assertThat(dataPlaneHalfClosedLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); + assertThat(dataPlaneHalfClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(serverReceivedMessages).containsExactly("mutated1", "mutated2"); // Client app continues to send messages after super half close propagated. @@ -5318,7 +5320,7 @@ public void halfClose() { ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new java.util.concurrent.CopyOnWriteArrayList<>(); + new CopyOnWriteArrayList<>(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -5382,7 +5384,7 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final List serverReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List serverReceivedMessages = new CopyOnWriteArrayList<>(); MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) .fallbackHandlerRegistry(uniqueRegistry) @@ -5413,8 +5415,8 @@ public void onCompleted() { })) .build()); - final java.util.concurrent.CountDownLatch dataPlaneHalfClosedLatch = - new java.util.concurrent.CountDownLatch(1); + final CountDownLatch dataPlaneHalfClosedLatch = + new CountDownLatch(1); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .intercept(new ClientInterceptor() { @@ -5442,7 +5444,7 @@ public void halfClose() { proxyCall.request(1); proxyCall.sendMessage("req1"); - assertThat(dataPlaneHalfClosedLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); + assertThat(dataPlaneHalfClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(serverReceivedMessages).containsExactly("mutated1", "mutated2"); // Client app continues to send messages after super half close propagated. @@ -5492,7 +5494,7 @@ public void givenObservabilityTrue_whenExtProcBusy_thenIsReadyReturnsFalse() ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new java.util.concurrent.CopyOnWriteArrayList<>(); + new CopyOnWriteArrayList<>(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -5614,7 +5616,7 @@ public void givenObservabilityMode_whenUpstreamBusy_thenIsReadyReturnsFalse() ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new java.util.concurrent.CopyOnWriteArrayList<>(); + new CopyOnWriteArrayList<>(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -6174,7 +6176,7 @@ public void onCompleted() { })) .build()); - final List dataPlaneSentMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List dataPlaneSentMessages = new CopyOnWriteArrayList<>(); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName) .directExecutor() @@ -6301,7 +6303,7 @@ public void onCompleted() {} })) .build()); - final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List appReceivedMessages = new CopyOnWriteArrayList<>(); final CountDownLatch appMessageLatch = new CountDownLatch(1); ClientCall.Listener appListener = new ClientCall.Listener() { @Override @@ -7168,7 +7170,7 @@ public void onCompleted() { filterConfig, channelManager, scheduler, FAKE_CONTEXT); final List dataPlaneReceivedMessages = - new java.util.concurrent.CopyOnWriteArrayList<>(); + new CopyOnWriteArrayList<>(); final CountDownLatch dataPlaneLatch = new CountDownLatch(1); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( @@ -7348,7 +7350,7 @@ public void onCompleted() { filterConfig, channelManager, scheduler, FAKE_CONTEXT); final List dataPlaneReceivedMessages = - new java.util.concurrent.CopyOnWriteArrayList<>(); + new CopyOnWriteArrayList<>(); final CountDownLatch dataPlaneLatch = new CountDownLatch(1); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( @@ -7554,7 +7556,7 @@ public void onCompleted() { ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List appReceivedMessages = new CopyOnWriteArrayList<>(); final AtomicReference appReceivedHeaders = new AtomicReference<>(); final AtomicReference appReceivedStatus = new AtomicReference<>(); final AtomicReference appReceivedTrailers = new AtomicReference<>(); @@ -7781,7 +7783,7 @@ public void onCompleted() {} ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List appReceivedMessages = new CopyOnWriteArrayList<>(); final AtomicReference appReceivedHeaders = new AtomicReference<>(); final AtomicReference appReceivedStatus = new AtomicReference<>(); final AtomicReference appReceivedTrailers = new AtomicReference<>(); @@ -9945,7 +9947,7 @@ public void onNext(ProcessingRequest request) { // 2. Schedule the immediate response to be sent after 2 seconds @SuppressWarnings("unused") - java.util.concurrent.ScheduledFuture unused = + ScheduledFuture unused = fakeClock.getScheduledExecutorService().schedule(() -> { responseObserver.onNext(ProcessingResponse.newBuilder() .setImmediateResponse( @@ -10309,7 +10311,7 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenIsReadyReturnsFalse( ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new java.util.concurrent.CopyOnWriteArrayList<>(); + new CopyOnWriteArrayList<>(); // Sidecar server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -10846,7 +10848,7 @@ public void testFlowControlStateInitialization() throws Exception { assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch extProcLatch = new CountDownLatch(2); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -10963,7 +10965,7 @@ public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Except assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch firstBodyLatch = new CountDownLatch(2); // Headers + First Body final CountDownLatch secondBodyLatch = new CountDownLatch(1); final AtomicReference> responseObserverRef = new AtomicReference<>(); @@ -10986,13 +10988,17 @@ public void onNext(ProcessingRequest request) { firstBodyLatch.countDown(); } else if (request.hasRequestBody()) { if (firstBodyLatch.getCount() > 0) { - // This is the first body (40000 bytes) + // This is the first body (70000 bytes). Truncate to 40000 to fit return window (65536) and trigger update. + ByteString bodyToSend = request.getRequestBody().getBody(); + if (bodyToSend.size() == 70000) { + bodyToSend = bodyToSend.substring(0, 40000); + } responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() .setBodyMutation(BodyMutation.newBuilder() .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getRequestBody().getBody()) + .setBody(bodyToSend) .build()) .build()) .build()) @@ -11042,7 +11048,7 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final List dataPlaneReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List dataPlaneReceivedMessages = new CopyOnWriteArrayList<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( new ServerCalls.ClientStreamingMethod() { @@ -11076,18 +11082,18 @@ public void onCompleted() { proxyCall.start(new ClientCall.Listener() {}, new Metadata()); // Generate large messages - String largeMessage40k = new String(new char[40000]).replace('\0', 'a'); + String largeMessage70k = new String(new char[70000]).replace('\0', 'a'); String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); - // Send first message (40000 bytes) - fits in 65536 window - proxyCall.sendMessage(largeMessage40k); + // Send first message (70000 bytes) - fits in 65536 window + proxyCall.sendMessage(largeMessage70k); assertThat(firstBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(proxyCall.isReady()).isTrue(); + assertThat(proxyCall.isReady()).isFalse(); - // Send second message (30000 bytes) - total 70000 > 65536, should buffer + // Send second message (30000 bytes) - total 100000 > 65536, should buffer proxyCall.sendMessage(largeMessage30k); - // Wait a brief moment to confirm it is NOT delivered to ext_proc (latch shouldn't tick) + // Assert that it is NOT delivered to ext_proc (delivery is synchronous on directExecutor, so we can check immediately) assertThat(receivedRequests).hasSize(3); // Headers + First Body + Client Window Update (Path 2 replenishment) assertThat(proxyCall.isReady()).isFalse(); @@ -11111,7 +11117,7 @@ public void onCompleted() { assertThat(proxyCall.isReady()).isTrue(); // Verify both messages reached the backend service - assertThat(dataPlaneReceivedMessages).containsExactly(largeMessage40k, largeMessage30k).inOrder(); + assertThat(dataPlaneReceivedMessages).containsExactly(largeMessage70k.substring(0, 40000), largeMessage30k).inOrder(); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -11141,7 +11147,7 @@ public void testUpstreamToSidestreamFlowControl_EnforcesWindow() throws Exceptio assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch extProcLatch = new CountDownLatch(4); // Headers, Request Body, Response Headers, Response Body 1 final CountDownLatch secondResponseBodyLatch = new CountDownLatch(1); final AtomicReference> responseObserverRef = new AtomicReference<>(); @@ -11179,18 +11185,23 @@ public void onNext(ProcessingRequest request) { .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasResponseBody()) { + com.google.protobuf.ByteString originalBody = request.getResponseBody().getBody(); + com.google.protobuf.ByteString bodyToSend = originalBody; + if (originalBody.size() == 70000) { + bodyToSend = bodyToSend.substring(0, 40000); + } responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() .setBodyMutation(BodyMutation.newBuilder() .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getResponseBody().getBody()) + .setBody(bodyToSend) .build()) .build()) .build()) .build()) .build()); - if (request.getResponseBody().getBody().size() == 30000) { + if (originalBody.size() == 30000) { secondResponseBodyLatch.countDown(); } } @@ -11243,7 +11254,7 @@ public void onCompleted() {} ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List appReceivedMessages = new CopyOnWriteArrayList<>(); final CountDownLatch messagesLatch2 = new CountDownLatch(2); final CountDownLatch messagesLatch3 = new CountDownLatch(3); ClientCall.Listener appListener = new ClientCall.Listener() { @@ -11266,12 +11277,12 @@ public void onMessage(String message) { StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); upstreamResponseObserver.onNext("Dummy for headers"); - String largeMessage40k = new String(new char[40000]).replace('\0', 'a'); + String largeMessage70k = new String(new char[70000]).replace('\0', 'a'); String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); - upstreamResponseObserver.onNext(largeMessage40k); + upstreamResponseObserver.onNext(largeMessage70k); assertThat(messagesLatch2.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appReceivedMessages).hasSize(2); @@ -11587,7 +11598,7 @@ public void testThresholdBasedWindowUpdates() throws Exception { assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch extProcLatch = new CountDownLatch(1); final List> observers = new ArrayList<>(); @@ -11747,7 +11758,7 @@ public void testHalfClosePiggybacking() throws Exception { assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final List receivedRequests = new java.util.concurrent.CopyOnWriteArrayList<>(); + final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch extProcLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -11853,6 +11864,746 @@ public void onCompleted() {} channelManager.close(); } + @Test + @SuppressWarnings("unchecked") + public void testPiggybackingOnRequestBody() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasRequestBody()) { + // Mutate Msg 1 to be 15 bytes + if (request.getRequestBody().getBody().toStringUtf8().equals("Msg 1")) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFrom(new byte[15])) + .build()) + .build()) + .build()) + .build()) + .build()); + } else { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseBody()) { + // Forward response body mutation (20 bytes) + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getResponseBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.request(10); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(filterClientRequests(receivedRequests)).hasSize(1); // Headers request + + // Send Msg 1 (5 bytes). It is processed by sidecar and mutated to 15 bytes. + proxyCall.sendMessage("Msg 1"); + + // Wait until Msg 1 request and response are processed + long startTime = System.currentTimeMillis(); + while (filterClientRequests(receivedRequests).size() < 2 && System.currentTimeMillis() - startTime < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + assertThat(filterClientRequests(receivedRequests)).hasSize(2); + assertThat(filterClientRequests(receivedRequests).get(1).hasRequestBody()).isTrue(); + + // Trigger response headers and body (20 bytes) from upstream + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Dummy for headers"); + + // Wait until response headers are processed by sidecar + startTime = System.currentTimeMillis(); + while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + + String msg20Bytes = new String(new char[20]).replace('\0', 's'); + upstreamResponseObserver.onNext(msg20Bytes); + + // Wait until response body is processed by sidecar + startTime = System.currentTimeMillis(); + while (receivedRequests.size() < 4 && System.currentTimeMillis() - startTime < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + + // Now send Msg 2 from app (5 bytes). This should piggyback the accumulated updates. + proxyCall.sendMessage("Msg 2"); + + startTime = System.currentTimeMillis(); + while (receivedRequests.size() < 6 && System.currentTimeMillis() - startTime < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + assertThat(receivedRequests).hasSize(6); + + // receivedRequests.get(3) is RespBody(Dummy for headers). It should piggyback the accumulated 15 bytes update. + ProcessingRequest dummyRespBodyReq = receivedRequests.get(3); + assertThat(dummyRespBodyReq.hasResponseBody()).isTrue(); + assertThat(dummyRespBodyReq.hasClientWindowUpdate()).isTrue(); + assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(15); + assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(0); + + // receivedRequests.get(5) is ReqBody(Msg 2). It should piggyback the accumulated 20 bytes update. + ProcessingRequest msg2Req = receivedRequests.get(5); + assertThat(msg2Req.hasRequestBody()).isTrue(); + assertThat(msg2Req.getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 2"); + assertThat(msg2Req.hasClientWindowUpdate()).isTrue(); + assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(0); + assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(20); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testPiggybackingOnResponseBody() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasRequestBody()) { + // Mutate Msg 1 to be 15 bytes + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFrom(new byte[15])) + .build()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseBody()) { + // Forward response body mutation + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getResponseBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.request(10); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(filterClientRequests(receivedRequests)).hasSize(1); // Headers request + + // Send Msg 1 (5 bytes) from app. It is processed by sidecar and mutated to 15 bytes. + proxyCall.sendMessage("Msg 1"); + + // Wait until Msg 1 request and response are processed + long startTime = System.currentTimeMillis(); + while (filterClientRequests(receivedRequests).size() < 2 && System.currentTimeMillis() - startTime < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + assertThat(filterClientRequests(receivedRequests)).hasSize(2); + + // Trigger response headers and first response body (20 bytes) from upstream + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Dummy for headers"); + + // Wait until response headers are processed by sidecar + startTime = System.currentTimeMillis(); + while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + + String msg20Bytes = new String(new char[20]).replace('\0', 's'); + upstreamResponseObserver.onNext(msg20Bytes); + + // Wait until response body is processed by sidecar + startTime = System.currentTimeMillis(); + while (receivedRequests.size() < 4 && System.currentTimeMillis() - startTime < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + + // Now send a second response body (5 bytes) from upstream. + upstreamResponseObserver.onNext("Msg 2"); + + startTime = System.currentTimeMillis(); + while (receivedRequests.size() < 6 && System.currentTimeMillis() - startTime < 5000) { + fakeClock.forwardTime(1, TimeUnit.SECONDS); + Thread.sleep(10); + } + assertThat(receivedRequests).hasSize(6); + + // receivedRequests.get(3) is RespBody(Dummy for headers). It should piggyback the accumulated 15 bytes update. + ProcessingRequest dummyRespBodyReq = receivedRequests.get(3); + assertThat(dummyRespBodyReq.hasResponseBody()).isTrue(); + assertThat(dummyRespBodyReq.hasClientWindowUpdate()).isTrue(); + assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(15); + assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(0); + + // receivedRequests.get(4) is RespBody(ssssssssssssssssssss). It should piggyback the accumulated 17 bytes update (from "Dummy for headers" which is 17 bytes). + ProcessingRequest ssssRespBodyReq = receivedRequests.get(4); + assertThat(ssssRespBodyReq.hasResponseBody()).isTrue(); + assertThat(ssssRespBodyReq.hasClientWindowUpdate()).isTrue(); + assertThat(ssssRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(0); + assertThat(ssssRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(17); + + // receivedRequests.get(5) is RespBody(Msg 2). It should piggyback the accumulated 20 bytes update (from ssssssssssssssssssss). + ProcessingRequest msg2Req = receivedRequests.get(5); + assertThat(msg2Req.hasResponseBody()).isTrue(); + assertThat(msg2Req.getResponseBody().getBody().toStringUtf8()).isEqualTo("Msg 2"); + assertThat(msg2Req.hasClientWindowUpdate()).isTrue(); + assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(0); + assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(20); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testClientWindowUpdateDeferredUntilRequestBodySendMessageCompletes() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(2); // Headers + Request Body + final AtomicReference> responseObserverRef = new AtomicReference<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + extProcLatch.countDown(); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + // Mutate request body and send back 40000 bytes. This triggers client window update replenishment. + extProcLatch.countDown(); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFrom(new byte[40000])) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } + })) + .build()); + + final CountDownLatch blockSendMessageLatch = new CountDownLatch(1); + final CountDownLatch sendMessageEnteredLatch = new CountDownLatch(1); + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void sendMessage(ReqT message) { + sendMessageEnteredLatch.countDown(); + try { + blockSendMessageLatch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + super.sendMessage(message); + } + }; + } + }) + .directExecutor() + .build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + // Send 40k message to ext_proc. + String body40k = new String(new char[40000]).replace('\0', 'a'); + + // Call sendMessage in a background thread to avoid blocking the main test execution thread + new Thread(() -> proxyCall.sendMessage(body40k)).start(); + + // Wait until interceptor's super.sendMessage() enters the custom interceptor and blocks + assertThat(sendMessageEnteredLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // Wait a brief moment to make sure no standalone ClientWindowUpdate is sent while blocked + Thread.sleep(200); + assertThat(receivedRequests).hasSize(2); // Only Headers and Request Body requests sent so far + + // Unblock the sendMessage call + blockSendMessageLatch.countDown(); + + // Wait for the interceptor to complete super.sendMessage() and send the window update + long startTime = System.currentTimeMillis(); + while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { + Thread.sleep(10); + } + assertThat(receivedRequests).hasSize(3); + assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); + assertThat(receivedRequests.get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(40000); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testClientWindowUpdateDeferredUntilResponseBodyOnMessageCompletes() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(3); // Request Headers, Response Headers, Response Body 1 + final AtomicReference> responseObserverRef = new AtomicReference<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + extProcLatch.countDown(); + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasResponseBody()) { + // Forward response body as-is. + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getResponseBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + final CountDownLatch blockOnMessageLatch = new CountDownLatch(1); + final CountDownLatch onMessageEnteredLatch = new CountDownLatch(1); + ClientCall.Listener appListener = new ClientCall.Listener() { + @Override + public void onMessage(String message) { + if (message.length() == 40000) { + onMessageEnteredLatch.countDown(); + try { + blockOnMessageLatch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + } + } + }; + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(appListener, new Metadata()); + proxyCall.request(10); + + // Call client to activate + proxyCall.sendMessage("Client Msg"); + + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Dummy for headers"); + + // Trigger response body from upstream in a background thread to avoid blocking the main test execution thread + String response40k = new String(new char[40000]).replace('\0', 'a'); + new Thread(() -> upstreamResponseObserver.onNext(response40k)).start(); + + // Wait until client app's onMessage enters and blocks + assertThat(onMessageEnteredLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // Wait a brief moment to make sure no standalone ClientWindowUpdate is sent while blocked + Thread.sleep(200); + assertThat(receivedRequests).hasSize(4); // Headers + Response Headers + Dummy Body + Response 40k Body + + // Unblock the onMessage call + blockOnMessageLatch.countDown(); + + // Wait for the interceptor to complete onMessage processing and send the window update + long startTime = System.currentTimeMillis(); + while (receivedRequests.size() < 5 && System.currentTimeMillis() - startTime < 5000) { + Thread.sleep(10); + } + assertThat(receivedRequests).hasSize(5); + assertThat(receivedRequests.get(4).hasClientWindowUpdate()).isTrue(); + assertThat(receivedRequests.get(4).getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(40000); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + // --- Category 21: Streaming Completeness (Client & Bi-Di) --- @Test From dc0370cc6ec45926680a0a6446a1bf28823252bd Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 21 Jul 2026 08:12:28 +0000 Subject: [PATCH 07/13] Evaluate flow control violations based on the window size before applying the current message size, rather than after. --- .../ExternalProcessorClientInterceptor.java | 10 +- ...xternalProcessorClientInterceptorTest.java | 222 ++++++++++++++++-- 2 files changed, 211 insertions(+), 21 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java index 9f7da1d201e..cd567c64bb4 100644 --- a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java +++ b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java @@ -765,14 +765,14 @@ private void trySendAccumulatedWindowUpdates() { if (extProcStreamState.get().isCompleted()) { return; } - long incrementUpstream = (super.isReady() || sidestreamToUpstreamWindow < 0) + long incrementUpstream = (super.isReady() || sidestreamToUpstreamWindow <= 0) ? accumulatedWindowUpdateSidestreamToUpstream : 0; long incrementDownstream = accumulatedWindowUpdateSidestreamToDownstream; boolean shouldSend = (incrementUpstream >= WINDOW_UPDATE_THRESHOLD) || (incrementDownstream >= WINDOW_UPDATE_THRESHOLD) - || (sidestreamToUpstreamWindow < 0 && accumulatedWindowUpdateSidestreamToUpstream > 0) - || (sidestreamToDownstreamWindow < 0 && accumulatedWindowUpdateSidestreamToDownstream > 0); + || (sidestreamToUpstreamWindow <= 0 && accumulatedWindowUpdateSidestreamToUpstream > 0) + || (sidestreamToDownstreamWindow <= 0 && accumulatedWindowUpdateSidestreamToDownstream > 0); if (shouldSend) { accumulatedWindowUpdateSidestreamToUpstream -= incrementUpstream; @@ -1086,7 +1086,7 @@ private void handleRequestBodyResponse(BodyResponse bodyResponse) { com.google.protobuf.ByteString body = streamed.getBody(); if (!config.getObservabilityMode() && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { synchronized (streamLock) { - if (body.size() > sidestreamToUpstreamWindow) { + if (sidestreamToUpstreamWindow <= 0) { internalOnError(Status.INTERNAL .withDescription("Flow control violation: received client body from ext_proc when window is closed") .asRuntimeException()); @@ -1120,7 +1120,7 @@ private void handleResponseBodyResponse( final int bodySize = body.size(); if (!config.getObservabilityMode() && currentProcessingMode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC) { synchronized (streamLock) { - if (bodySize > sidestreamToDownstreamWindow) { + if (sidestreamToDownstreamWindow <= 0) { internalOnError(Status.INTERNAL .withDescription("Flow control violation: received server body from ext_proc when window is closed") .asRuntimeException()); diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index fc0a8d4fa33..6eb7b265664 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -10988,17 +10988,12 @@ public void onNext(ProcessingRequest request) { firstBodyLatch.countDown(); } else if (request.hasRequestBody()) { if (firstBodyLatch.getCount() > 0) { - // This is the first body (70000 bytes). Truncate to 40000 to fit return window (65536) and trigger update. - ByteString bodyToSend = request.getRequestBody().getBody(); - if (bodyToSend.size() == 70000) { - bodyToSend = bodyToSend.substring(0, 40000); - } responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() .setBodyMutation(BodyMutation.newBuilder() .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(bodyToSend) + .setBody(request.getRequestBody().getBody()) .build()) .build()) .build()) @@ -11117,7 +11112,7 @@ public void onCompleted() { assertThat(proxyCall.isReady()).isTrue(); // Verify both messages reached the backend service - assertThat(dataPlaneReceivedMessages).containsExactly(largeMessage70k.substring(0, 40000), largeMessage30k).inOrder(); + assertThat(dataPlaneReceivedMessages).containsExactly(largeMessage70k, largeMessage30k).inOrder(); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -11187,9 +11182,7 @@ public void onNext(ProcessingRequest request) { } else if (request.hasResponseBody()) { com.google.protobuf.ByteString originalBody = request.getResponseBody().getBody(); com.google.protobuf.ByteString bodyToSend = originalBody; - if (originalBody.size() == 70000) { - bodyToSend = bodyToSend.substring(0, 40000); - } + // Return the original 70,000 bytes as-is responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() @@ -11334,7 +11327,7 @@ public void testSidestreamToDownstreamFlowControl_Violations() throws Exception assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'd'); + final String mutatedMessage = new String(new char[10000]).replace('\0', 'd'); final CountDownLatch callClosedLatch = new CountDownLatch(1); final AtomicReference capturedStatus = new AtomicReference<>(); @@ -11368,13 +11361,12 @@ public void onNext(ProcessingRequest request) { .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasResponseBody()) { - // Respond with a mutated body that is larger than the initial window size of 65536 responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() .setBodyMutation(BodyMutation.newBuilder() .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) + .setBody(ByteString.copyFromUtf8(mutatedMessage)) .build()) .build()) .build()) @@ -11444,11 +11436,18 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("Client Msg"); + // Retrieve the DataPlaneClientCall and set the sidestreamToDownstreamWindow to 0 via reflection + Object dataPlaneCall = unwrapToDataPlaneClientCall(proxyCall); + java.lang.reflect.Field windowField = dataPlaneCall.getClass().getDeclaredField("sidestreamToDownstreamWindow"); + windowField.setAccessible(true); + windowField.setLong(dataPlaneCall, 0); + // Send a response from upstream to trigger headers and then the body response StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); upstreamResponseObserver.onNext("Response Msg"); assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // The call should fail immediately with INTERNAL error code due to flow control violation assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); @@ -11482,7 +11481,7 @@ public void testSidestreamToUpstreamFlowControl_Violations() throws Exception { assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'c'); + final String mutatedMessage = new String(new char[10000]).replace('\0', 'c'); final CountDownLatch callClosedLatch = new CountDownLatch(1); final AtomicReference capturedStatus = new AtomicReference<>(); @@ -11500,13 +11499,12 @@ public void onNext(ProcessingRequest request) { .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasRequestBody()) { - // Respond with a mutated body that is larger than the initial window size of 65536 responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() .setBodyMutation(BodyMutation.newBuilder() .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) + .setBody(ByteString.copyFromUtf8(mutatedMessage)) .build()) .build()) .build()) @@ -11562,9 +11560,16 @@ public void onClose(Status status, Metadata trailers) { } }, new Metadata()); + // Retrieve the DataPlaneClientCall and set the sidestreamToUpstreamWindow to 0 via reflection + Object dataPlaneCall = unwrapToDataPlaneClientCall(proxyCall); + java.lang.reflect.Field windowField = dataPlaneCall.getClass().getDeclaredField("sidestreamToUpstreamWindow"); + windowField.setAccessible(true); + windowField.setLong(dataPlaneCall, 0); + proxyCall.sendMessage("Message 1"); assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // The call should fail immediately with INTERNAL error code due to flow control violation assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); @@ -12604,6 +12609,155 @@ public void onMessage(String message) { channelManager.close(); } + @Test + @SuppressWarnings("unchecked") + public void testClientWindowUpdateSentImmediatelyWhenSidestreamToUpstreamWindowExhaustedEvenIfClientCallNotReady() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(2); // Headers + Request Body + final AtomicReference> responseObserverRef = new AtomicReference<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + extProcLatch.countDown(); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + extProcLatch.countDown(); + // Mutate request body and send back 65536 bytes. This completely exhausts the return window (starts at 65536). + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFrom(new byte[65536])) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public boolean isReady() { + return false; // Force client call to be not ready + } + }; + } + }) + .directExecutor() + .build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + // Send 10k message to ext_proc. + String body10k = new String(new char[10000]).replace('\0', 'a'); + proxyCall.sendMessage(body10k); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // Since the window is exhausted (65536 bytes returned from sidecar), the ClientWindowUpdate + // should be sent immediately even though the client call is not ready (isReady() == false). + long startTime = System.currentTimeMillis(); + while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { + Thread.sleep(10); + } + assertThat(receivedRequests).hasSize(3); + assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); + assertThat(receivedRequests.get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(65536); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + // --- Category 21: Streaming Completeness (Client & Bi-Di) --- @Test @@ -17182,4 +17336,40 @@ private void shutdownAndAwaitTermination(ExecutorService executor) { Thread.currentThread().interrupt(); } } + + private static Object unwrapToDataPlaneClientCall(ClientCall call) throws Exception { + ClientCall current = call; + while (current != null) { + if (current.getClass().getName().equals("io.grpc.xds.ExternalProcessorClientInterceptor$DataPlaneClientCall")) { + return current; + } + java.lang.reflect.Field delegateField = null; + Class c = current.getClass(); + while (c != null) { + try { + delegateField = c.getDeclaredField("delegate"); + break; + } catch (NoSuchFieldException e) { + try { + delegateField = c.getDeclaredField("val$rawCall"); + break; + } catch (NoSuchFieldException e2) { + c = c.getSuperclass(); + } + } + } + if (delegateField == null) { + try { + java.lang.reflect.Method getDelegateMethod = current.getClass().getMethod("delegate"); + current = (ClientCall) getDelegateMethod.invoke(current); + continue; + } catch (Throwable ignored) {} + throw new IllegalArgumentException("Could not find delegate or val$rawCall field in " + current.getClass().getName()); + } + delegateField.setAccessible(true); + current = (ClientCall) delegateField.get(current); + } + throw new IllegalArgumentException("Could not unwrap to DataPlaneClientCall: " + call); + } } + From ceef8df697fe57bc94990207a93ab235c7768d55 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Wed, 22 Jul 2026 08:21:27 +0000 Subject: [PATCH 08/13] xds: Fix ext_proc client flow control to gate window updates by transport readiness - Gated the transmission of `ClientWindowUpdate` messages by transport readiness. If the upstream client call is not ready (`isReady() == false`), the upstream window increment is kept at 0 to enforce backpressure. - Modified `trySendAccumulatedWindowUpdates()` to avoid transmitting redundant, zero-increment window updates when transport readiness prevents replenishment. A `ClientWindowUpdate` is now sent only if at least one window increment is positive (`incrementUpstream > 0 || incrementDownstream > 0`). - Updated `testClientWindowUpdateSentImmediatelyOnWindowExhaustion` to verify that no redundant window updates are sent while the transport is not ready, and that the accumulated `65536` increment is correctly flushed immediately upon calling `onReady()`. - Fixed checkstyle violations (missing line separator) in the test code. --- .../ExternalProcessorClientInterceptor.java | 106 +- ...xternalProcessorClientInterceptorTest.java | 1088 +++++++++++++---- 2 files changed, 925 insertions(+), 269 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java index cd567c64bb4..f6f7da52001 100644 --- a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java +++ b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java @@ -311,7 +311,13 @@ private static class DataPlaneClientCall private long sidestreamToDownstreamWindow = DEFAULT_INITIAL_WINDOW_SIZE; // Buffered request body messages from downstream - private final Queue pendingRequestBodyMessages = new java.util.concurrent.ConcurrentLinkedQueue<>(); + private final Queue pendingRequestBodyMessages = + new java.util.concurrent.ConcurrentLinkedQueue<>(); + + // Buffered mutated response bodies from sidecar + private int downstreamRequestsPending = 0; + private final Queue pendingMutatedResponseBodies = + new java.util.concurrent.ConcurrentLinkedQueue<>(); private static final long WINDOW_UPDATE_THRESHOLD = DEFAULT_INITIAL_WINDOW_SIZE / 2; @@ -737,7 +743,7 @@ private void sendToExtProcDirect(ProcessingRequest request) { void mergeAccumulatedWindowUpdates(ProcessingRequest.Builder requestBuilder) { synchronized (streamLock) { - long incrementUpstream = (super.isReady() || sidestreamToUpstreamWindow < 0) + long incrementUpstream = super.isReady() ? accumulatedWindowUpdateSidestreamToUpstream : 0; long incrementDownstream = accumulatedWindowUpdateSidestreamToDownstream; @@ -757,22 +763,20 @@ void mergeAccumulatedWindowUpdates(ProcessingRequest.Builder requestBuilder) { private void trySendAccumulatedWindowUpdates() { synchronized (streamLock) { - System.out.println("trySendAccumulatedWindowUpdates: completed=" + extProcStreamState.get().isCompleted() + - ", super.isReady()=" + super.isReady() + - ", sidestreamToUpstreamWindow=" + sidestreamToUpstreamWindow + - ", accumulatedUpstream=" + accumulatedWindowUpdateSidestreamToUpstream + - ", accumulatedDownstream=" + accumulatedWindowUpdateSidestreamToDownstream); if (extProcStreamState.get().isCompleted()) { return; } - long incrementUpstream = (super.isReady() || sidestreamToUpstreamWindow <= 0) + long incrementUpstream = super.isReady() ? accumulatedWindowUpdateSidestreamToUpstream : 0; long incrementDownstream = accumulatedWindowUpdateSidestreamToDownstream; - boolean shouldSend = (incrementUpstream >= WINDOW_UPDATE_THRESHOLD) + boolean shouldSend = (incrementUpstream > 0 || incrementDownstream > 0) && ( + (incrementUpstream >= WINDOW_UPDATE_THRESHOLD) || (incrementDownstream >= WINDOW_UPDATE_THRESHOLD) || (sidestreamToUpstreamWindow <= 0 && accumulatedWindowUpdateSidestreamToUpstream > 0) - || (sidestreamToDownstreamWindow <= 0 && accumulatedWindowUpdateSidestreamToDownstream > 0); + || (sidestreamToDownstreamWindow <= 0 + && accumulatedWindowUpdateSidestreamToDownstream > 0) + ); if (shouldSend) { accumulatedWindowUpdateSidestreamToUpstream -= incrementUpstream; @@ -889,7 +893,8 @@ public boolean isReady() { if (config.getObservabilityMode()) { return super.isReady() && sidecarReady; } - return downstreamToSidestreamWindow > 0 && sidecarReady && pendingRequestBodyMessages.isEmpty(); + return downstreamToSidestreamWindow > 0 && sidecarReady + && pendingRequestBodyMessages.isEmpty(); } } @@ -904,10 +909,6 @@ public void request(int numMessages) { super.request(numMessages); return; } - if (!isSidecarReady()) { - pendingRequests.addAndGet(numMessages); - return; - } if (config.getObservabilityMode() || currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC) { super.request(numMessages); @@ -915,7 +916,11 @@ public void request(int numMessages) { } synchronized (streamLock) { pendingRequests.addAndGet(numMessages); - drainPendingRequests(); + downstreamRequestsPending += numMessages; + if (isSidecarReady()) { + drainPendingMutatedResponseBodies(); + drainPendingRequests(); + } } } @@ -1019,10 +1024,6 @@ private void proceedWithHalfClose() { @Override public void halfClose() { - System.out.println("halfClose: passThrough=" + passThroughMode.get() + - ", extProcStreamCompleted=" + extProcStreamState.get().isCompleted() + - ", extProcStreamDraining=" + extProcStreamState.get().isDraining() + - ", bodyMode=" + currentProcessingMode.getRequestBodyMode()); clientHalfCloseStartNanos = System.nanoTime(); if (passThroughMode.get()) { if (requestSideClosed.compareAndSet(false, true)) { @@ -1084,11 +1085,14 @@ private void handleRequestBodyResponse(BodyResponse bodyResponse) { StreamedBodyResponse streamed = mutation.getStreamedResponse(); if (!streamed.getEndOfStreamWithoutMessage()) { com.google.protobuf.ByteString body = streamed.getBody(); - if (!config.getObservabilityMode() && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { + if (!config.getObservabilityMode() + && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { synchronized (streamLock) { if (sidestreamToUpstreamWindow <= 0) { internalOnError(Status.INTERNAL - .withDescription("Flow control violation: received client body from ext_proc when window is closed") + .withDescription( + "Flow control violation: received client body from ext_proc " + + "when window is closed") .asRuntimeException()); return; } @@ -1097,7 +1101,8 @@ private void handleRequestBodyResponse(BodyResponse bodyResponse) { } } super.sendMessage(new KnownLengthInputStream(body)); - if (!config.getObservabilityMode() && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { + if (!config.getObservabilityMode() + && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { trySendAccumulatedWindowUpdates(); } } @@ -1118,29 +1123,74 @@ private void handleResponseBodyResponse( StreamedBodyResponse streamed = mutation.getStreamedResponse(); com.google.protobuf.ByteString body = streamed.getBody(); final int bodySize = body.size(); - if (!config.getObservabilityMode() && currentProcessingMode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC) { + if (!config.getObservabilityMode() + && currentProcessingMode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC) { synchronized (streamLock) { if (sidestreamToDownstreamWindow <= 0) { internalOnError(Status.INTERNAL - .withDescription("Flow control violation: received server body from ext_proc when window is closed") + .withDescription( + "Flow control violation: received server body from ext_proc " + + "when window is closed") .asRuntimeException()); return; } sidestreamToDownstreamWindow -= bodySize; } } + deliverResponseBody(body, listener); + } + } + } + + private void deliverResponseBody(ByteString body, DataPlaneListener listener) { + synchronized (streamLock) { + if (config.getObservabilityMode() + || currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC) { + callContext.run(() -> listener.onExternalBody(body)); + return; + } + + if (downstreamRequestsPending > 0) { + downstreamRequestsPending--; + final int bodySize = body.size(); callContext.run(() -> { try { listener.onExternalBody(body); } finally { - if (!config.getObservabilityMode() && currentProcessingMode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC) { + synchronized (streamLock) { + accumulatedWindowUpdateSidestreamToDownstream += bodySize; + } + trySendAccumulatedWindowUpdates(); + } + }); + } else { + pendingMutatedResponseBodies.add(body); + } + } + } + + private void drainPendingMutatedResponseBodies() { + synchronized (streamLock) { + while (downstreamRequestsPending > 0 && !pendingMutatedResponseBodies.isEmpty()) { + ByteString body = pendingMutatedResponseBodies.poll(); + downstreamRequestsPending--; + if (pendingRequests.get() > 0) { + pendingRequests.decrementAndGet(); + } + final int bodySize = body.size(); + final DataPlaneListener listener = wrappedListener; + if (listener != null) { + callContext.run(() -> { + try { + listener.onExternalBody(body); + } finally { synchronized (streamLock) { accumulatedWindowUpdateSidestreamToDownstream += bodySize; } trySendAccumulatedWindowUpdates(); } - } - }); + }); + } } } } diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index 6eb7b265664..fec739a330a 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -114,6 +114,10 @@ */ @RunWith(JUnit4.class) public class ExternalProcessorClientInterceptorTest { + private static final String INSECURE_CREDENTIALS_TYPE_URL = + "type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials"; + static { System.setProperty("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", "true"); } @@ -167,6 +171,7 @@ public InputStream stream(String value) { return new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)); } + @Override public String parse(InputStream stream) { try { @@ -194,10 +199,12 @@ public String getServiceAuthority() { return "localhost"; } + @Override public void start(Listener2 listener) { } + @Override public void shutdown() { } @@ -206,21 +213,25 @@ public void shutdown() { return null; } + @Override protected boolean isAvailable() { return true; } + @Override protected int priority() { return 5; } + @Override public String getDefaultScheme() { return "in-process"; } + @Override public Collection> getProducedSocketAddressTypes() { return Collections.emptyList(); @@ -454,7 +465,9 @@ public void givenOverrideConfig_whenProcessingModeOverridden_thenReplacesWholeMo // Full replacement: requestBodyMode becomes GRPC, others become defaults (0/DEFAULT/NONE) assertThat(mergedMode.getRequestBodyMode()).isEqualTo(ProcessingMode.BodySendMode.GRPC); assertThat(mergedMode.getRequestHeaderMode()).isEqualTo(ProcessingMode.HeaderSendMode.DEFAULT); - assertThat(mergedMode.getResponseHeaderMode()).isEqualTo(ProcessingMode.HeaderSendMode.DEFAULT); + assertThat(mergedMode.getResponseHeaderMode()) + + .isEqualTo(ProcessingMode.HeaderSendMode.DEFAULT); assertThat(mergedMode.getResponseBodyMode()).isEqualTo(ProcessingMode.BodySendMode.NONE); } @@ -645,7 +658,8 @@ public void givenInterceptor_whenCallIntercepted_thenExtProcStubUsesSerializingE .build()) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -663,10 +677,12 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -740,7 +756,8 @@ public void givenGrpcServiceWithTimeout_whenCallIntercepted_thenExtProcStubHasCo .setTimeout(com.google.protobuf.Duration.newBuilder().setSeconds(5).build()) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -758,10 +775,12 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -857,9 +876,11 @@ public void onNext(ProcessingRequest request) { sidecarLatch.countDown(); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -963,9 +984,11 @@ public void onNext(ProcessingRequest request) { sidecarLatch.countDown(); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1069,9 +1092,11 @@ public void onNext(ProcessingRequest request) { sidecarLatch.countDown(); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1171,9 +1196,11 @@ public void onNext(ProcessingRequest request) { sidecarLatch.countDown(); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1273,9 +1300,11 @@ public void onNext(ProcessingRequest request) { sidecarLatch.countDown(); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1374,7 +1403,8 @@ public void givenGrpcServiceWithInitialMetadata_whenCallIntercepted_thenSendsMet .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -1392,9 +1422,11 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1497,9 +1529,11 @@ public void onNext(ProcessingRequest request) { sidecarLatch.countDown(); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1605,9 +1639,11 @@ public void onNext(ProcessingRequest request) { sidecarLatch.countDown(); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1713,9 +1749,11 @@ public void onNext(ProcessingRequest request) { sidecarLatch.countDown(); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1792,7 +1830,8 @@ public void givenRequestHeaderModeSend_whenStartCalled_thenCallIsBuffered() .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -1813,10 +1852,12 @@ public void onNext(ProcessingRequest request) { requestSentLatch.countDown(); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1892,7 +1933,8 @@ public void givenRequestHeaderModeSend_whenExtProcRespondsWithMutations_thenCall .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -1939,10 +1981,12 @@ public void onNext(ProcessingRequest request) { }).start(); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -2037,7 +2081,8 @@ public void givenHeaderModeSend_whenCallHasBinaryHeaders_thenBinaryHeadersForwar .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2069,10 +2114,12 @@ public void onNext(ProcessingRequest request) { }).start(); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2162,7 +2209,8 @@ public void givenRequestHeaderModeSkip_whenStartCalled_thenCallIsActivated() thr .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2182,10 +2230,12 @@ public void onNext(ProcessingRequest request) { sidecarMessages.incrementAndGet(); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2268,7 +2318,8 @@ public void onCompleted() { .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2295,9 +2346,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2383,7 +2436,8 @@ public void givenRequestBodyModeGrpc_whenSendMessageCalled_thenMessageSentToExtP .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2437,10 +2491,12 @@ public void onNext(ProcessingRequest request) { }).start(); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -2517,7 +2573,8 @@ public void givenRequestBodyModeGrpc_whenExtProcRespondsWithMutatedBody_thenMuta .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2567,10 +2624,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2652,7 +2711,8 @@ public void givenRequestBodyModeGrpc_whenExtProcRespondsEmpty_thenEmptyMsgDelive .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2696,10 +2756,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2792,7 +2854,8 @@ public void givenExtProcSignaledEndOfStream_whenClientSendsMoreMessages_thenMess .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2843,10 +2906,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2930,7 +2995,8 @@ public void givenRequestBodyModeNone_whenSendMessageCalled_thenMessageSentDirect .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.NONE).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2951,10 +3017,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3037,7 +3105,8 @@ public void givenResponseHeaderModeSend_whenExtProcRespondsWithMutatedHeaders_th .setProcessingMode(ProcessingMode.newBuilder() .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -3080,10 +3149,12 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(response.build()); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3176,7 +3247,8 @@ public void givenResponseHeaderModeSkip_responseHeadersSentDirectlyUpstream() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -3213,10 +3285,12 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(response.build()); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3274,6 +3348,7 @@ public ServerCall.Listener interceptCall( headersLatch.countDown(); } + @Override public void onClose(Status status, Metadata trailers) { appCloseLatch.countDown(); } @@ -3329,7 +3404,8 @@ public void givenResponseBodyModeGrpc_whenOnMessageCalled_thenMessageSentToExtPr .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -3378,10 +3454,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3433,6 +3511,7 @@ public void onMessage(String message) { appMessageLatch.countDown(); } + @Override public void onClose(Status status, Metadata trailers) { appCloseLatch.countDown(); @@ -3530,10 +3609,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3591,6 +3672,7 @@ public void onMessage(String message) { appMessageLatch.countDown(); } + @Override public void onClose(Status status, Metadata trailers) { appCloseLatch.countDown(); @@ -3650,6 +3732,7 @@ private void completeResponse() { } } + @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { @@ -3679,9 +3762,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { completeResponse(); @@ -3815,6 +3900,7 @@ private void completeResponse() { } } + @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { @@ -3835,10 +3921,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { completeResponse(); @@ -3956,6 +4044,7 @@ private void completeResponse() { } } + @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { @@ -3973,10 +4062,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { completeResponse(); @@ -4114,6 +4205,7 @@ private void completeResponse() { } } + @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { @@ -4131,10 +4223,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { completeResponse(); @@ -4313,10 +4407,12 @@ public void onNext( } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -4468,10 +4564,12 @@ public void onNext( } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -4589,9 +4687,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -4710,7 +4810,8 @@ public void givenRequestBodyModeGrpc_whenHalfCloseCalled_thenSuperHalfCloseDefer .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -4733,10 +4834,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -4807,7 +4910,8 @@ public void deferredHalfClose_whenExtProcRespondsWithEosWithoutMessage_thenSuper .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -4862,10 +4966,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -4903,10 +5009,12 @@ public void onNext(String value) { serverReceivedMessages.add(value); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -4973,7 +5081,8 @@ public void givenDeferredHalfClose_whenExtProcRespondsWithEndOfStream_thenSuperH .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5018,10 +5127,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5059,10 +5170,12 @@ public void onNext(String value) { serverReceivedMessages.add(value); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -5130,7 +5243,8 @@ public void halfClose() { .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5186,10 +5300,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5227,10 +5343,12 @@ public void onNext(String value) { serverReceivedMessages.add(value); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -5314,7 +5432,8 @@ public void halfClose() { .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5360,10 +5479,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5401,10 +5522,12 @@ public void onNext(String value) { serverReceivedMessages.add(value); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -5488,7 +5611,8 @@ public void givenObservabilityTrue_whenExtProcBusy_thenIsReadyReturnsFalse() .build()) .setObservabilityMode(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5514,10 +5638,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5610,7 +5736,8 @@ public void givenObservabilityMode_whenUpstreamBusy_thenIsReadyReturnsFalse() .build()) .setObservabilityMode(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5636,10 +5763,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5734,7 +5863,8 @@ public void givenNormalMode_whenUpstreamBusy_thenIsReadyReturnsTrue() .build()) .setObservabilityMode(false) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5757,10 +5887,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5847,7 +5979,8 @@ public void givenCongestionInExtProc_whenExtProcBecomesReady_thenTriggersOnReady .build()) .setObservabilityMode(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5865,10 +5998,12 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -5982,9 +6117,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -6115,7 +6252,7 @@ public void testRequestBodyDrainingBypassedWhenRequestBodyModeNone() throws Exce .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -6123,7 +6260,8 @@ public void testRequestBodyDrainingBypassedWhenRequestBodyModeNone() throws Exce .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6146,8 +6284,10 @@ public void onNext(ProcessingRequest request) { sidecarActionLatch.countDown(); } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -6191,7 +6331,8 @@ public void sendMessage(ReqT message) { try { InputStream stream = (InputStream) message; byte[] bytes = com.google.common.io.ByteStreams.toByteArray(stream); - dataPlaneSentMessages.add(new String(bytes, java.nio.charset.StandardCharsets.UTF_8)); + dataPlaneSentMessages.add( + new String(bytes, java.nio.charset.StandardCharsets.UTF_8)); super.sendMessage((ReqT) new java.io.ByteArrayInputStream(bytes)); } catch (IOException e) { throw new RuntimeException(e); @@ -6230,7 +6371,7 @@ public void testResponseBodyDrainingBypassedWhenResponseBodyModeNone() throws Ex .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -6239,7 +6380,8 @@ public void testResponseBodyDrainingBypassedWhenResponseBodyModeNone() throws Ex .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6262,8 +6404,10 @@ public void onNext(ProcessingRequest request) { sidecarActionLatch.countDown(); } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -6284,7 +6428,8 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( new ServerCalls.BidiStreamingMethod() { @@ -6294,8 +6439,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -6326,12 +6473,14 @@ public void onMessage(String message) { // Wait for the drain signal to be received and processed by client call Thread.sleep(100); - // Send response headers first (they bypass ext_proc because send mode is default SKIP, so they proceed immediately) + // Send response headers first (they bypass ext_proc because send mode is default SKIP, + // so they proceed immediately) StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); upstreamResponseObserver.onNext("Dummy for headers"); // Now call is in DRAINING state, and savedHeaders is null. - // Send response body message. Since response_body_mode is NONE, it should go directly downstream. + // Send response body message. Since response_body_mode is NONE, it should go + // directly downstream. upstreamResponseObserver.onNext("Hello Downstream"); assertThat(appMessageLatch.await(5, TimeUnit.SECONDS)).isTrue(); @@ -6350,7 +6499,7 @@ public void testResponseHeadersDrainingBypassedWhenResponseHeadersSkip() throws .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -6358,7 +6507,8 @@ public void testResponseHeadersDrainingBypassedWhenResponseHeadersSkip() throws .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6381,8 +6531,10 @@ public void onNext(ProcessingRequest request) { sidecarActionLatch.countDown(); } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -6403,7 +6555,8 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( new ServerCalls.BidiStreamingMethod() { @@ -6413,8 +6566,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -6444,7 +6599,8 @@ public void onHeaders(Metadata headers) { Thread.sleep(100); // Call is in DRAINING state. - // Send response headers from server. Since response_header_mode is SKIP, they should go directly downstream. + // Send response headers from server. Since response_header_mode is SKIP, they should + // go directly downstream. StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); upstreamResponseObserver.onNext("Dummy for headers"); @@ -6463,7 +6619,7 @@ public void testResponseTrailersDrainingBypassedWhenResponseTrailersSkip() throw .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -6472,7 +6628,8 @@ public void testResponseTrailersDrainingBypassedWhenResponseTrailersSkip() throw .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6495,8 +6652,10 @@ public void onNext(ProcessingRequest request) { sidecarActionLatch.countDown(); } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -6517,7 +6676,8 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( new ServerCalls.BidiStreamingMethod() { @@ -6527,8 +6687,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -6558,7 +6720,8 @@ public void onClose(Status status, Metadata trailers) { Thread.sleep(100); // Call is in DRAINING state. - // Complete the server call. Since response_trailer_mode is SKIP, onClose should trigger immediately. + // Complete the server call. Since response_trailer_mode is SKIP, onClose should + // trigger immediately. StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); upstreamResponseObserver.onCompleted(); @@ -6584,7 +6747,8 @@ public void givenRequestDrainActive_whenIsReadyCalled_thenReturnsFalse() throws .build()) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6608,10 +6772,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { // Don't complete responseObserver immediately to allow test to check draining state @@ -6685,7 +6851,8 @@ public void givenDrainingStream_whenExtProcStreamCompletes_thenOnReady() throws .build()) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6722,10 +6889,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -6846,7 +7015,8 @@ public void givenDrainingStream_whenObserverIsNull_thenSendMessageDoesNotQueue() .build()) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6869,9 +7039,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -6951,7 +7123,8 @@ public void givenDrainingStream_whenExtProcStreamCompletes_thenMessagesProceed() .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6984,10 +7157,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { // Already handled in the background thread @@ -7107,7 +7282,8 @@ public void onMessage(String message) { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7145,10 +7321,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { drainCompletedLatch.countDown(); @@ -7183,10 +7361,12 @@ public void onNext(String value) { dataPlaneReceivedMessages.add(value); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onNext("Direct Response"); @@ -7274,7 +7454,8 @@ public void drainingStartsAfterRequestHeaders_whenAppSendsAndHalfCloses_thenBuff .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7325,10 +7506,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { drainCompletedLatch.countDown(); @@ -7366,10 +7549,12 @@ public void onNext(String value) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onNext("Direct Response"); @@ -7467,7 +7652,8 @@ public void drainingStartsBeforeResponseHeaders_whenUpstreamResponds_thenBuffere .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7502,10 +7688,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { drainCompletedLatch.countDown(); @@ -7541,10 +7729,12 @@ public StreamObserver invoke(StreamObserver responseObserver) { public void onNext(String value) { } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -7567,11 +7757,13 @@ public void onHeaders(Metadata headers) { appReceivedHeaders.set(headers); } + @Override public void onMessage(String message) { appReceivedMessages.add(message); } + @Override public void onClose(Status status, Metadata trailers) { appReceivedStatus.set(status); @@ -7642,7 +7834,8 @@ public void drainingStartsAfterResponseHeaders_whenUpstreamResponds_thenBuffered .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7731,9 +7924,11 @@ public void onNext(ProcessingRequest request) { } } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { @@ -7770,9 +7965,11 @@ public StreamObserver invoke(StreamObserver responseObserver) { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -7797,6 +7994,7 @@ public void onHeaders(Metadata headers) { appReceivedHeaders.set(headers); } + @Override public void onMessage(String message) { appReceivedMessages.add(message); @@ -7807,6 +8005,7 @@ public void onMessage(String message) { } } + @Override public void onClose(Status status, Metadata trailers) { appReceivedStatus.set(status); @@ -7901,7 +8100,8 @@ public void givenObservabilityTrue_whenExtProcBusy_thenAppRequestsBuffered() .build()) .setObservabilityMode(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7919,10 +8119,12 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -7953,6 +8155,7 @@ public void start(Listener responseListener, Metadata headers) { super.start(responseListener, headers); } + @Override public boolean isReady() { return sidecarReady.get(); @@ -7977,10 +8180,12 @@ public StreamObserver invoke(StreamObserver responseObserver) { public void onNext(String value) { } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -8061,7 +8266,8 @@ public void givenRequestDrainActive_whenAppRequestsMessages_thenRequestsBuffered .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8084,10 +8290,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -8175,7 +8383,8 @@ public void givenBufferedRequests_whenExtProcStreamBecomesReady_thenDataPlaneDra .build()) .setObservabilityMode(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8198,10 +8407,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -8232,6 +8443,7 @@ public void start(Listener responseListener, Metadata headers) { super.start(responseListener, headers); } + @Override public boolean isReady() { return sidecarReady.get(); @@ -8319,7 +8531,8 @@ public void givenExtProcStreamCompleted_whenAppRequestsMessages_thenRequestsForw .build()) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8341,10 +8554,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -8485,10 +8700,12 @@ public void onNext(ProcessingRequest request) { }); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { extProcCompletedLatch.countDown(); @@ -8512,7 +8729,8 @@ public void onCompleted() { .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8555,11 +8773,13 @@ public void onHeaders(Metadata headers) { appEvents.add("HEADERS"); } + @Override public void onMessage(String message) { appEvents.add("MESSAGE"); } + @Override public void onClose(Status status, Metadata trailers) { appEvents.add("CLOSE:" + status.getCode()); @@ -8659,9 +8879,11 @@ public void onNext(ProcessingRequest request) { }); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { extProcCompletedLatch.countDown(); @@ -8686,7 +8908,8 @@ public void onCompleted() { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8703,6 +8926,7 @@ public void onMessage(String message) { call.sendMessage("server-response-" + message); } + @Override public void onHalfClose() { call.close(Status.OK, new Metadata()); @@ -8734,11 +8958,13 @@ public void onHeaders(Metadata headers) { appEvents.add("HEADERS"); } + @Override public void onMessage(String message) { appEvents.add("MESSAGE:" + message); } + @Override public void onClose(Status status, Metadata trailers) { appEvents.add("CLOSE:" + status.getCode()); @@ -8790,7 +9016,8 @@ public void givenFailureModeAllowFalse_whenExtProcStreamFails_thenDataPlaneCallC .build()) .setFailureModeAllow(false) // Fail Closed .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8815,10 +9042,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -8881,7 +9110,8 @@ public void givenFailureModeAllowTrue_whenExtProcStreamFails_thenCallFailsOpen() .build()) .setFailureModeAllow(true) // Fail Open .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8904,10 +9134,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -9016,7 +9248,8 @@ public void givenObservabilityMode_whenDataPlaneClosed_thenSidecarCloseIsDeferre .setDeferredCloseTimeout( com.google.protobuf.Duration.newBuilder().setSeconds(10).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9035,10 +9268,12 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { sidecarCompletedLatch.countDown(); @@ -9145,7 +9380,8 @@ public void givenUnsupportedCompressionInResponse_whenReceived_thenStreamErrored .setProcessingMode(ProcessingMode.newBuilder() .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9183,10 +9419,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -9300,7 +9538,8 @@ public void givenUnsupportedCompressionInResponseBody_whenReceived_thenStreamErr .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9350,10 +9589,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -9439,7 +9680,8 @@ public void givenHeaderSendModeDefault_whenProcessing_thenFollowsDefaultBehavior .setResponseHeaderMode(ProcessingMode.HeaderSendMode.DEFAULT) .setResponseTrailerMode(ProcessingMode.HeaderSendMode.DEFAULT).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9477,10 +9719,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -9587,7 +9831,8 @@ public void givenImmediateResponse_whenReceived_thenDataPlaneCallCancelled() .build()) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9617,10 +9862,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -9698,7 +9945,8 @@ public void givenImmediateResponseAndObservabilityTrue_whenReceived_thenImmediat .build()) .setObservabilityMode(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9728,9 +9976,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -9809,7 +10059,8 @@ public void givenImmediateResponseDisabled_whenReceivedBeforeActivation_thenSide .build()) .setDisableImmediateResponse(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9839,10 +10090,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -9921,7 +10174,8 @@ public void givenImmediateResponseDisabled_whenReceivedAfterActivation_thenSidec .build()) .setDisableImmediateResponse(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9963,10 +10217,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -10047,7 +10303,8 @@ public void givenImmediateResponseInTrailers_whenReceived_thenDataPlaneCallStatu .setProcessingMode(ProcessingMode.newBuilder() .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10107,10 +10364,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { } @@ -10207,7 +10466,8 @@ public void givenActiveRpc_whenDataPlaneCallCancelled_thenExtProcStreamIsErrored .build()) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10231,11 +10491,13 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { cancelLatch.countDown(); } + @Override public void onCompleted() { } @@ -10305,7 +10567,8 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenIsReadyReturnsFalse( .build()) .setObservabilityMode(false) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10335,10 +10598,12 @@ public void onNext(ProcessingRequest request) { }).start(); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -10456,7 +10721,8 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenAppRequestsAreBuffer .build()) .setObservabilityMode(false) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10499,10 +10765,12 @@ public void onNext(ProcessingRequest request) { }).start(); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -10534,6 +10802,7 @@ public void start(Listener responseListener, Metadata headers) { super.start(responseListener, headers); } + @Override public boolean isReady() { return sidecarReady.get(); @@ -10547,7 +10816,8 @@ public boolean isReady() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( new ServerCalls.BidiStreamingMethod() { @@ -10557,8 +10827,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -10603,7 +10875,8 @@ public void request(int numMessages) { // Sidecar busy -> request(5) should be buffered sidecarReady.set(false); proxyCall.request(5); - assertThat(dataPlaneRequestCount.get()).isEqualTo(1); // Only the initial bootstrap request went through + assertThat(dataPlaneRequestCount.get()).isEqualTo(1); + // (Only the initial bootstrap request went through) // Sidecar becomes ready -> buffered requests should start draining (pulling next message) sidecarReady.set(true); @@ -10684,7 +10957,8 @@ public void givenResponseBodyModeNone_whenExtProcBusy_thenAppRequestsAreNotBuffe .build()) .setObservabilityMode(false) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10711,10 +10985,12 @@ public void onNext(ProcessingRequest request) { }).start(); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -10743,6 +11019,7 @@ public void start(Listener responseListener, Metadata headers) { super.start(responseListener, headers); } + @Override public boolean isReady() { return sidecarReady.get(); @@ -10764,8 +11041,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -10810,7 +11089,8 @@ public void request(int numMessages) { // Sidecar busy sidecarReady.set(false); - // Since responseBodyMode is NONE and not in observabilityMode, request(5) should be passed upstream immediately + // Since responseBodyMode is NONE and not in observabilityMode, request(5) should + // be passed upstream immediately proxyCall.request(5); long startTime = System.currentTimeMillis(); @@ -10832,7 +11112,7 @@ public void testFlowControlStateInitialization() throws Exception { .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -10843,7 +11123,8 @@ public void testFlowControlStateInitialization() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10881,9 +11162,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -10918,7 +11201,9 @@ public void onCompleted() { InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_SAY_HELLO, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); proxyCall.sendMessage("Message 1"); @@ -10931,8 +11216,10 @@ public void onCompleted() { assertThat(firstRequest.hasRequestHeaders()).isTrue(); assertThat(firstRequest.hasFlowControlInit()).isTrue(); - assertThat(firstRequest.getFlowControlInit().getInitialWindowDownstreamToSidestream()).isEqualTo(65536); - assertThat(firstRequest.getFlowControlInit().getInitialWindowSidestreamToUpstream()).isEqualTo(65536); + assertThat(firstRequest.getFlowControlInit().getInitialWindowDownstreamToSidestream()) + .isEqualTo(65536); + assertThat(firstRequest.getFlowControlInit().getInitialWindowSidestreamToUpstream()) + .isEqualTo(65536); assertThat(secondRequest.hasRequestBody()).isTrue(); assertThat(secondRequest.hasFlowControlInit()).isFalse(); @@ -10949,7 +11236,7 @@ public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Except .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -10960,7 +11247,8 @@ public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Except .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10968,7 +11256,8 @@ public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Except final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch firstBodyLatch = new CountDownLatch(2); // Headers + First Body final CountDownLatch secondBodyLatch = new CountDownLatch(1); - final AtomicReference> responseObserverRef = new AtomicReference<>(); + final AtomicReference> + responseObserverRef = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -11018,9 +11307,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11055,9 +11346,11 @@ public void onNext(String value) { dataPlaneReceivedMessages.add(value); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onNext("Response"); @@ -11072,7 +11365,9 @@ public void onCompleted() { InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); @@ -11088,8 +11383,10 @@ public void onCompleted() { // Send second message (30000 bytes) - total 100000 > 65536, should buffer proxyCall.sendMessage(largeMessage30k); - // Assert that it is NOT delivered to ext_proc (delivery is synchronous on directExecutor, so we can check immediately) - assertThat(receivedRequests).hasSize(3); // Headers + First Body + Client Window Update (Path 2 replenishment) + // Assert that it is NOT delivered to ext_proc (delivery is synchronous on + // directExecutor, so we can check immediately) + assertThat(receivedRequests).hasSize(3); + // (Headers + First Body + Client Window Update (Path 2 replenishment)) assertThat(proxyCall.isReady()).isFalse(); // Now send ServerWindowUpdate from ext_proc to interceptor to increment window by 40000 @@ -11112,7 +11409,8 @@ public void onCompleted() { assertThat(proxyCall.isReady()).isTrue(); // Verify both messages reached the backend service - assertThat(dataPlaneReceivedMessages).containsExactly(largeMessage70k, largeMessage30k).inOrder(); + assertThat(dataPlaneReceivedMessages) + .containsExactly(largeMessage70k, largeMessage30k).inOrder(); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -11126,7 +11424,7 @@ public void testUpstreamToSidestreamFlowControl_EnforcesWindow() throws Exceptio .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -11137,15 +11435,18 @@ public void testUpstreamToSidestreamFlowControl_EnforcesWindow() throws Exceptio .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(4); // Headers, Request Body, Response Headers, Response Body 1 + final CountDownLatch extProcLatch = new CountDownLatch(4); + // (Headers, Request Body, Response Headers, Response Body 1) final CountDownLatch secondResponseBodyLatch = new CountDownLatch(1); - final AtomicReference> responseObserverRef = new AtomicReference<>(); + final AtomicReference> + responseObserverRef = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -11200,9 +11501,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11225,7 +11528,8 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( new ServerCalls.BidiStreamingMethod() { @@ -11235,8 +11539,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -11260,11 +11566,14 @@ public void onMessage(String message) { }; ClientCall proxyCall = - interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(appListener, new Metadata()); proxyCall.request(10); + // Send first dummy message to initialize headers and stream proxyCall.sendMessage("Client Msg"); StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); @@ -11273,27 +11582,38 @@ public void onMessage(String message) { String largeMessage70k = new String(new char[70000]).replace('\0', 'a'); String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); + // Wait for the initialization (headers, request body, response headers) to reach the sidecar assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // Upstream sends 70k response chunk. Since window is 65,536, this drives the window + // negative (-4,464). upstreamResponseObserver.onNext(largeMessage70k); + // Verify Chunk 1 is successfully delivered (2 messages total in app: dummy and chunk 1) assertThat(messagesLatch2.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appReceivedMessages).hasSize(2); + // Upstream sends 30k response chunk. Since the window is negative, the filter + // must block/buffer this chunk. upstreamResponseObserver.onNext(largeMessage30k); + // Wait a brief period and verify that the 30k chunk has NOT been sent to the sidecar Thread.sleep(200); assertThat(secondResponseBodyLatch.getCount()).isEqualTo(1); assertThat(appReceivedMessages).hasSize(2); + // Sidecar sends a ServerWindowUpdate of 40k to the filter, unblocking the window. responseObserverRef.get().onNext(ProcessingResponse.newBuilder() .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder() .setWindowIncrementUpstreamToSidestream(40000) .build()) .build()); + // Once the window is unblocked, the filter immediately forwards the 30k chunk + // to the sidecar, which processes it. assertThat(secondResponseBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // Verify that Chunk 2 is now successfully delivered to the client application assertThat(messagesLatch3.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appReceivedMessages).hasSize(3); assertThat(appReceivedMessages.get(2)).isEqualTo(largeMessage30k); @@ -11311,7 +11631,7 @@ public void testSidestreamToDownstreamFlowControl_Violations() throws Exception .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -11322,12 +11642,13 @@ public void testSidestreamToDownstreamFlowControl_Violations() throws Exception .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final String mutatedMessage = new String(new char[10000]).replace('\0', 'd'); + final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'd'); final CountDownLatch callClosedLatch = new CountDownLatch(1); final AtomicReference capturedStatus = new AtomicReference<>(); @@ -11361,12 +11682,40 @@ public void onNext(ProcessingRequest request) { .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasResponseBody()) { + // Send Chunk 1 (70k) -> Consumed by client app's pending request count of 1. + // Replenishes window. + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) + .build()) + .build()) + .build()) + .build()) + .build()); + + // Send Chunk 2 (70k) -> Excess chunk. Stored in queue. Window stays negative. + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) + .build()) + .build()) + .build()) + .build()) + .build()); + + // Send Chunk 3 (1 byte) -> Arrives when window is negative, triggering violation. responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() .setBodyMutation(BodyMutation.newBuilder() .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8(mutatedMessage)) + .setBody(ByteString.copyFromUtf8("a")) .build()) .build()) .build()) @@ -11375,9 +11724,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11400,7 +11751,8 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( new ServerCalls.BidiStreamingMethod() { @@ -11410,8 +11762,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -11423,7 +11777,9 @@ public void onCompleted() {} InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(new ClientCall.Listener() { @Override @@ -11432,16 +11788,10 @@ public void onClose(Status status, Metadata trailers) { callClosedLatch.countDown(); } }, new Metadata()); - proxyCall.request(10); + proxyCall.request(1); proxyCall.sendMessage("Client Msg"); - // Retrieve the DataPlaneClientCall and set the sidestreamToDownstreamWindow to 0 via reflection - Object dataPlaneCall = unwrapToDataPlaneClientCall(proxyCall); - java.lang.reflect.Field windowField = dataPlaneCall.getClass().getDeclaredField("sidestreamToDownstreamWindow"); - windowField.setAccessible(true); - windowField.setLong(dataPlaneCall, 0); - // Send a response from upstream to trigger headers and then the body response StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); upstreamResponseObserver.onNext("Response Msg"); @@ -11450,9 +11800,13 @@ public void onClose(Status status, Metadata trailers) { // The call should fail immediately with INTERNAL error code due to flow control violation assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); + assertThat(capturedStatus.get().getDescription()) + + .isEqualTo("External processor stream failed"); assertThat(capturedStatus.get().getCause()).isInstanceOf(io.grpc.StatusRuntimeException.class); - assertThat(capturedStatus.get().getCause().getMessage()).contains("Flow control violation: received server body from ext_proc when window is closed"); + assertThat(capturedStatus.get().getCause().getMessage()) + .contains("Flow control violation: received server body from ext_proc " + + "when window is closed"); channelManager.close(); } @@ -11465,7 +11819,7 @@ public void testSidestreamToUpstreamFlowControl_Violations() throws Exception { .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -11476,12 +11830,13 @@ public void testSidestreamToUpstreamFlowControl_Violations() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final String mutatedMessage = new String(new char[10000]).replace('\0', 'c'); + final String mutatedMessageTooLarge = new String(new char[70000]).replace('\0', 'c'); final CountDownLatch callClosedLatch = new CountDownLatch(1); final AtomicReference capturedStatus = new AtomicReference<>(); @@ -11499,23 +11854,40 @@ public void onNext(ProcessingRequest request) { .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasRequestBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8(mutatedMessage)) - .build()) - .build()) - .build()) - .build()) - .build()); + ByteString original = request.getRequestBody().getBody(); + if (original.toStringUtf8().equals("Message 1")) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8(mutatedMessageTooLarge)) + .build()) + .build()) + .build()) + .build()) + .build()); + } else { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8("a")) + .build()) + .build()) + .build()) + .build()) + .build()); + } } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11539,18 +11911,53 @@ public void onCompleted() { filterConfig, channelManager, scheduler, FAKE_CONTEXT); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello " + request); - responseObserver.onCompleted(); + .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } })) .build()); + // Build the data plane channel with a custom ClientInterceptor. + // This interceptor overrides isReady() to always return false. + // This simulates a blocked backend server + // (transport flow control buffer is full) and blocks the filter's upstream + // window replenishment. ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public boolean isReady() { + return false; + } + }; + } + }) + .directExecutor() + .build()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); proxyCall.start(new ClientCall.Listener() { @Override @@ -11560,21 +11967,23 @@ public void onClose(Status status, Metadata trailers) { } }, new Metadata()); - // Retrieve the DataPlaneClientCall and set the sidestreamToUpstreamWindow to 0 via reflection - Object dataPlaneCall = unwrapToDataPlaneClientCall(proxyCall); - java.lang.reflect.Field windowField = dataPlaneCall.getClass().getDeclaredField("sidestreamToUpstreamWindow"); - windowField.setAccessible(true); - windowField.setLong(dataPlaneCall, 0); - + // Send first message. Window becomes negative. proxyCall.sendMessage("Message 1"); + // Send second message. Window is still negative, triggering flow control violation. + proxyCall.sendMessage("Message 2"); + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); // The call should fail immediately with INTERNAL error code due to flow control violation assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); - assertThat(capturedStatus.get().getDescription()).isEqualTo("External processor stream failed"); + assertThat(capturedStatus.get().getDescription()) + + .isEqualTo("External processor stream failed"); assertThat(capturedStatus.get().getCause()).isInstanceOf(io.grpc.StatusRuntimeException.class); - assertThat(capturedStatus.get().getCause().getMessage()).contains("Flow control violation: received client body from ext_proc when window is closed"); + assertThat(capturedStatus.get().getCause().getMessage()) + .contains("Flow control violation: received client body from ext_proc " + + "when window is closed"); channelManager.close(); } @@ -11587,7 +11996,7 @@ public void testThresholdBasedWindowUpdates() throws Exception { .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -11598,7 +12007,8 @@ public void testThresholdBasedWindowUpdates() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -11638,9 +12048,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11671,8 +12083,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -11697,22 +12111,32 @@ public void onCompleted() {} assertThat(filterClientRequests(receivedRequests)).hasSize(2); assertThat(filterClientRequests(receivedRequests).get(1).hasRequestBody()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(1).getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 1"); + assertThat(filterClientRequests(receivedRequests).get(1) + .getRequestBody().getBody().toStringUtf8()) + + .isEqualTo("Msg 1"); // No window updates were accumulated yet. assertThat(filterClientRequests(receivedRequests).get(1).hasClientWindowUpdate()).isFalse(); // 2. Trigger window replenishment below threshold (e.g. 5 bytes from Msg 1 response). // The interceptor processes the response, forwards it upstream, and increments - // accumulatedWindowUpdateSidestreamToUpstream. Since 5 < 32768, it won't send standalone updates. + // accumulatedWindowUpdateSidestreamToUpstream. Since 5 < 32768, it won't send + // standalone updates. // We send another message "Msg 2" to trigger piggybacking. proxyCall.sendMessage("Msg 2"); assertThat(filterClientRequests(receivedRequests)).hasSize(3); assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2).getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 2"); + assertThat(filterClientRequests(receivedRequests).get(2) + .getRequestBody().getBody().toStringUtf8()) + + .isEqualTo("Msg 2"); // Verify accumulated 5 bytes update is piggybacked. assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(5); + assertThat(filterClientRequests(receivedRequests).get(2) + .getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(5); // 3. Accumulate past threshold (e.g. 35,000 bytes) without sending body messages. // This should trigger an immediate standalone ClientWindowUpdate. @@ -11732,7 +12156,10 @@ public void onCompleted() {} // standalone client window update received. assertThat(filterClientRequests(receivedRequests)).hasSize(4); assertThat(filterClientRequests(receivedRequests).get(3).hasClientWindowUpdate()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(3).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(35005); + assertThat(filterClientRequests(receivedRequests).get(3) + .getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(35005); assertThat(filterClientRequests(receivedRequests).get(3).hasRequestBody()).isFalse(); proxyCall.cancel("Cleanup", null); @@ -11747,7 +12174,7 @@ public void testHalfClosePiggybacking() throws Exception { .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -11758,7 +12185,8 @@ public void testHalfClosePiggybacking() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -11796,9 +12224,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11829,8 +12259,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -11861,9 +12293,15 @@ public void onCompleted() {} // Verify halfClose sends EOF request piggybacking the accumulated 12 bytes update. assertThat(filterClientRequests(receivedRequests)).hasSize(3); assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2).getRequestBody().getEndOfStreamWithoutMessage()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2) + .getRequestBody().getEndOfStreamWithoutMessage()) + + .isTrue(); assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(12); + assertThat(filterClientRequests(receivedRequests).get(2) + .getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(12); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -11877,7 +12315,7 @@ public void testPiggybackingOnRequestBody() throws Exception { .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -11888,7 +12326,8 @@ public void testPiggybackingOnRequestBody() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -11958,9 +12397,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11983,7 +12424,8 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( new ServerCalls.BidiStreamingMethod() { @@ -11993,8 +12435,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -12019,7 +12463,8 @@ public void onCompleted() {} // Wait until Msg 1 request and response are processed long startTime = System.currentTimeMillis(); - while (filterClientRequests(receivedRequests).size() < 2 && System.currentTimeMillis() - startTime < 5000) { + while (filterClientRequests(receivedRequests).size() < 2 + && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); Thread.sleep(10); } @@ -12057,20 +12502,30 @@ public void onCompleted() {} } assertThat(receivedRequests).hasSize(6); - // receivedRequests.get(3) is RespBody(Dummy for headers). It should piggyback the accumulated 15 bytes update. + // receivedRequests.get(3) is RespBody(Dummy for headers). + // It should piggyback the accumulated 15 bytes update. ProcessingRequest dummyRespBodyReq = receivedRequests.get(3); assertThat(dummyRespBodyReq.hasResponseBody()).isTrue(); assertThat(dummyRespBodyReq.hasClientWindowUpdate()).isTrue(); - assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(15); - assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(0); + assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(15); + assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) + + .isEqualTo(0); - // receivedRequests.get(5) is ReqBody(Msg 2). It should piggyback the accumulated 20 bytes update. + // receivedRequests.get(5) is ReqBody(Msg 2). + // It should piggyback the accumulated 20 bytes update. ProcessingRequest msg2Req = receivedRequests.get(5); assertThat(msg2Req.hasRequestBody()).isTrue(); assertThat(msg2Req.getRequestBody().getBody().toStringUtf8()).isEqualTo("Msg 2"); assertThat(msg2Req.hasClientWindowUpdate()).isTrue(); - assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(0); - assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(20); + assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(0); + assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) + + .isEqualTo(20); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -12084,7 +12539,7 @@ public void testPiggybackingOnResponseBody() throws Exception { .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -12095,7 +12550,8 @@ public void testPiggybackingOnResponseBody() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -12151,9 +12607,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -12176,7 +12634,8 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( new ServerCalls.BidiStreamingMethod() { @@ -12186,8 +12645,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -12212,7 +12673,8 @@ public void onCompleted() {} // Wait until Msg 1 request and response are processed long startTime = System.currentTimeMillis(); - while (filterClientRequests(receivedRequests).size() < 2 && System.currentTimeMillis() - startTime < 5000) { + while (filterClientRequests(receivedRequests).size() < 2 + && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); Thread.sleep(10); } @@ -12249,27 +12711,44 @@ public void onCompleted() {} } assertThat(receivedRequests).hasSize(6); - // receivedRequests.get(3) is RespBody(Dummy for headers). It should piggyback the accumulated 15 bytes update. + // receivedRequests.get(3) is RespBody(Dummy for headers). + // It should piggyback the accumulated 15 bytes update. ProcessingRequest dummyRespBodyReq = receivedRequests.get(3); assertThat(dummyRespBodyReq.hasResponseBody()).isTrue(); assertThat(dummyRespBodyReq.hasClientWindowUpdate()).isTrue(); - assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(15); - assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(0); + assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(15); + assertThat(dummyRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) - // receivedRequests.get(4) is RespBody(ssssssssssssssssssss). It should piggyback the accumulated 17 bytes update (from "Dummy for headers" which is 17 bytes). + .isEqualTo(0); + + // receivedRequests.get(4) is RespBody(ssssssssssssssssssss). + // It should piggyback the accumulated 17 bytes update + // (from "Dummy for headers" which is 17 bytes). ProcessingRequest ssssRespBodyReq = receivedRequests.get(4); assertThat(ssssRespBodyReq.hasResponseBody()).isTrue(); assertThat(ssssRespBodyReq.hasClientWindowUpdate()).isTrue(); - assertThat(ssssRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(0); - assertThat(ssssRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(17); + assertThat(ssssRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(0); + assertThat(ssssRespBodyReq.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) - // receivedRequests.get(5) is RespBody(Msg 2). It should piggyback the accumulated 20 bytes update (from ssssssssssssssssssss). + .isEqualTo(17); + + // receivedRequests.get(5) is RespBody(Msg 2). + // It should piggyback the accumulated 20 bytes update + // (from ssssssssssssssssssss). ProcessingRequest msg2Req = receivedRequests.get(5); assertThat(msg2Req.hasResponseBody()).isTrue(); assertThat(msg2Req.getResponseBody().getBody().toStringUtf8()).isEqualTo("Msg 2"); assertThat(msg2Req.hasClientWindowUpdate()).isTrue(); - assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(0); - assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(20); + assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(0); + assertThat(msg2Req.getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) + + .isEqualTo(20); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -12277,13 +12756,13 @@ public void onCompleted() {} @Test @SuppressWarnings("unchecked") - public void testClientWindowUpdateDeferredUntilRequestBodySendMessageCompletes() throws Exception { + public void testClientWindowUpdateDeferredUntilRequestBodySendMessage() throws Exception { ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -12294,14 +12773,16 @@ public void testClientWindowUpdateDeferredUntilRequestBodySendMessageCompletes() .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch extProcLatch = new CountDownLatch(2); // Headers + Request Body - final AtomicReference> responseObserverRef = new AtomicReference<>(); + final AtomicReference> + responseObserverRef = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -12320,7 +12801,8 @@ public void onNext(ProcessingRequest request) { .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasRequestBody()) { - // Mutate request body and send back 40000 bytes. This triggers client window update replenishment. + // Mutate request body and send back 40000 bytes. This triggers client window + // update replenishment. extProcLatch.countDown(); responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() @@ -12336,9 +12818,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -12369,8 +12853,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onNext("Response"); @@ -12408,7 +12894,9 @@ public void sendMessage(ReqT message) { .build()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); @@ -12436,7 +12924,10 @@ public void sendMessage(ReqT message) { } assertThat(receivedRequests).hasSize(3); assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); - assertThat(receivedRequests.get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(40000); + assertThat(receivedRequests.get(2) + .getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(40000); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -12444,13 +12935,13 @@ public void sendMessage(ReqT message) { @Test @SuppressWarnings("unchecked") - public void testClientWindowUpdateDeferredUntilResponseBodyOnMessageCompletes() throws Exception { + public void testClientWindowUpdateDeferredUntilResponseBodyOnMessage() throws Exception { ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -12461,14 +12952,17 @@ public void testClientWindowUpdateDeferredUntilResponseBodyOnMessageCompletes() .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(3); // Request Headers, Response Headers, Response Body 1 - final AtomicReference> responseObserverRef = new AtomicReference<>(); + final CountDownLatch extProcLatch = new CountDownLatch(3); + // (Request Headers, Response Headers, Response Body 1) + final AtomicReference> + responseObserverRef = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -12506,9 +13000,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -12531,7 +13027,8 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( new ServerCalls.BidiStreamingMethod() { @@ -12541,8 +13038,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -12570,7 +13069,9 @@ public void onMessage(String message) { }; ClientCall proxyCall = - interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(appListener, new Metadata()); proxyCall.request(10); @@ -12581,7 +13082,8 @@ public void onMessage(String message) { StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); upstreamResponseObserver.onNext("Dummy for headers"); - // Trigger response body from upstream in a background thread to avoid blocking the main test execution thread + // Trigger response body from upstream in a background thread to avoid blocking + // the main test execution thread String response40k = new String(new char[40000]).replace('\0', 'a'); new Thread(() -> upstreamResponseObserver.onNext(response40k)).start(); @@ -12591,7 +13093,8 @@ public void onMessage(String message) { // Wait a brief moment to make sure no standalone ClientWindowUpdate is sent while blocked Thread.sleep(200); - assertThat(receivedRequests).hasSize(4); // Headers + Response Headers + Dummy Body + Response 40k Body + assertThat(receivedRequests).hasSize(4); + // (Headers + Response Headers + Dummy Body + Response 40k Body) // Unblock the onMessage call blockOnMessageLatch.countDown(); @@ -12603,7 +13106,10 @@ public void onMessage(String message) { } assertThat(receivedRequests).hasSize(5); assertThat(receivedRequests.get(4).hasClientWindowUpdate()).isTrue(); - assertThat(receivedRequests.get(4).getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()).isEqualTo(40000); + assertThat(receivedRequests.get(4) + .getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) + + .isEqualTo(40000); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -12611,13 +13117,13 @@ public void onMessage(String message) { @Test @SuppressWarnings("unchecked") - public void testClientWindowUpdateSentImmediatelyWhenSidestreamToUpstreamWindowExhaustedEvenIfClientCallNotReady() throws Exception { + public void testClientWindowUpdateSentImmediatelyOnWindowExhaustion() throws Exception { ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + extProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service.channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) .build()) .build()) .build()) @@ -12628,14 +13134,16 @@ public void testClientWindowUpdateSentImmediatelyWhenSidestreamToUpstreamWindowE .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch extProcLatch = new CountDownLatch(2); // Headers + Request Body - final AtomicReference> responseObserverRef = new AtomicReference<>(); + final AtomicReference> + responseObserverRef = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -12655,7 +13163,8 @@ public void onNext(ProcessingRequest request) { .build()); } else if (request.hasRequestBody()) { extProcLatch.countDown(); - // Mutate request body and send back 65536 bytes. This completely exhausts the return window (starts at 65536). + // Mutate request body and send back 65536 bytes. This completely exhausts the + // return window (starts at 65536). responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() @@ -12670,9 +13179,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -12703,8 +13214,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { return new StreamObserver() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onNext("Response"); @@ -12715,6 +13228,9 @@ public void onCompleted() { })) .build()); + final AtomicBoolean transportReady = new AtomicBoolean(false); + final AtomicReference> dataPlaneListenerRef = new AtomicReference<>(); + ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName) .intercept(new ClientInterceptor() { @@ -12723,9 +13239,15 @@ public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + dataPlaneListenerRef.set(responseListener); + super.start(responseListener, headers); + } + @Override public boolean isReady() { - return false; // Force client call to be not ready + return transportReady.get(); } }; } @@ -12734,7 +13256,9 @@ public boolean isReady() { .build()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + interceptCall(interceptor, METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); @@ -12744,15 +13268,24 @@ public boolean isReady() { assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // Since the window is exhausted (65536 bytes returned from sidecar), the ClientWindowUpdate - // should be sent immediately even though the client call is not ready (isReady() == false). + // Since the window is exhausted but isReady() is false, no window update should be sent. + // Wait a brief moment to ensure no window update is sent. + Thread.sleep(200); + assertThat(receivedRequests).hasSize(2); + + // Now, trigger transport ready. This should flush the accumulated increment of 65536. + transportReady.set(true); + dataPlaneListenerRef.get().onReady(); + long startTime = System.currentTimeMillis(); while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { Thread.sleep(10); } assertThat(receivedRequests).hasSize(3); assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); - assertThat(receivedRequests.get(2).getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()).isEqualTo(65536); + assertThat(receivedRequests.get(2) + .getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + .isEqualTo(65536); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -12777,7 +13310,8 @@ public void givenClientStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveM .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -12901,10 +13435,12 @@ public void onNext(ProcessingRequest request) { }); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -12933,10 +13469,12 @@ public void onNext(String value) { serverReceivedBody.set(value); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -13001,6 +13539,7 @@ public void onHeaders(Metadata headers) { } }); + final AtomicReference errorRef = new AtomicReference<>(); final AtomicReference clientReceivedBody = new AtomicReference<>(); StreamObserver requestObserver = ClientCalls.asyncClientStreamingCall( interceptCall(interceptor, @@ -13013,11 +13552,14 @@ public void onNext(String value) { clientReceivedBody.set(value); } + @Override public void onError(Throwable t) { + errorRef.set(t); finishLatch.countDown(); } + @Override public void onCompleted() { finishLatch.countDown(); @@ -13031,6 +13573,9 @@ public void onCompleted() { throw new AssertionError("Sidecar actions failed. Received: " + receivedPhases); } assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); + if (errorRef.get() != null) { + throw new AssertionError("RPC failed", errorRef.get()); + } List expectedPhases = Arrays.asList( @@ -13070,7 +13615,8 @@ public void givenBidiStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveMut .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -13198,10 +13744,12 @@ public void onNext(ProcessingRequest request) { }); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13233,10 +13781,12 @@ public void onNext(String value) { responseObserver.onNext(value + "Echo"); } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13304,6 +13854,7 @@ public void onHeaders(Metadata headers) { } }); + final AtomicReference errorRef = new AtomicReference<>(); StreamObserver bidiRequestObserver = ClientCalls.asyncBidiStreamingCall( interceptCall(interceptor, METHOD_BIDI_STREAMING, @@ -13315,11 +13866,14 @@ public void onNext(String value) { clientReceivedBody.set(value); } + @Override public void onError(Throwable t) { + errorRef.set(t); finishLatch.countDown(); } + @Override public void onCompleted() { finishLatch.countDown(); @@ -13333,6 +13887,9 @@ public void onCompleted() { throw new AssertionError("Sidecar bidi actions failed. Received: " + receivedPhases); } assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); + if (errorRef.get() != null) { + throw new AssertionError("RPC failed", errorRef.get()); + } List expectedPhases = Arrays.asList( @@ -13388,10 +13945,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13519,10 +14078,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13662,10 +14223,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13784,10 +14347,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13944,10 +14509,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14051,10 +14618,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14173,11 +14742,13 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { extProcError.set(t); } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14273,11 +14844,13 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { extProcError.set(t); } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14298,7 +14871,8 @@ public void onCompleted() { .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -14384,11 +14958,13 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { extProcError.set(t); } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14409,7 +14985,8 @@ public void onCompleted() { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -14500,11 +15077,13 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { extProcError.set(t); } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14528,7 +15107,8 @@ public void onCompleted() { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -14610,10 +15190,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14757,9 +15339,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { observerRef.get().onCompleted(); @@ -14785,9 +15369,11 @@ public StreamObserver invoke(StreamObserver responseObserver) { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14840,9 +15426,11 @@ public void onCompleted() { @Override public void onNext(String value) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { allDoneLatch.countDown(); @@ -14910,11 +15498,13 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { sidecarFinishedLatch.countDown(); } + @Override public void onCompleted() { sidecarFinishedLatch.countDown(); @@ -15020,11 +15610,13 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { sidecarFinishedLatch.countDown(); } + @Override public void onCompleted() { sidecarFinishedLatch.countDown(); @@ -15129,10 +15721,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -15284,10 +15878,12 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) { } + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -15434,7 +16030,8 @@ public ServerCall.Listener interceptCall( ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) .setFailureModeAllow(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -15454,9 +16051,11 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -15537,7 +16136,8 @@ public void onClose(Status status, Metadata trailers) { ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) .setFailureModeAllow(false) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -15557,9 +16157,11 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -15641,7 +16243,8 @@ public void givenFailureModeAllowTrue_whenExtProcStreamFailsAfterRequestBodySent .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -15667,9 +16270,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -15759,7 +16364,8 @@ public void givenFailureModeAllowTrue_whenExtProcStreamFailsAfterResponseBodySen .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -15796,9 +16402,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -15878,7 +16486,8 @@ public void givenObservabilityTrue_whenExtProcStreamFails_thenCallContinues() .setFailureModeAllow(false) .setObservabilityMode(true) .build(); - ConfigOrError configOrError = + ConfigOrError + configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -15902,9 +16511,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -15988,9 +16599,11 @@ public void onNext(ProcessingRequest request) { .build()); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -16110,9 +16723,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -16173,17 +16788,20 @@ public void onHeaders(Metadata headers) { onHeadersContext.set(testKey.get()); } + @Override public void onMessage(String message) { onMessageContext.set(testKey.get()); } + @Override public void onClose(Status status, Metadata trailers) { onCloseContext.set(testKey.get()); latch.countDown(); } + @Override public void onReady() { onReadyContext.set(testKey.get()); @@ -16238,9 +16856,11 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) {} + @Override public void onError(Throwable t) {} + @Override public void onCompleted() {} }; @@ -16355,9 +16975,11 @@ public void onNext(ProcessingRequest request) { requestSentLatch.countDown(); } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -16479,9 +17101,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -16602,9 +17226,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -16724,9 +17350,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -16831,9 +17459,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -16934,9 +17564,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17042,9 +17674,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17145,9 +17779,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17255,9 +17891,11 @@ public void onNext(ProcessingRequest request) { } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17307,7 +17945,9 @@ public void onClose(Status status, Metadata trailers) { private static List filterClientRequests(List requests) { List clientRequests = new ArrayList<>(); for (ProcessingRequest r : requests) { - if (r.hasRequestHeaders() || r.hasRequestBody() || (r.hasClientWindowUpdate() && !r.hasResponseBody() && !r.hasResponseHeaders() && !r.hasResponseTrailers())) { + if (r.hasRequestHeaders() || r.hasRequestBody() + || (r.hasClientWindowUpdate() && !r.hasResponseBody() + && !r.hasResponseHeaders() && !r.hasResponseTrailers())) { clientRequests.add(r); } } @@ -17337,39 +17977,5 @@ private void shutdownAndAwaitTermination(ExecutorService executor) { } } - private static Object unwrapToDataPlaneClientCall(ClientCall call) throws Exception { - ClientCall current = call; - while (current != null) { - if (current.getClass().getName().equals("io.grpc.xds.ExternalProcessorClientInterceptor$DataPlaneClientCall")) { - return current; - } - java.lang.reflect.Field delegateField = null; - Class c = current.getClass(); - while (c != null) { - try { - delegateField = c.getDeclaredField("delegate"); - break; - } catch (NoSuchFieldException e) { - try { - delegateField = c.getDeclaredField("val$rawCall"); - break; - } catch (NoSuchFieldException e2) { - c = c.getSuperclass(); - } - } - } - if (delegateField == null) { - try { - java.lang.reflect.Method getDelegateMethod = current.getClass().getMethod("delegate"); - current = (ClientCall) getDelegateMethod.invoke(current); - continue; - } catch (Throwable ignored) {} - throw new IllegalArgumentException("Could not find delegate or val$rawCall field in " + current.getClass().getName()); - } - delegateField.setAccessible(true); - current = (ClientCall) delegateField.get(current); - } - throw new IllegalArgumentException("Could not unwrap to DataPlaneClientCall: " + call); - } } From e34bc57fadada774c7377992b288b8a0a417ba9f Mon Sep 17 00:00:00 2001 From: Kannan J Date: Wed, 22 Jul 2026 12:05:03 +0000 Subject: [PATCH 09/13] xds: implement upstream flow control queuing and draining in ext_proc client interceptor - Queue mutated request body messages received from ext_proc in a new `pendingUpstreamBodyMessages` queue if the upstream transport is not ready (`isReady() == false`). - Drain the queued request body messages and forward them to the transport when the transport becomes ready (triggered in `onReady()`). - Defer request half-close if there are still body messages in the queue, executing the half-close once the queue is fully drained. - Remove the `isReady()` check when merging or sending accumulated client window updates (`accumulatedWindowUpdateSidestreamToUpstream`), ensuring window updates are always propagated. - Add a new unit test `testSidestreamToUpstreamFlowControl_QueuingAndDraining` in `ExternalProcessorClientInterceptorTest` to verify flow control queuing, transport transitions, and draining behavior. --- .../ExternalProcessorClientInterceptor.java | 182 ++-- ...xternalProcessorClientInterceptorTest.java | 956 ++++++++---------- 2 files changed, 526 insertions(+), 612 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java index f6f7da52001..9357462ba7b 100644 --- a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java +++ b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java @@ -197,15 +197,7 @@ public ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { - java.util.concurrent.Executor callExecutor = callOptions.getExecutor(); - if (callExecutor == null) { - callExecutor = new java.util.concurrent.Executor() { - @Override - public void execute(Runnable command) { - command.run(); - } - }; - } + Executor callExecutor = callOptions.getExecutor(); SerializingExecutor serializingExecutor = new SerializingExecutor(callExecutor); ExternalProcessorGrpc.ExternalProcessorStub extProcStub = ExternalProcessorGrpc.newStub( @@ -300,6 +292,7 @@ private static class DataPlaneClientCall private final AtomicInteger pendingRequests = new AtomicInteger(0); private final ProcessingMode currentProcessingMode; + // Default initial window size private static final long DEFAULT_INITIAL_WINDOW_SIZE = 65536; // Outbound (sending) windows @@ -310,23 +303,30 @@ private static class DataPlaneClientCall private long sidestreamToUpstreamWindow = DEFAULT_INITIAL_WINDOW_SIZE; private long sidestreamToDownstreamWindow = DEFAULT_INITIAL_WINDOW_SIZE; - // Buffered request body messages from downstream - private final Queue pendingRequestBodyMessages = - new java.util.concurrent.ConcurrentLinkedQueue<>(); + // Threshold to trigger standalone client window updates + private static final long WINDOW_UPDATE_THRESHOLD = DEFAULT_INITIAL_WINDOW_SIZE / 2; + + // Path 1: Pending/buffered request body messages from downstream + private final Queue pendingRequestBodyMessages = new ConcurrentLinkedQueue<>(); - // Buffered mutated response bodies from sidecar + // Path 2: Buffered request body messages from ext_proc server to forward upstream + private final Queue pendingUpstreamBodyMessages = + new java.util.concurrent.ConcurrentLinkedQueue<>(); + // Path 4: Outstanding requests from downstream for pulling responses private int downstreamRequestsPending = 0; + // Buffered mutated response bodies from ext_proc server private final Queue pendingMutatedResponseBodies = new java.util.concurrent.ConcurrentLinkedQueue<>(); + // Deferred half-close flag for upstream direction + private final AtomicBoolean pendingUpstreamHalfClose = new AtomicBoolean(false); - private static final long WINDOW_UPDATE_THRESHOLD = DEFAULT_INITIAL_WINDOW_SIZE / 2; - - // Accumulated client window updates for external processor + // Accumulated client window updates to send to ext_proc private long accumulatedWindowUpdateSidestreamToUpstream = 0; private long accumulatedWindowUpdateSidestreamToDownstream = 0; - // Track whether flow_control_init has been sent on the stream + // Flag to track if FlowControlInit was sent in the initial message private boolean flowControlInitSent = false; + private final MethodDescriptor method; private final Channel channel; private final MetricRecorder metricsRecorder; @@ -488,10 +488,12 @@ public void onNext(ProcessingResponse response) { upstreamToSidestreamWindow += update.getWindowIncrementUpstreamToSidestream(); drainPendingRequestBodyMessages(); drainPendingRequests(); + if (wrappedListener != null) { + wrappedListener.drainSavedMessages(); + } } - if (wrappedListener != null) { - wrappedListener.drainSavedMessages(); - } + // If isReady() becomes true (depends on updated downstreamToSidestreamWindow), + // notify the client application via onReadyNotify() (runs unlocked). if (!wasReady && isReady()) { onReadyNotify(); } @@ -671,7 +673,7 @@ public void onCompleted() { == ProcessingMode.HeaderSendMode.DEFAULT; if (sendRequestHeaders) { - sendToExtProcDirect(ProcessingRequest.newBuilder() + sendToExtProc(ProcessingRequest.newBuilder() .setRequestHeaders(HttpHeaders.newBuilder() .setHeaders(toHeaderMap(headers, config.getForwardRulesConfig())) .setEndOfStream(false) @@ -684,7 +686,7 @@ public void onCompleted() { } } - private void sendToExtProcDirect(ProcessingRequest request) { + private void sendToExtProc(ProcessingRequest request) { synchronized (streamLock) { if (extProcStreamState.get().isCompleted()) { return; @@ -743,8 +745,7 @@ private void sendToExtProcDirect(ProcessingRequest request) { void mergeAccumulatedWindowUpdates(ProcessingRequest.Builder requestBuilder) { synchronized (streamLock) { - long incrementUpstream = super.isReady() - ? accumulatedWindowUpdateSidestreamToUpstream : 0; + long incrementUpstream = accumulatedWindowUpdateSidestreamToUpstream; long incrementDownstream = accumulatedWindowUpdateSidestreamToDownstream; if (incrementUpstream > 0 || incrementDownstream > 0) { @@ -766,8 +767,7 @@ private void trySendAccumulatedWindowUpdates() { if (extProcStreamState.get().isCompleted()) { return; } - long incrementUpstream = super.isReady() - ? accumulatedWindowUpdateSidestreamToUpstream : 0; + long incrementUpstream = accumulatedWindowUpdateSidestreamToUpstream; long incrementDownstream = accumulatedWindowUpdateSidestreamToDownstream; boolean shouldSend = (incrementUpstream > 0 || incrementDownstream > 0) && ( @@ -784,7 +784,7 @@ private void trySendAccumulatedWindowUpdates() { sidestreamToUpstreamWindow += incrementUpstream; sidestreamToDownstreamWindow += incrementDownstream; - sendToExtProcDirect(ProcessingRequest.newBuilder() + sendToExtProc(ProcessingRequest.newBuilder() .setClientWindowUpdate(ProcessingRequest.ClientWindowUpdate.newBuilder() .setWindowIncrementSidestreamToUpstream(incrementUpstream) .setWindowIncrementSidestreamToDownstream(incrementDownstream) @@ -811,7 +811,7 @@ void drainPendingRequests() { } // Normal mode flow control: pull 1 message at a time - if (isSidecarReady() && upstreamToSidestreamWindow > 0 && pendingRequests.get() > 0) { + if (isExtProcReady() && upstreamToSidestreamWindow > 0 && pendingRequests.get() > 0) { super.request(1); pendingRequests.decrementAndGet(); } @@ -863,7 +863,7 @@ private void onReadyNotify() { wrappedListener.onReadyNotify(); } - boolean isSidecarReady() { + boolean isExtProcReady() { ExtProcStreamState state = extProcStreamState.get(); if (state.isCompleted()) { return true; @@ -889,11 +889,11 @@ public boolean isReady() { return false; } synchronized (streamLock) { - boolean sidecarReady = isSidecarReady(); + boolean extProcReady = isExtProcReady(); if (config.getObservabilityMode()) { - return super.isReady() && sidecarReady; + return super.isReady() && extProcReady; } - return downstreamToSidestreamWindow > 0 && sidecarReady + return downstreamToSidestreamWindow > 0 && extProcReady && pendingRequestBodyMessages.isEmpty(); } } @@ -904,22 +904,34 @@ public void request(int numMessages) { super.request(numMessages); return; } - if (!config.getObservabilityMode() - && currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC) { - super.request(numMessages); - return; - } - if (config.getObservabilityMode() - || currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC) { - super.request(numMessages); - return; - } synchronized (streamLock) { - pendingRequests.addAndGet(numMessages); - downstreamRequestsPending += numMessages; - if (isSidecarReady()) { - drainPendingMutatedResponseBodies(); - drainPendingRequests(); + boolean sendResponseBodiesToExtProc = config.getObservabilityMode() + || currentProcessingMode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC; + + if (!sendResponseBodiesToExtProc) { + // We do not send response bodies to ext_proc server at all. Bypassed. + super.request(numMessages); + return; + } + + // We send response bodies to ext_proc server (either in normal GRPC mode or observability mode). + // Gated by ext_proc server readiness. + boolean normalFlowControl = !config.getObservabilityMode(); // i.e. normal GRPC response body mode + + if (normalFlowControl) { + pendingRequests.addAndGet(numMessages); + downstreamRequestsPending += numMessages; + if (isExtProcReady()) { + drainPendingMutatedResponseBodies(); + drainPendingRequests(); + } + } else { + // Observability mode: gate on readiness but pull all at once + if (isExtProcReady()) { + super.request(numMessages); + } else { + pendingRequests.addAndGet(numMessages); + } } } } @@ -966,7 +978,7 @@ public void sendMessage(InputStream message) { try { ByteString bodyByteString = outboundStreamToByteString(message); if (config.getObservabilityMode()) { - sendToExtProcDirect(ProcessingRequest.newBuilder() + sendToExtProc(ProcessingRequest.newBuilder() .setRequestBody(HttpBody.newBuilder() .setBody(bodyByteString) .setEndOfStream(false) @@ -996,7 +1008,7 @@ private void sendRequestBodyToExtProc(ByteString body) { .setEndOfStream(false) .build()); mergeAccumulatedWindowUpdates(builder); - sendToExtProcDirect(builder.build()); + sendToExtProc(builder.build()); bodyMessageSentToExtProc.set(true); } } @@ -1060,7 +1072,7 @@ public void halfClose() { .setEndOfStreamWithoutMessage(true) .build()); mergeAccumulatedWindowUpdates(builder); - sendToExtProcDirect(builder.build()); + sendToExtProc(builder.build()); } } @@ -1087,6 +1099,7 @@ private void handleRequestBodyResponse(BodyResponse bodyResponse) { com.google.protobuf.ByteString body = streamed.getBody(); if (!config.getObservabilityMode() && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { + boolean sendImmediately = false; synchronized (streamLock) { if (sidestreamToUpstreamWindow <= 0) { internalOnError(Status.INTERNAL @@ -1097,18 +1110,30 @@ private void handleRequestBodyResponse(BodyResponse bodyResponse) { return; } sidestreamToUpstreamWindow -= body.size(); - accumulatedWindowUpdateSidestreamToUpstream += body.size(); + if (super.isReady() && pendingUpstreamBodyMessages.isEmpty()) { + sendImmediately = true; + accumulatedWindowUpdateSidestreamToUpstream += body.size(); + } else { + pendingUpstreamBodyMessages.add(body); + } } - } - super.sendMessage(new KnownLengthInputStream(body)); - if (!config.getObservabilityMode() - && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { - trySendAccumulatedWindowUpdates(); + if (sendImmediately) { + super.sendMessage(new KnownLengthInputStream(body)); + trySendAccumulatedWindowUpdates(); + } + } else { + super.sendMessage(new KnownLengthInputStream(body)); } } if (streamed.getEndOfStream() || streamed.getEndOfStreamWithoutMessage()) { - if (requestSideClosed.compareAndSet(false, true)) { - proceedWithHalfClose(); + synchronized (streamLock) { + if (pendingUpstreamBodyMessages.isEmpty()) { + if (requestSideClosed.compareAndSet(false, true)) { + proceedWithHalfClose(); + } + } else { + pendingUpstreamHalfClose.set(true); + } } } } @@ -1195,6 +1220,34 @@ private void drainPendingMutatedResponseBodies() { } } + void drainPendingUpstreamBodyMessages() { + while (true) { + ByteString body = null; + boolean triggerHalfClose = false; + synchronized (streamLock) { + if (super.isReady() && !pendingUpstreamBodyMessages.isEmpty()) { + body = pendingUpstreamBodyMessages.poll(); + if (body != null) { + accumulatedWindowUpdateSidestreamToUpstream += body.size(); + } + if (pendingUpstreamBodyMessages.isEmpty() && pendingUpstreamHalfClose.compareAndSet(true, false)) { + triggerHalfClose = true; + } + } + } + if (body == null) { + break; + } + super.sendMessage(new KnownLengthInputStream(body)); + trySendAccumulatedWindowUpdates(); + if (triggerHalfClose) { + if (requestSideClosed.compareAndSet(false, true)) { + proceedWithHalfClose(); + } + } + } + } + private void handleImmediateResponse(ImmediateResponse immediate, DataPlaneListener listener) throws HeaderMutationDisallowedException { Status status = Status.fromCodeValue(immediate.getGrpcStatus().getStatus()); @@ -1307,6 +1360,8 @@ AtomicBoolean getIsProcessingTrailers() { private static class DataPlaneListener extends SimpleForwardingClientCallListener { private final ClientCall rawCall; private final DataPlaneClientCall dataPlaneClientCall; + // Path 3: Upstream response bodies queued because upstream to sidestream window not available, + // response headers not cleared by ext_proc or ext_proc stream draining private final Queue savedMessages = new ConcurrentLinkedQueue<>(); private boolean inboundPassThrough = false; @Nullable private volatile Metadata savedHeaders; @@ -1344,6 +1399,7 @@ void setImmediateResponse(Status status, Metadata trailers) { @Override public void onReady() { + dataPlaneClientCall.drainPendingUpstreamBodyMessages(); dataPlaneClientCall.trySendAccumulatedWindowUpdates(); dataPlaneClientCall.drainPendingRequests(); onReadyNotify(); @@ -1372,7 +1428,7 @@ public void onHeaders(Metadata headers) { } this.savedHeaders = headers; - dataPlaneClientCall.sendToExtProcDirect(ProcessingRequest.newBuilder() + dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder() .setResponseHeaders(HttpHeaders.newBuilder() .setHeaders( toHeaderMap(headers, dataPlaneClientCall.getConfig().getForwardRulesConfig())) @@ -1443,7 +1499,7 @@ public void onMessage(InputStream message) { void drainSavedMessages() { synchronized (dataPlaneClientCall.getStreamLock()) { - while (dataPlaneClientCall.isSidecarReady() + while (dataPlaneClientCall.isExtProcReady() && dataPlaneClientCall.upstreamToSidestreamWindow > 0 && !savedMessages.isEmpty()) { InputStream msg = savedMessages.poll(); @@ -1605,7 +1661,7 @@ private void triggerCloseHandshake() { if (trailersOnly.get()) { if (sendResponseHeaders) { - dataPlaneClientCall.sendToExtProcDirect(ProcessingRequest.newBuilder() + dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder() .setResponseHeaders(HttpHeaders.newBuilder() .setHeaders( toHeaderMap( @@ -1622,7 +1678,7 @@ private void triggerCloseHandshake() { } } else if (sendResponseTrailers) { dataPlaneClientCall.getIsProcessingTrailers().set(true); - dataPlaneClientCall.sendToExtProcDirect(ProcessingRequest.newBuilder() + dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder() .setResponseTrailers(HttpTrailers.newBuilder() .setTrailers( toHeaderMap( @@ -1665,7 +1721,7 @@ private void sendResponseBodyToExtProc( ProcessingRequest.Builder builder = ProcessingRequest.newBuilder() .setResponseBody(bodyBuilder.build()); dataPlaneClientCall.mergeAccumulatedWindowUpdates(builder); - dataPlaneClientCall.sendToExtProcDirect(builder.build()); + dataPlaneClientCall.sendToExtProc(builder.build()); } } } diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index fec739a330a..56a7ff48dd9 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -171,7 +171,6 @@ public InputStream stream(String value) { return new ByteArrayInputStream(value.getBytes(StandardCharsets.UTF_8)); } - @Override public String parse(InputStream stream) { try { @@ -199,12 +198,10 @@ public String getServiceAuthority() { return "localhost"; } - @Override public void start(Listener2 listener) { } - @Override public void shutdown() { } @@ -213,25 +210,21 @@ public void shutdown() { return null; } - @Override protected boolean isAvailable() { return true; } - @Override protected int priority() { return 5; } - @Override public String getDefaultScheme() { return "in-process"; } - @Override public Collection> getProducedSocketAddressTypes() { return Collections.emptyList(); @@ -272,8 +265,6 @@ public void setUp() throws Exception { .build().start()); } - - private ExternalProcessor.Builder createBaseProto(String targetName) { return ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() @@ -287,7 +278,6 @@ private ExternalProcessor.Builder createBaseProto(String targetName) { .build()); } - // --- Category 1: Configuration Override --- @Test @@ -382,8 +372,6 @@ public void givenOverrideConfig_whenFailureModeAllowOverridden_thenTakesEffect() assertThat(interceptor.getFilterConfig().getFailureModeAllow()).isTrue(); } - - @Test public void givenOverrideConfig_whenOtherFieldsOverridden_thenReplaced() throws Exception { ExternalProcessor parentProto = createBaseProto(extProcServerName) @@ -550,7 +538,6 @@ public void givenOverrideConfig_whenSomeFieldsOverridden_thenMergedCorrectly() t .containsExactly("attr-parent"); } - @Test public void givenOverrideConfig_whenDisableImmediateResponseOverridden_thenInheritedFromParent() throws Exception { @@ -677,12 +664,10 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -775,12 +760,10 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -836,8 +819,6 @@ public ClientCall interceptCall( channelManager.close(); } - - // --- Category 3: Protocol config propagation --- @Test @@ -846,7 +827,7 @@ public void protocolConfig_onHeaders() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch sidecarLatch = new CountDownLatch(3); + final CountDownLatch extProcLatch = new CountDownLatch(3); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -873,14 +854,12 @@ public void onNext(ProcessingRequest request) { .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } - sidecarLatch.countDown(); + extProcLatch.countDown(); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -930,7 +909,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(2); @@ -958,7 +937,7 @@ public void protocolConfig_onBody() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch sidecarLatch = new CountDownLatch(2); + final CountDownLatch extProcLatch = new CountDownLatch(2); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -981,14 +960,12 @@ public void onNext(ProcessingRequest request) { .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } - sidecarLatch.countDown(); + extProcLatch.countDown(); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1038,7 +1015,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); @@ -1066,7 +1043,7 @@ public void protocolConfig_onResponseHeaders() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch sidecarLatch = new CountDownLatch(2); + final CountDownLatch extProcLatch = new CountDownLatch(2); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1089,14 +1066,12 @@ public void onNext(ProcessingRequest request) { .setResponseBody(BodyResponse.newBuilder().build()) .build()); } - sidecarLatch.countDown(); + extProcLatch.countDown(); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1146,7 +1121,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); @@ -1174,7 +1149,7 @@ public void protocolConfig_onResponseBody() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1193,14 +1168,12 @@ public void onNext(ProcessingRequest request) { .setResponseBody(BodyResponse.newBuilder().build()) .build()); } - sidecarLatch.countDown(); + extProcLatch.countDown(); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1250,7 +1223,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); @@ -1278,7 +1251,7 @@ public void protocolConfig_onResponseTrailers() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1297,14 +1270,12 @@ public void onNext(ProcessingRequest request) { .setResponseTrailers(TrailersResponse.newBuilder().build()) .build()); } - sidecarLatch.countDown(); + extProcLatch.countDown(); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1354,7 +1325,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); @@ -1422,11 +1393,9 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) {} - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1504,7 +1473,7 @@ public void requestAttributes_onHeaders() .addRequestAttributes("request.host") .build(); - final CountDownLatch sidecarLatch = new CountDownLatch(2); + final CountDownLatch extProcLatch = new CountDownLatch(2); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1526,14 +1495,12 @@ public void onNext(ProcessingRequest request) { .setRequestBody(BodyResponse.newBuilder().build()) .build()); } - sidecarLatch.countDown(); + extProcLatch.countDown(); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1577,7 +1544,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(2); // First request should be RequestHeaders and should have attributes @@ -1618,7 +1585,7 @@ public void requestAttributes_onBody() .addRequestAttributes("request.host") .build(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1636,14 +1603,12 @@ public void onNext(ProcessingRequest request) { .setRequestBody(BodyResponse.newBuilder().build()) .build()); } - sidecarLatch.countDown(); + extProcLatch.countDown(); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1687,7 +1652,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); // First request should be RequestBody and should have attributes @@ -1728,7 +1693,7 @@ public void requestAttributes_notSent() .addRequestAttributes("request.host") .build(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1746,14 +1711,12 @@ public void onNext(ProcessingRequest request) { .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } - sidecarLatch.countDown(); + extProcLatch.countDown(); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1797,7 +1760,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); // First request should be ResponseHeaders, and should NOT have attributes @@ -1852,12 +1815,10 @@ public void onNext(ProcessingRequest request) { requestSentLatch.countDown(); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -1981,12 +1942,10 @@ public void onNext(ProcessingRequest request) { }).start(); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -2114,12 +2073,10 @@ public void onNext(ProcessingRequest request) { }).start(); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2216,7 +2173,7 @@ public void givenRequestHeaderModeSkip_whenStartCalled_thenCallIsActivated() thr ExternalProcessorFilterConfig filterConfig = configOrError.config; // External Processor Server - final AtomicInteger sidecarMessages = new AtomicInteger(0); + final AtomicInteger extProcMessages = new AtomicInteger(0); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override @@ -2227,15 +2184,13 @@ public StreamObserver process( return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - sidecarMessages.incrementAndGet(); + extProcMessages.incrementAndGet(); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2289,8 +2244,8 @@ public void onCompleted() { // Verify main call started immediately assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // Verify sidecar RECEIVED message about headers because default is SEND - assertThat(sidecarMessages.get()).isEqualTo(1); + // Verify ext_proc server RECEIVED message about headers because default is SEND + assertThat(extProcMessages.get()).isEqualTo(1); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -2346,11 +2301,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2491,12 +2444,10 @@ public void onNext(ProcessingRequest request) { }).start(); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -2624,12 +2575,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2756,12 +2705,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2861,7 +2808,7 @@ public void givenExtProcSignaledEndOfStream_whenClientSendsMoreMessages_thenMess ExternalProcessorFilterConfig filterConfig = configOrError.config; // External Processor Server - final AtomicInteger sidecarMessages = new AtomicInteger(0); + final AtomicInteger extProcMessages = new AtomicInteger(0); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override @@ -2877,7 +2824,7 @@ public void onNext(ProcessingRequest request) { .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasRequestBody()) { - sidecarMessages.incrementAndGet(); + extProcMessages.incrementAndGet(); boolean triggerEos = request.getRequestBody().getBody().toStringUtf8().equals("Trigger EOS"); BodyResponse.Builder bodyResponse = BodyResponse.newBuilder(); @@ -2906,12 +2853,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -2968,7 +2913,7 @@ public void onCompleted() { proxyCall.sendMessage("Too late"); assertThat(dataPlaneMessages.get()).isEqualTo(1); - // Verify sidecar received Trigger EOS and half-close + // Verify ext_proc server received Trigger EOS and half-close proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -3017,12 +2962,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3081,7 +3024,6 @@ public void onCompleted() { channelManager.close(); } - // --- Category 8: Response Header Mutation --- @Test @@ -3149,12 +3091,10 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(response.build()); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3285,12 +3225,10 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(response.build()); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3348,7 +3286,6 @@ public ServerCall.Listener interceptCall( headersLatch.countDown(); } - @Override public void onClose(Status status, Metadata trailers) { appCloseLatch.countDown(); } @@ -3377,7 +3314,6 @@ public ServerCall.Listener interceptCall( channelManager.close(); } - // --- Category 9: Body Mutation: Inbound/Response (GRPC Mode) --- @Test @@ -3411,7 +3347,7 @@ public void givenResponseBodyModeGrpc_whenOnMessageCalled_thenMessageSentToExtPr ExternalProcessorFilterConfig filterConfig = configOrError.config; // External Processor Server - final CountDownLatch sidecarBodyLatch = new CountDownLatch(1); + final CountDownLatch extProcBodyLatch = new CountDownLatch(1); final AtomicReference capturedRequest = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -3434,7 +3370,7 @@ public void onNext(ProcessingRequest request) { } else if (request.hasResponseBody()) { if (capturedRequest.get() == null && !request.getResponseBody().getBody().isEmpty()) { capturedRequest.set(request); - sidecarBodyLatch.countDown(); + extProcBodyLatch.countDown(); } responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder() @@ -3454,12 +3390,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3511,7 +3445,6 @@ public void onMessage(String message) { appMessageLatch.countDown(); } - @Override public void onClose(Status status, Metadata trailers) { appCloseLatch.countDown(); @@ -3524,7 +3457,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.halfClose(); long startTime = System.currentTimeMillis(); - while (sidecarBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { + while (extProcBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); Thread.sleep(10); } @@ -3570,7 +3503,7 @@ public void givenResponseBodyModeGrpc_whenExtProcRespondsWithMutatedBody_thenMut // External Processor Server MutableHandlerRegistry extProcRegistry = new MutableHandlerRegistry(); - final CountDownLatch sidecarBodyLatch = new CountDownLatch(1); + final CountDownLatch extProcBodyLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override @@ -3601,7 +3534,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - sidecarBodyLatch.countDown(); + extProcBodyLatch.countDown(); } else if (request.hasResponseTrailers()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseTrailers(TrailersResponse.newBuilder().build()) @@ -3609,12 +3542,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -3672,7 +3603,6 @@ public void onMessage(String message) { appMessageLatch.countDown(); } - @Override public void onClose(Status status, Metadata trailers) { appCloseLatch.countDown(); @@ -3685,7 +3615,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.halfClose(); long startTime = System.currentTimeMillis(); - while (sidecarBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { + while (extProcBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); Thread.sleep(10); } @@ -3703,7 +3633,6 @@ public void onClose(Status status, Metadata trailers) { channelManager.close(); } - // --- Category 10: Response Trailers --- @Test @@ -3713,7 +3642,7 @@ public void onClose(Status status, Metadata trailers) { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final AtomicReference capturedRequest = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -3732,7 +3661,6 @@ private void completeResponse() { } } - @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { @@ -3749,7 +3677,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); completeResponse(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() @@ -3762,11 +3690,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { completeResponse(); @@ -3856,7 +3782,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Verify status was propagated correctly @@ -3881,7 +3807,7 @@ public void givenResponseTrailerModeSend_whenCallCloses_thenResponseTrailersSent String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final AtomicReference capturedRequest = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -3900,7 +3826,6 @@ private void completeResponse() { } } - @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { @@ -3908,7 +3833,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseTrailers(TrailersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); completeResponse(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() @@ -3921,12 +3846,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { completeResponse(); @@ -4008,7 +3931,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequest.get().hasResponseTrailers()).isTrue(); assertThat(capturedRequest.get().getResponseTrailers().getTrailers().getHeadersList()) @@ -4023,9 +3946,9 @@ public void givenResponseTrailerModeDefault_whenCallCloses_thenResponseTrailersN String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final AtomicInteger sidecarTrailerCount = new AtomicInteger(0); - final CountDownLatch sidecarLatch = new CountDownLatch(1); - final CountDownLatch sidecarHeadersLatch = new CountDownLatch(1); + final AtomicInteger extProcTrailerCount = new AtomicInteger(0); + final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch extProcHeadersLatch = new CountDownLatch(1); final CountDownLatch extProcCompletedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -4044,30 +3967,27 @@ private void completeResponse() { } } - @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { - sidecarTrailerCount.incrementAndGet(); + extProcTrailerCount.incrementAndGet(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarHeadersLatch.countDown(); + extProcHeadersLatch.countDown(); } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { completeResponse(); @@ -4158,12 +4078,12 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(sidecarHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Wait for the ext_proc stream to complete assertThat(extProcCompletedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(sidecarTrailerCount.get()).isEqualTo(0); + assertThat(extProcTrailerCount.get()).isEqualTo(0); // Verify status was propagated correctly assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INVALID_ARGUMENT); @@ -4184,9 +4104,9 @@ public void givenResponseTrailerModeSkip_whenCallCloses_thenResponseTrailersNotS String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final AtomicInteger sidecarTrailerCount = new AtomicInteger(0); - final CountDownLatch sidecarLatch = new CountDownLatch(1); - final CountDownLatch sidecarHeadersLatch = new CountDownLatch(1); + final AtomicInteger extProcTrailerCount = new AtomicInteger(0); + final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch extProcHeadersLatch = new CountDownLatch(1); final CountDownLatch extProcCompletedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -4205,30 +4125,27 @@ private void completeResponse() { } } - @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { - sidecarTrailerCount.incrementAndGet(); + extProcTrailerCount.incrementAndGet(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarHeadersLatch.countDown(); + extProcHeadersLatch.countDown(); } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { completeResponse(); @@ -4319,12 +4236,12 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(sidecarHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Wait for the ext_proc stream to complete assertThat(extProcCompletedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(sidecarTrailerCount.get()).isEqualTo(0); + assertThat(extProcTrailerCount.get()).isEqualTo(0); // Verify status was propagated correctly assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INVALID_ARGUMENT); @@ -4348,7 +4265,7 @@ public void onClose(Status status, Metadata trailers) { String myExtProcServerName = InProcessServerBuilder.generateName(); final AtomicReference capturedResponseHeadersRequest = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); class MyExtProcImpl extends io.envoyproxy.envoy.service.ext_proc.v3.ExternalProcessorGrpc .ExternalProcessorImplBase { @@ -4377,7 +4294,7 @@ public void onNext( .build()); } else if (request.hasResponseHeaders()) { capturedResponseHeadersRequest.set(request); - // Sidecar mutates the trailers-only headers (which are the trailers) + // Ext_proc server mutates the trailers-only headers (which are the trailers) responseObserver.onNext( io.envoyproxy.envoy.service.ext_proc.v3.ProcessingResponse.newBuilder() .setResponseHeaders( @@ -4402,17 +4319,15 @@ public void onNext( .build()) .build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); responseObserver.onCompleted(); } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -4482,7 +4397,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); ProcessingRequest req = capturedResponseHeadersRequest.get(); @@ -4505,7 +4420,7 @@ public void onClose(Status status, Metadata trailers) { String myExtProcServerName = InProcessServerBuilder.generateName(); final AtomicReference capturedResponseHeadersRequest = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); class MyExtProcImpl extends io.envoyproxy.envoy.service.ext_proc.v3.ExternalProcessorGrpc .ExternalProcessorImplBase { @@ -4534,7 +4449,7 @@ public void onNext( .build()); } else if (request.hasResponseHeaders()) { capturedResponseHeadersRequest.set(request); - // Sidecar mutates the trailers-only headers (which are the trailers) + // Ext_proc server mutates the trailers-only headers (which are the trailers) responseObserver.onNext( io.envoyproxy.envoy.service.ext_proc.v3.ProcessingResponse.newBuilder() .setResponseHeaders( @@ -4559,17 +4474,15 @@ public void onNext( .build()) .build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); responseObserver.onCompleted(); } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -4639,7 +4552,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); ProcessingRequest req = capturedResponseHeadersRequest.get(); @@ -4662,8 +4575,8 @@ public void onClose(Status status, Metadata trailers) { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final AtomicInteger sidecarTrailerCount = new AtomicInteger(0); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final AtomicInteger extProcTrailerCount = new AtomicInteger(0); + final CountDownLatch extProcLatch = new CountDownLatch(1); final CountDownLatch extProcCompletedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -4676,22 +4589,20 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { - sidecarTrailerCount.incrementAndGet(); + extProcTrailerCount.incrementAndGet(); } else if (request.hasResponseHeaders()) { - sidecarTrailerCount.incrementAndGet(); + extProcTrailerCount.incrementAndGet(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -4769,11 +4680,11 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Wait for the ext_proc stream to complete assertThat(extProcCompletedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(sidecarTrailerCount.get()).isEqualTo(0); + assertThat(extProcTrailerCount.get()).isEqualTo(0); // Verify status was propagated correctly assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INVALID_ARGUMENT); @@ -4788,8 +4699,6 @@ public void onClose(Status status, Metadata trailers) { channelManager.close(); } - - // --- Category 12: Half-Close handling --- @Test @@ -4834,12 +4743,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -4863,7 +4770,7 @@ public void onCompleted() { dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( (request, responseObserver) -> { - // Should only be called AFTER sidecar response + // Should only be called AFTER ext_proc server response dataPlaneHalfCloseLatch.countDown(); responseObserver.onNext("Hello"); responseObserver.onCompleted(); @@ -4880,7 +4787,7 @@ public void onCompleted() { proxyCall.halfClose(); - // Verify sidecar received end_of_stream_without_message + // Verify ext_proc server received end_of_stream_without_message assertThat(halfCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Verify main call NOT yet started (data plane server NOT yet reached) @@ -4966,12 +4873,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5009,12 +4914,10 @@ public void onNext(String value) { serverReceivedMessages.add(value); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -5127,12 +5030,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5170,12 +5071,10 @@ public void onNext(String value) { serverReceivedMessages.add(value); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -5300,12 +5199,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5343,12 +5240,10 @@ public void onNext(String value) { serverReceivedMessages.add(value); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -5479,12 +5374,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5522,12 +5415,10 @@ public void onNext(String value) { serverReceivedMessages.add(value); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -5590,8 +5481,6 @@ public void halfClose() { channelManager.close(); } - - // --- Category 13: Outbound Backpressure (isReady / onReady) --- @Test @@ -5638,12 +5527,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5702,11 +5589,11 @@ public void onReady() { } }, new Metadata()); - // Wait for activation (sidecar needs to respond to headers) + // Wait for activation (ext_proc server needs to respond to headers) assertThat(readyLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isTrue(); - // Sidecar busy + // Ext_proc server busy sidecarReady.set(false); assertThat(proxyCall.isReady()).isFalse(); @@ -5763,12 +5650,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5829,7 +5714,7 @@ public void onReady() { } }, new Metadata()); - // Wait for activation (sidecar needs to respond to headers) + // Wait for activation (ext_proc server needs to respond to headers) assertThat(readyLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isTrue(); @@ -5887,12 +5772,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -5953,9 +5836,9 @@ public void onReady() { } }, new Metadata()); - // Wait for activation (sidecar needs to respond to headers) + // Wait for activation (ext_proc server needs to respond to headers) assertThat(readyLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // Since sidecar is ready, proxyCall.isReady() should return true, + // Since ext_proc server is ready, proxyCall.isReady() should return true, // ignoring that upstream is busy assertThat(proxyCall.isReady()).isTrue(); @@ -5998,12 +5881,10 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -6015,7 +5896,7 @@ public void onCompleted() { .directExecutor() .build().start()); - final AtomicReference> sidecarListenerRef = + final AtomicReference> extProcListenerRef = new AtomicReference<>(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( @@ -6029,7 +5910,7 @@ public ClientCall interceptCall( next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - sidecarListenerRef.set((Listener) responseListener); + extProcListenerRef.set((Listener) responseListener); super.start(responseListener, headers); } }; @@ -6064,15 +5945,15 @@ public void onReady() { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(appListener, new Metadata()); - // Wait for sidecar call to start and listener to be captured + // Wait for ext_proc server call to start and listener to be captured long startTime = System.currentTimeMillis(); - while (sidecarListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { + while (extProcListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { Thread.sleep(10); } - assertThat(sidecarListenerRef.get()).isNotNull(); + assertThat(extProcListenerRef.get()).isNotNull(); - // Trigger sidecar onReady - sidecarListenerRef.get().onReady(); + // Trigger ext_proc server onReady + extProcListenerRef.get().onReady(); // Verify app listener notified assertThat(onReadyLatch.await(5, TimeUnit.SECONDS)).isTrue(); @@ -6117,11 +5998,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -6187,7 +6066,7 @@ public boolean isReady() { proxyCall.start(new ClientCall.Listener() {}, new Metadata()); proxyCall.request(1); - // 3. Wait for the sidecar response to complete the external processor stream + // 3. Wait for the ext_proc server response to complete the external processor stream assertThat(sidecarResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // 4. Assert that proxyCall.isReady() delegates directly to the downstream call @@ -6772,12 +6651,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { // Don't complete responseObserver immediately to allow test to check draining state @@ -6889,12 +6766,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -6959,14 +6834,14 @@ public void onReady() { fakeClock.forwardTime(1, TimeUnit.SECONDS); } - // Wait for sidecar to send drain and test to observe it + // Wait for ext_proc server to send drain and test to observe it assertThat(sidecarOnNextLatch.await(5, TimeUnit.SECONDS)).isTrue(); for (int i = 0; i < 10; i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); } assertThat(proxyCall.isReady()).isFalse(); - // Now let sidecar complete + // Now let ext_proc server complete sidecarFinishLatch.countDown(); for (int i = 0; i < 10; i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); @@ -6982,7 +6857,7 @@ public void onReady() { fakeClock.forwardTime(1, TimeUnit.SECONDS); } - // After sidecar stream completes, it should trigger onReady and become ready + // After ext_proc server stream completes, it should trigger onReady and become ready assertThat(onReadyLatch.await(5, TimeUnit.SECONDS)).isTrue(); for (int i = 0; i < 50 && !proxyCall.isReady(); i++) { fakeClock.forwardTime(100, TimeUnit.MILLISECONDS); @@ -7039,11 +6914,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() {} }; @@ -7157,12 +7030,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { // Already handled in the background thread @@ -7229,10 +7100,10 @@ public void onMessage(String message) { } assertThat(proxyCall.isReady()).isFalse(); - // Request messages from server while stream is draining (and sidecar not ready) + // Request messages from server while stream is draining (and ext_proc server not ready) proxyCall.request(1); - // Now let sidecar complete + // Now let ext_proc server complete sidecarFinishLatch.countDown(); // Wait for it to become ready again @@ -7242,7 +7113,7 @@ public void onMessage(String message) { } assertThat(proxyCall.isReady()).isTrue(); - // 1. Verify application message is forwarded to data plane WITHOUT sidecar contact + // 1. Verify application message is forwarded to data plane WITHOUT ext_proc server contact proxyCall.sendMessage("Direct Message"); proxyCall.halfClose(); @@ -7252,7 +7123,7 @@ public void onMessage(String message) { assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(dataPlaneReceivedMessage.get()).isEqualTo("Direct Message"); - // 2. Verify server response is delivered to application WITHOUT sidecar call + // 2. Verify server response is delivered to application WITHOUT ext_proc server call assertThat(appLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appReceivedMessage.get()).isEqualTo("Direct Response"); @@ -7321,12 +7192,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { drainCompletedLatch.countDown(); @@ -7361,12 +7230,10 @@ public void onNext(String value) { dataPlaneReceivedMessages.add(value); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onNext("Direct Response"); @@ -7413,7 +7280,7 @@ public void onMessage(String message) { // Assert that it was NOT received by extProc assertThat(extProcReceivedBodyCount.get()).isEqualTo(0); - // Now let sidecar complete + // Now let ext_proc server complete sidecarFinishLatch.countDown(); // Request response from data plane @@ -7506,12 +7373,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { drainCompletedLatch.countDown(); @@ -7549,12 +7414,10 @@ public void onNext(String value) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onNext("Direct Response"); @@ -7608,7 +7471,7 @@ public void onMessage(String message) { // Verify the message during drain has NOT been delivered to the data plane server yet assertThat(dataPlaneReceivedMessages).containsExactly("Mutated Message 1"); - // Now let sidecar complete + // Now let ext_proc server complete sidecarFinishLatch.countDown(); // Wait for the control stream drain to be fully completed @@ -7688,12 +7551,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { drainCompletedLatch.countDown(); @@ -7729,12 +7590,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { public void onNext(String value) { } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -7757,13 +7616,11 @@ public void onHeaders(Metadata headers) { appReceivedHeaders.set(headers); } - @Override public void onMessage(String message) { appReceivedMessages.add(message); } - @Override public void onClose(Status status, Metadata trailers) { appReceivedStatus.set(status); @@ -7780,7 +7637,7 @@ public void onClose(Status status, Metadata trailers) { // Request messages from server proxyCall.request(10); - // Wait for drain to be processed and sidecar's client stream to finish + // Wait for drain to be processed and ext_proc server's client stream to finish assertThat(drainCompletedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isFalse(); @@ -7799,7 +7656,7 @@ public void onClose(Status status, Metadata trailers) { assertThat(appReceivedMessages).isEmpty(); assertThat(appReceivedStatus.get()).isNull(); - // Now let sidecar complete the drain + // Now let ext_proc server complete the drain sidecarFinishLatch.countDown(); // Wait for the call to close on application side @@ -7875,7 +7732,7 @@ public void onNext(ProcessingRequest request) { if ("Original Message 1".equals(msgStr)) { new Thread(() -> { try { - // Wait until M2 is received by sidecar so both M1 and M2 are in flight + // Wait until M2 is received by ext_proc server so both M1 and M2 are in flight if (m2ReceivedLatch.await(5, TimeUnit.SECONDS)) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder() @@ -7925,11 +7782,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { drainCompletedLatch.countDown(); @@ -7965,11 +7820,9 @@ public StreamObserver invoke(StreamObserver responseObserver) { @Override public void onNext(String value) {} - @Override public void onError(Throwable t) {} - @Override public void onCompleted() {} }; @@ -7994,7 +7847,6 @@ public void onHeaders(Metadata headers) { appReceivedHeaders.set(headers); } - @Override public void onMessage(String message) { appReceivedMessages.add(message); @@ -8005,7 +7857,6 @@ public void onMessage(String message) { } } - @Override public void onClose(Status status, Metadata trailers) { appReceivedStatus.set(status); @@ -8027,13 +7878,13 @@ public void onClose(Status status, Metadata trailers) { StreamObserver dataPlaneResponseObserver = dataPlaneResponseObserverRef.get(); assertThat(dataPlaneResponseObserver).isNotNull(); - // 1. Upstream sends M1 (which triggers response headers and M1 body to sidecar) + // 1. Upstream sends M1 (which triggers response headers and M1 body to ext_proc server) dataPlaneResponseObserver.onNext("Original Message 1"); - // Wait for sidecar to receive and respond to response headers + // Wait for ext_proc server to receive and respond to response headers assertThat(respHeadersLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // 2. Upstream sends M2 (which triggers M2 body to sidecar) + // 2. Upstream sends M2 (which triggers M2 body to ext_proc server) dataPlaneResponseObserver.onNext("Original Message 2"); // Wait for app to receive Mutated Message 1 (meaning M1's response with request_drain=true @@ -8052,10 +7903,10 @@ public void onClose(Status status, Metadata trailers) { assertThat(appReceivedMessages).containsExactly("Mutated Message 1"); assertThat(appReceivedStatus.get()).isNull(); - // 4. Signal sidecar to send Mutated Message 2 + // 4. Signal ext_proc server to send Mutated Message 2 m3SentLatch.countDown(); - // Wait for sidecar to finish sending M2 and app to receive it + // Wait for ext_proc server to finish sending M2 and app to receive it assertThat(respBody2Latch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(mutatedMsg2ReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue(); @@ -8063,7 +7914,7 @@ public void onClose(Status status, Metadata trailers) { // M3 is released) assertThat(appReceivedMessages).containsExactly("Mutated Message 1", "Mutated Message 2"); - // 5. Complete sidecar stream to finish the drain + // 5. Complete ext_proc server stream to finish the drain sidecarFinishLatch.countDown(); // Wait for the call to close on application side @@ -8119,12 +7970,10 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -8137,7 +7986,7 @@ public void onCompleted() { .build().start()); final AtomicBoolean sidecarReady = new AtomicBoolean(true); - final AtomicReference> sidecarListenerRef = + final AtomicReference> extProcListenerRef = new AtomicReference<>(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( @@ -8151,11 +8000,10 @@ public ClientCall interceptCall( ReqT, RespT>(next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - sidecarListenerRef.set((Listener) responseListener); + extProcListenerRef.set((Listener) responseListener); super.start(responseListener, headers); } - @Override public boolean isReady() { return sidecarReady.get(); @@ -8180,12 +8028,10 @@ public StreamObserver invoke(StreamObserver responseObserver) { public void onNext(String value) { } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -8219,27 +8065,27 @@ public void request(int numMessages) { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - // Wait for sidecar call to start + // Wait for ext_proc server call to start long startTime = System.currentTimeMillis(); - while (sidecarListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { + while (extProcListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { Thread.sleep(10); } - assertThat(sidecarListenerRef.get()).isNotNull(); + assertThat(extProcListenerRef.get()).isNotNull(); - // Sidecar is busy + // Ext_proc server is busy sidecarReady.set(false); assertThat(proxyCall.isReady()).isFalse(); proxyCall.request(5); - // Verify data plane call NOT requested yet (due to observability mode and sidecar busy) + // Verify data plane call NOT requested yet (due to observability mode and ext_proc server busy) assertThat(dataPlaneRequestCount.get()).isEqualTo(0); - // Sidecar becomes ready + // Ext_proc server becomes ready sidecarReady.set(true); - sidecarListenerRef.get().onReady(); + extProcListenerRef.get().onReady(); - // After sidecar becomes ready, pending requests should be drained to data plane. + // After ext_proc server becomes ready, pending requests should be drained to data plane. assertThat(dataPlaneRequestCount.get()).isEqualTo(5); assertThat(proxyCall.isReady()).isTrue(); @@ -8290,12 +8136,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -8407,12 +8251,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -8425,7 +8267,7 @@ public void onCompleted() { .build().start()); final AtomicBoolean sidecarReady = new AtomicBoolean(true); - final AtomicReference> sidecarListenerRef = + final AtomicReference> extProcListenerRef = new AtomicReference<>(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( @@ -8439,11 +8281,10 @@ public ClientCall interceptCall( ReqT, RespT>(next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - sidecarListenerRef.set((Listener) responseListener); + extProcListenerRef.set((Listener) responseListener); super.start(responseListener, headers); } - @Override public boolean isReady() { return sidecarReady.get(); @@ -8490,23 +8331,23 @@ public void request(int numMessages) { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - // Wait for sidecar call to start + // Wait for ext_proc server call to start long startTime = System.currentTimeMillis(); - while (sidecarListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { + while (extProcListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { Thread.sleep(10); } - assertThat(sidecarListenerRef.get()).isNotNull(); + assertThat(extProcListenerRef.get()).isNotNull(); - // Sidecar is busy initially + // Ext_proc server is busy initially sidecarReady.set(false); // Request from application proxyCall.request(10); assertThat(dataPlaneRequestCount.get()).isEqualTo(0); - // Sidecar becomes ready + // Ext_proc server becomes ready sidecarReady.set(true); - sidecarListenerRef.get().onReady(); + extProcListenerRef.get().onReady(); // Verify buffered request drained assertThat(dataPlaneRequestCount.get()).isEqualTo(10); @@ -8554,12 +8395,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -8618,7 +8457,7 @@ public void onReady() { } }, new Metadata()); - // Wait for sidecar stream completion + // Wait for ext_proc server stream completion assertThat(readyLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isTrue(); @@ -8626,7 +8465,7 @@ public void onReady() { // Verify request forwarded immediately assertThat(dataPlaneRequestCount.get()).isEqualTo(7); - // proxyCall.isReady() should remain true as sidecar is gone + // proxyCall.isReady() should remain true as ext_proc server is gone assertThat(proxyCall.isReady()).isTrue(); proxyCall.cancel("Cleanup", null); @@ -8700,12 +8539,10 @@ public void onNext(ProcessingRequest request) { }); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { extProcCompletedLatch.countDown(); @@ -8773,13 +8610,11 @@ public void onHeaders(Metadata headers) { appEvents.add("HEADERS"); } - @Override public void onMessage(String message) { appEvents.add("MESSAGE"); } - @Override public void onClose(Status status, Metadata trailers) { appEvents.add("CLOSE:" + status.getCode()); @@ -8801,7 +8636,6 @@ public void onClose(Status status, Metadata trailers) { channelManager.close(); } - @Test @SuppressWarnings("FutureReturnValueIgnored") public void @@ -8879,11 +8713,9 @@ public void onNext(ProcessingRequest request) { }); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { extProcCompletedLatch.countDown(); @@ -8926,7 +8758,6 @@ public void onMessage(String message) { call.sendMessage("server-response-" + message); } - @Override public void onHalfClose() { call.close(Status.OK, new Metadata()); @@ -8958,13 +8789,11 @@ public void onHeaders(Metadata headers) { appEvents.add("HEADERS"); } - @Override public void onMessage(String message) { appEvents.add("MESSAGE:" + message); } - @Override public void onClose(Status status, Metadata trailers) { appEvents.add("CLOSE:" + status.getCode()); @@ -9037,17 +8866,15 @@ public void onNext(ProcessingRequest request) { // Fail the stream immediately on headers responseObserver.onError( Status.INTERNAL - .withDescription("Simulated sidecar failure") + .withDescription("Simulated ext_proc server failure") .asRuntimeException()); } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -9085,7 +8912,7 @@ public void onClose(Status status, Metadata trailers) { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(appListener, new Metadata()); - // Verify application receives INTERNAL due to sidecar failure + // Verify application receives INTERNAL due to ext_proc server failure assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); assertThat(closedStatus.get().getDescription()).contains("External processor stream failed"); @@ -9134,12 +8961,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -9209,7 +9034,7 @@ public void onClose(Status status, Metadata trailers) { // Trigger unary call. request(1) starts it. proxyCall.request(1); - // Wait for the async sidecar thread to enter activateCall() and block inside interceptCall + // Wait for the async ext_proc server thread to enter activateCall() and block inside interceptCall assertThat(headersReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Now, while the async thread is blocked (and passThroughMode is still false), @@ -9268,12 +9093,10 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { sidecarCompletedLatch.countDown(); @@ -9335,7 +9158,7 @@ public void onCompleted() { } assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // At this point, app received onClose, but sidecar should NOT be completed yet + // At this point, app received onClose, but ext_proc server should NOT be completed yet assertThat(sidecarCompletedLatch.getCount()).isEqualTo(1); // Fast forward time to trigger deferred close @@ -9404,7 +9227,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()); } else if (request.hasRequestBody()) { - // Simulate sidecar sending compressed body mutation (unsupported) + // Simulate ext_proc server sending compressed body mutation (unsupported) responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() @@ -9419,12 +9242,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -9491,7 +9312,7 @@ public void onClose(Status status, Metadata trailers) { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(appListener, new Metadata()); - // Wait for sidecar to receive headers and filter to activate call + // Wait for ext_proc server to receive headers and filter to activate call for (int i = 0; i < 5000 && closedLatch.getCount() > 0; i++) { fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); Thread.sleep(1); @@ -9574,7 +9395,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()); } else if (request.hasResponseBody()) { - // Simulate sidecar sending compressed body mutation (unsupported) for response body + // Simulate ext_proc server sending compressed body mutation (unsupported) for response body responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() @@ -9589,12 +9410,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -9719,12 +9538,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -9862,12 +9679,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -9922,7 +9737,7 @@ public void onClose(Status status, Metadata trailers) { assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.UNAUTHENTICATED); assertThat(closedStatus.get().getDescription()).isEqualTo("Custom security rejection"); - // Data plane call should NOT have been started as sidecar rejected immediately on headers + // Data plane call should NOT have been started as ext_proc server rejected immediately on headers assertThat(dataPlaneStarted.get()).isFalse(); proxyCall.cancel("Cleanup", null); @@ -9976,11 +9791,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() {} }; @@ -10090,12 +9903,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -10140,7 +9951,7 @@ public void onCompleted() { fakeClock.forwardTime(1, TimeUnit.SECONDS); Thread.sleep(1); } - // Verify app listener notified with an error (not the sidecar's UNAUTHENTICATED) + // Verify app listener notified with an error (not the ext_proc server's UNAUTHENTICATED) assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); @@ -10217,12 +10028,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -10342,7 +10151,7 @@ public void onNext(ProcessingRequest request) { io.envoyproxy.envoy.service.ext_proc.v3.GrpcStatus.newBuilder() .setStatus(Status.DATA_LOSS.getCode().value()) .build()) - .setDetails("Sidecar detected data loss") + .setDetails("Ext_proc server detected data loss") .setHeaders( io.envoyproxy.envoy.service.ext_proc.v3.HeaderMutation .newBuilder() @@ -10352,7 +10161,7 @@ public void onNext(ProcessingRequest request) { .setHeader( io.envoyproxy.envoy.config.core.v3.HeaderValue .newBuilder() - .setKey("x-sidecar-extra") + .setKey("x-ext-proc-server-extra") .setValue("true") .build()) .build()) @@ -10364,12 +10173,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { } @@ -10425,11 +10232,11 @@ public void onCompleted() { assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.DATA_LOSS); - assertThat(closedStatus.get().getDescription()).isEqualTo("Sidecar detected data loss"); + assertThat(closedStatus.get().getDescription()).isEqualTo("Ext_proc server detected data loss"); assertThat( closedTrailers .get() - .get(Metadata.Key.of("x-sidecar-extra", Metadata.ASCII_STRING_MARSHALLER))) + .get(Metadata.Key.of("x-ext-proc-server-extra", Metadata.ASCII_STRING_MARSHALLER))) .isEqualTo("true"); proxyCall.cancel("Cleanup", null); @@ -10491,13 +10298,11 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { cancelLatch.countDown(); } - @Override public void onCompleted() { } @@ -10543,7 +10348,7 @@ public void onCompleted() { // Application cancels the RPC proxyCall.cancel("User cancelled", null); - // Verify sidecar stream also cancelled + // Verify ext_proc server stream also cancelled assertThat(cancelLatch.await(5, TimeUnit.SECONDS)).isTrue(); channelManager.close(); @@ -10575,7 +10380,7 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenIsReadyReturnsFalse( final List extProcRequests = new CopyOnWriteArrayList<>(); - // Sidecar server + // Ext_proc server server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -10598,12 +10403,10 @@ public void onNext(ProcessingRequest request) { }).start(); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -10681,11 +10484,11 @@ public boolean isReady() { } assertThat(proxyCall.isReady()).isTrue(); - // Sidecar becomes busy -> proxyCall becomes busy + // Ext_proc server becomes busy -> proxyCall becomes busy sidecarReady.set(false); assertThat(proxyCall.isReady()).isFalse(); - // Sidecar becomes ready, but Data Plane is busy -> proxyCall is STILL ready because Normal Mode + // Ext_proc server becomes ready, but Data Plane is busy -> proxyCall is STILL ready because Normal Mode sidecarReady.set(true); dataPlaneReady.set(false); assertThat(proxyCall.isReady()).isTrue(); @@ -10727,7 +10530,7 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenAppRequestsAreBuffer assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - // Sidecar server + // Ext_proc server server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -10765,12 +10568,10 @@ public void onNext(ProcessingRequest request) { }).start(); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -10784,7 +10585,7 @@ public void onCompleted() { .build().start()); final AtomicBoolean sidecarReady = new AtomicBoolean(true); - final AtomicReference> sidecarListenerRef = + final AtomicReference> extProcListenerRef = new AtomicReference<>(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( @@ -10798,11 +10599,10 @@ public ClientCall interceptCall( next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - sidecarListenerRef.set((Listener) responseListener); + extProcListenerRef.set((Listener) responseListener); super.start(responseListener, headers); } - @Override public boolean isReady() { return sidecarReady.get(); @@ -10872,15 +10672,15 @@ public void request(int numMessages) { } assertThat(proxyCall.isReady()).isTrue(); - // Sidecar busy -> request(5) should be buffered + // Ext_proc server busy -> request(5) should be buffered sidecarReady.set(false); proxyCall.request(5); assertThat(dataPlaneRequestCount.get()).isEqualTo(1); // (Only the initial bootstrap request went through) - // Sidecar becomes ready -> buffered requests should start draining (pulling next message) + // Ext_proc server becomes ready -> buffered requests should start draining (pulling next message) sidecarReady.set(true); - sidecarListenerRef.get().onReady(); + extProcListenerRef.get().onReady(); long startTime2 = System.currentTimeMillis(); while (dataPlaneRequestCount.get() < 2 && System.currentTimeMillis() - startTime2 < 5000) { @@ -10963,7 +10763,7 @@ public void givenResponseBodyModeNone_whenExtProcBusy_thenAppRequestsAreNotBuffe assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - // Sidecar server + // Ext_proc server server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -10985,12 +10785,10 @@ public void onNext(ProcessingRequest request) { }).start(); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { new Thread(() -> responseObserver.onCompleted()).start(); @@ -11019,7 +10817,6 @@ public void start(Listener responseListener, Metadata headers) { super.start(responseListener, headers); } - @Override public boolean isReady() { return sidecarReady.get(); @@ -11086,7 +10883,7 @@ public void request(int numMessages) { } assertThat(proxyCall.isReady()).isTrue(); - // Sidecar busy + // Ext_proc server busy sidecarReady.set(false); // Since responseBodyMode is NONE and not in observabilityMode, request(5) should @@ -11162,11 +10959,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11307,11 +11102,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11346,11 +11139,9 @@ public void onNext(String value) { dataPlaneReceivedMessages.add(value); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onNext("Response"); @@ -11501,11 +11292,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11582,7 +11371,7 @@ public void onMessage(String message) { String largeMessage70k = new String(new char[70000]).replace('\0', 'a'); String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); - // Wait for the initialization (headers, request body, response headers) to reach the sidecar + // Wait for the initialization (headers, request body, response headers) to reach the ext_proc server assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Upstream sends 70k response chunk. Since window is 65,536, this drives the window @@ -11597,12 +11386,12 @@ public void onMessage(String message) { // must block/buffer this chunk. upstreamResponseObserver.onNext(largeMessage30k); - // Wait a brief period and verify that the 30k chunk has NOT been sent to the sidecar + // Wait a brief period and verify that the 30k chunk has NOT been sent to the ext_proc server Thread.sleep(200); assertThat(secondResponseBodyLatch.getCount()).isEqualTo(1); assertThat(appReceivedMessages).hasSize(2); - // Sidecar sends a ServerWindowUpdate of 40k to the filter, unblocking the window. + // Ext_proc server sends a ServerWindowUpdate of 40k to the filter, unblocking the window. responseObserverRef.get().onNext(ProcessingResponse.newBuilder() .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder() .setWindowIncrementUpstreamToSidestream(40000) @@ -11610,7 +11399,7 @@ public void onMessage(String message) { .build()); // Once the window is unblocked, the filter immediately forwards the 30k chunk - // to the sidecar, which processes it. + // to the ext_proc server, which processes it. assertThat(secondResponseBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Verify that Chunk 2 is now successfully delivered to the client application @@ -11724,11 +11513,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11883,11 +11670,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11990,7 +11775,7 @@ public void onClose(Status status, Metadata trailers) { @Test @SuppressWarnings("unchecked") - public void testThresholdBasedWindowUpdates() throws Exception { + public void testSidestreamToUpstreamFlowControl_QueuingAndDraining() throws Exception { ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() @@ -12002,12 +11787,185 @@ public void testThresholdBasedWindowUpdates() throws Exception { .build()) .setProcessingMode(ProcessingMode.newBuilder() .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); - ConfigOrError + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final CountDownLatch finishLatch = new CountDownLatch(1); + final AtomicReference serverReceivedBody = new AtomicReference<>(); + final CountDownLatch serverReceivedLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + boolean eos = request.getRequestBody().getEndOfStream() + || request.getRequestBody().getEndOfStreamWithoutMessage(); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8(eos ? "" : "MutatedRequest")) + .setEndOfStream(eos) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + @Override + public void onError(Throwable t) {} + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) { + serverReceivedBody.set(value); + serverReceivedLatch.countDown(); + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } + })) + .build()); + + final AtomicBoolean transportReady = new AtomicBoolean(false); + final AtomicReference> capturedListenerRef = new AtomicReference<>(); + + class TriggerableForwardingCall + extends io.grpc.ForwardingClientCall.SimpleForwardingClientCall { + TriggerableForwardingCall(ClientCall delegate) { + super(delegate); + } + @Override + public void start(Listener responseListener, Metadata headers) { + capturedListenerRef.set(responseListener); + super.start(responseListener, headers); + } + @Override + public boolean isReady() { + return transportReady.get(); + } + } + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new TriggerableForwardingCall<>(next.newCall(method, callOptions)); + } + }) + .directExecutor() + .build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + finishLatch.countDown(); + } + }, new Metadata()); + proxyCall.request(1); + + // Send a message. This gets mutated to "MutatedRequest" by ext_proc. + proxyCall.sendMessage("OriginalRequest"); + + // Give some time to process and ensure the message is NOT received on the server side because transport is not ready + assertThat(serverReceivedLatch.await(500, TimeUnit.MILLISECONDS)).isFalse(); + assertThat(serverReceivedBody.get()).isNull(); + + // Now make the transport ready and trigger onReady callback + transportReady.set(true); + ClientCall.Listener listener = capturedListenerRef.get(); + assertThat(listener).isNotNull(); + listener.onReady(); + + // The message should be drained and forwarded to the backend server + assertThat(serverReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(serverReceivedBody.get()).isEqualTo("MutatedRequest"); + + proxyCall.halfClose(); + assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testThresholdBasedWindowUpdates() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); @@ -12048,11 +12006,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -12224,11 +12180,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -12283,8 +12237,6 @@ public void onCompleted() {} proxyCall.sendMessage("Last Message"); - - // Verify 12 bytes are accumulated but no standalone update is sent. assertThat(filterClientRequests(receivedRequests)).hasSize(2); @@ -12397,11 +12349,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -12458,7 +12408,7 @@ public void onCompleted() {} assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(filterClientRequests(receivedRequests)).hasSize(1); // Headers request - // Send Msg 1 (5 bytes). It is processed by sidecar and mutated to 15 bytes. + // Send Msg 1 (5 bytes). It is processed by ext_proc server and mutated to 15 bytes. proxyCall.sendMessage("Msg 1"); // Wait until Msg 1 request and response are processed @@ -12475,7 +12425,7 @@ public void onCompleted() {} StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); upstreamResponseObserver.onNext("Dummy for headers"); - // Wait until response headers are processed by sidecar + // Wait until response headers are processed by ext_proc server startTime = System.currentTimeMillis(); while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); @@ -12485,7 +12435,7 @@ public void onCompleted() {} String msg20Bytes = new String(new char[20]).replace('\0', 's'); upstreamResponseObserver.onNext(msg20Bytes); - // Wait until response body is processed by sidecar + // Wait until response body is processed by ext_proc server startTime = System.currentTimeMillis(); while (receivedRequests.size() < 4 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); @@ -12607,11 +12557,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -12668,7 +12616,7 @@ public void onCompleted() {} assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(filterClientRequests(receivedRequests)).hasSize(1); // Headers request - // Send Msg 1 (5 bytes) from app. It is processed by sidecar and mutated to 15 bytes. + // Send Msg 1 (5 bytes) from app. It is processed by ext_proc server and mutated to 15 bytes. proxyCall.sendMessage("Msg 1"); // Wait until Msg 1 request and response are processed @@ -12684,7 +12632,7 @@ public void onCompleted() {} StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); upstreamResponseObserver.onNext("Dummy for headers"); - // Wait until response headers are processed by sidecar + // Wait until response headers are processed by ext_proc server startTime = System.currentTimeMillis(); while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); @@ -12694,7 +12642,7 @@ public void onCompleted() {} String msg20Bytes = new String(new char[20]).replace('\0', 's'); upstreamResponseObserver.onNext(msg20Bytes); - // Wait until response body is processed by sidecar + // Wait until response body is processed by ext_proc server startTime = System.currentTimeMillis(); while (receivedRequests.size() < 4 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); @@ -12818,11 +12766,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13000,11 +12946,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13179,11 +13123,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13435,12 +13377,10 @@ public void onNext(ProcessingRequest request) { }); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13469,12 +13409,10 @@ public void onNext(String value) { serverReceivedBody.set(value); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onNext("Ack"); @@ -13552,14 +13490,12 @@ public void onNext(String value) { clientReceivedBody.set(value); } - @Override public void onError(Throwable t) { errorRef.set(t); finishLatch.countDown(); } - @Override public void onCompleted() { finishLatch.countDown(); @@ -13570,7 +13506,7 @@ public void onCompleted() { requestObserver.onCompleted(); if (!sidecarActionLatch.await(10, TimeUnit.SECONDS)) { - throw new AssertionError("Sidecar actions failed. Received: " + receivedPhases); + throw new AssertionError("Ext_proc server actions failed. Received: " + receivedPhases); } assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); if (errorRef.get() != null) { @@ -13744,12 +13680,10 @@ public void onNext(ProcessingRequest request) { }); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13781,12 +13715,10 @@ public void onNext(String value) { responseObserver.onNext(value + "Echo"); } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -13866,14 +13798,12 @@ public void onNext(String value) { clientReceivedBody.set(value); } - @Override public void onError(Throwable t) { errorRef.set(t); finishLatch.countDown(); } - @Override public void onCompleted() { finishLatch.countDown(); @@ -13884,7 +13814,7 @@ public void onCompleted() { bidiRequestObserver.onCompleted(); if (!sidecarBidiLatch.await(10, TimeUnit.SECONDS)) { - throw new AssertionError("Sidecar bidi actions failed. Received: " + receivedPhases); + throw new AssertionError("Ext_proc server bidi actions failed. Received: " + receivedPhases); } assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); if (errorRef.get() != null) { @@ -13921,7 +13851,7 @@ public void onCompleted() { final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -13937,7 +13867,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -13945,12 +13875,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14031,7 +13959,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); List headerNames = new ArrayList<>(); @@ -14054,7 +13982,7 @@ public void onClose(Status status, Metadata trailers) { final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -14074,16 +14002,14 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14177,7 +14103,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); List headerNames = new ArrayList<>(); @@ -14199,7 +14125,7 @@ public void givenDisallowedHeaders_whenHeadersForwarded_thenSkipped() throws Exc final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -14215,7 +14141,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -14223,12 +14149,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14301,7 +14225,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); List headerNames = new ArrayList<>(); @@ -14323,7 +14247,7 @@ public void givenBothRules_whenHeadersForwarded_thenBothAreApplied() throws Exce final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -14339,7 +14263,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -14347,12 +14271,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14428,7 +14350,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); List headerNames = new ArrayList<>(); @@ -14484,7 +14406,7 @@ public void givenRequestAttributes_whenHeaderPhase_thenAttributesSent() throws E .build(); final AtomicReference capturedRequest = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final CountDownLatch callLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -14501,7 +14423,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -14509,12 +14431,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14565,7 +14485,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(5, TimeUnit.SECONDS)).isTrue(); ProcessingRequest request = capturedRequest.get(); @@ -14593,7 +14513,7 @@ public void givenMetadataAttributes_whenHeadersPresent_thenAttributesSent() thro .build(); final AtomicReference capturedRequest = new AtomicReference<>(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final CountDownLatch callLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -14610,7 +14530,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -14618,12 +14538,10 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14683,7 +14601,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(5, TimeUnit.SECONDS)).isTrue(); ProcessingRequest request = capturedRequest.get(); @@ -14702,8 +14620,6 @@ public void onClose(Status status, Metadata trailers) { channelManager.close(); } - - // --- Category 24: Response Ordering Checks --- @Test @@ -14712,7 +14628,7 @@ public void givenOutOfOrderReqResponses_whenMessageArrivesBeforeHeaders_thenFail String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -14737,18 +14653,16 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); responseObserver.onCompleted(); // Complete stream to allow cleanup } } - @Override public void onError(Throwable t) { extProcError.set(t); } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14806,7 +14720,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // The call should fail with INTERNAL status @@ -14822,7 +14736,7 @@ public void givenUnexpectedResponseHeaders_whenHeadersArriveBeforeServerHeaders_ throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -14839,18 +14753,16 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); responseObserver.onCompleted(); } } - @Override public void onError(Throwable t) { extProcError.set(t); } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -14916,7 +14828,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // The call should fail with INTERNAL status due to protocol error @@ -14936,7 +14848,7 @@ public void givenUnexpectedResponseTrailers_whenTrailersArriveBeforeServerTraile throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -14953,18 +14865,16 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseTrailers(TrailersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); responseObserver.onCompleted(); } } - @Override public void onError(Throwable t) { extProcError.set(t); } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -15030,7 +14940,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // The call should fail with INTERNAL status due to protocol error @@ -15050,7 +14960,7 @@ public void givenOutOfOrderRespResponses_whenResponseBodyArrivesBeforeResponseHe throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -15072,18 +14982,16 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); responseObserver.onCompleted(); } } - @Override public void onError(Throwable t) { extProcError.set(t); } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -15152,7 +15060,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // The call should fail with INTERNAL status due to protocol error @@ -15171,7 +15079,7 @@ public void onClose(Status status, Metadata trailers) { public void givenValidOrder_whenResponsesArriveInOrder_thenSucceeds() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -15186,16 +15094,14 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -15263,7 +15169,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.halfClose(); // Verify that headers are processed correctly and the ordering check passes - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Verify that the call completes successfully assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); @@ -15339,11 +15245,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { observerRef.get().onCompleted(); @@ -15369,11 +15273,9 @@ public StreamObserver invoke(StreamObserver responseObserver) { @Override public void onNext(String value) {} - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -15426,11 +15328,9 @@ public void onCompleted() { @Override public void onNext(String value) {} - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { allDoneLatch.countDown(); @@ -15473,8 +15373,8 @@ public void givenRequestHeadersResponse_whenStatusIsContinueAndReplace_thenFails throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); - final CountDownLatch sidecarFinishedLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch extProcFinishedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -15493,21 +15393,19 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); responseObserver.onCompleted(); } } - @Override public void onError(Throwable t) { - sidecarFinishedLatch.countDown(); + extProcFinishedLatch.countDown(); } - @Override public void onCompleted() { - sidecarFinishedLatch.countDown(); + extProcFinishedLatch.countDown(); responseObserver.onCompleted(); } }; @@ -15566,8 +15464,8 @@ public void onCompleted() { // ignore } - assertThat(sidecarLatch.await(30, TimeUnit.SECONDS)).isTrue(); - assertThat(sidecarFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(30, TimeUnit.SECONDS)).isTrue(); // Call should succeed due to fail-open @@ -15581,8 +15479,8 @@ public void givenResponseHeadersResponse_whenStatusIsContinueAndReplace_thenFail throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch sidecarLatch = new CountDownLatch(1); - final CountDownLatch sidecarFinishedLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch extProcFinishedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -15605,21 +15503,19 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); responseObserver.onCompleted(); } } - @Override public void onError(Throwable t) { - sidecarFinishedLatch.countDown(); + extProcFinishedLatch.countDown(); } - @Override public void onCompleted() { - sidecarFinishedLatch.countDown(); + extProcFinishedLatch.countDown(); responseObserver.onCompleted(); } }; @@ -15681,8 +15577,8 @@ public void onCompleted() { // ignore } - assertThat(sidecarLatch.await(30, TimeUnit.SECONDS)).isTrue(); - assertThat(sidecarFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(30, TimeUnit.SECONDS)).isTrue(); // The call should succeed due to fail-open @@ -15694,8 +15590,8 @@ public void onCompleted() { @Test public void givenExtProcCall_whenExecutionSucceeds_thenAll4MetricsAreRecorded() throws Exception { final String uniqueExtProcServerName = "ext-proc-server-metrics-" + java.util.UUID.randomUUID(); - final CountDownLatch sidecarRequestHeadersLatch = new CountDownLatch(1); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcRequestHeadersLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); // In-process mock server for External Processor ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -15712,21 +15608,19 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarRequestHeadersLatch.countDown(); + extProcRequestHeadersLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -15806,13 +15700,13 @@ public void onCompleted() { proxyCall.halfClose(); // 1. Wait for mock Ext Proc to receive and process client request headers - assertThat(sidecarRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); // 2. Release the data plane server to respond back to the client call dataPlaneLatch.countDown(); // 3. Assert that all stages complete in sequence deterministically - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Clean up and close the Ext Proc stream to release in-process server/channel resources cleanly @@ -15851,8 +15745,8 @@ public void onCompleted() { public void givenExtProcCall_whenExecutionFails_thenAll4MetricsAreRecorded() throws Exception { final String uniqueExtProcServerName = "ext-proc-server-metrics-fail-" + java.util.UUID.randomUUID(); - final CountDownLatch sidecarRequestHeadersLatch = new CountDownLatch(1); - final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch extProcRequestHeadersLatch = new CountDownLatch(1); + final CountDownLatch extProcLatch = new CountDownLatch(1); // In-process mock server for External Processor ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -15869,21 +15763,19 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarRequestHeadersLatch.countDown(); + extProcRequestHeadersLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - sidecarLatch.countDown(); + extProcLatch.countDown(); } } - @Override public void onError(Throwable t) { } - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -15976,13 +15868,13 @@ public ServerCall.Listener interceptCall( proxyCall.halfClose(); // 1. Wait for mock Ext Proc to receive and process client request headers - assertThat(sidecarRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); // 2. Release the data plane server to respond back with error dataPlaneLatch.countDown(); // 3. Assert that all stages complete - assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.UNAUTHENTICATED); @@ -16051,11 +15943,9 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) {} - @Override public void onError(Throwable t) {} - @Override public void onCompleted() {} }; @@ -16157,11 +16047,9 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) {} - @Override public void onError(Throwable t) {} - @Override public void onCompleted() {} }; @@ -16270,11 +16158,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() {} }; @@ -16402,11 +16288,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() {} }; @@ -16506,16 +16390,14 @@ public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { // Fail the stream immediately on receiving headers responseObserver.onError( - Status.INTERNAL.withDescription("Simulated sidecar failure") + Status.INTERNAL.withDescription("Simulated ext_proc server failure") .asRuntimeException()); } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() {} }; @@ -16599,11 +16481,9 @@ public void onNext(ProcessingRequest request) { .build()); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -16723,11 +16603,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -16788,20 +16666,17 @@ public void onHeaders(Metadata headers) { onHeadersContext.set(testKey.get()); } - @Override public void onMessage(String message) { onMessageContext.set(testKey.get()); } - @Override public void onClose(Status status, Metadata trailers) { onCloseContext.set(testKey.get()); latch.countDown(); } - @Override public void onReady() { onReadyContext.set(testKey.get()); @@ -16856,11 +16731,9 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) {} - @Override public void onError(Throwable t) {} - @Override public void onCompleted() {} }; @@ -16975,11 +16848,9 @@ public void onNext(ProcessingRequest request) { requestSentLatch.countDown(); } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17101,11 +16972,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17226,11 +17095,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17350,11 +17217,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17459,11 +17324,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17564,11 +17427,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17674,11 +17535,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17779,11 +17638,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17891,11 +17748,9 @@ public void onNext(ProcessingRequest request) { } } - @Override public void onError(Throwable t) {} - @Override public void onCompleted() { responseObserver.onCompleted(); @@ -17959,6 +17814,9 @@ private static ClientCall interceptCall( MethodDescriptor method, CallOptions callOptions, Channel next) { + if (callOptions.getExecutor() == null) { + callOptions = callOptions.withExecutor(MoreExecutors.directExecutor()); + } Channel intercepted = ClientInterceptors.interceptForward( next, Arrays.asList(new XdsNameResolver.RawMessageClientInterceptor(), interceptor)); From 33d2df94dbd6e4447b19695d96b0305100a0cf82 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Tue, 28 Jul 2026 10:10:15 +0000 Subject: [PATCH 10/13] Test coverage and remove redundant branches. --- .../ExternalProcessorClientInterceptor.java | 106 +- ...xternalProcessorClientInterceptorTest.java | 3347 ++++++++++++++--- 2 files changed, 2902 insertions(+), 551 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java index 9357462ba7b..99ce8368e3a 100644 --- a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java +++ b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java @@ -185,12 +185,7 @@ static synchronized void initMetricInstruments() { ExternalProcessorFilterConfig getFilterConfig() { return filterConfig; } - - @VisibleForTesting - ManagedChannel getExtProcChannel() { - return extProcChannel; - } - + @Override @SuppressWarnings("unchecked") public ClientCall interceptCall( @@ -921,8 +916,8 @@ public void request(int numMessages) { if (normalFlowControl) { pendingRequests.addAndGet(numMessages); downstreamRequestsPending += numMessages; + drainPendingMutatedResponseBodies(); if (isExtProcReady()) { - drainPendingMutatedResponseBodies(); drainPendingRequests(); } } else { @@ -1097,32 +1092,27 @@ private void handleRequestBodyResponse(BodyResponse bodyResponse) { StreamedBodyResponse streamed = mutation.getStreamedResponse(); if (!streamed.getEndOfStreamWithoutMessage()) { com.google.protobuf.ByteString body = streamed.getBody(); - if (!config.getObservabilityMode() - && currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.GRPC) { - boolean sendImmediately = false; - synchronized (streamLock) { - if (sidestreamToUpstreamWindow <= 0) { - internalOnError(Status.INTERNAL - .withDescription( - "Flow control violation: received client body from ext_proc " - + "when window is closed") - .asRuntimeException()); - return; - } - sidestreamToUpstreamWindow -= body.size(); - if (super.isReady() && pendingUpstreamBodyMessages.isEmpty()) { - sendImmediately = true; - accumulatedWindowUpdateSidestreamToUpstream += body.size(); - } else { - pendingUpstreamBodyMessages.add(body); - } + boolean sendImmediately = false; + synchronized (streamLock) { + if (sidestreamToUpstreamWindow <= 0) { + internalOnError(Status.INTERNAL + .withDescription( + "Flow control violation: received client body from ext_proc " + + "when window is closed") + .asRuntimeException()); + return; } - if (sendImmediately) { - super.sendMessage(new KnownLengthInputStream(body)); - trySendAccumulatedWindowUpdates(); + sidestreamToUpstreamWindow -= body.size(); + if (super.isReady() && pendingUpstreamBodyMessages.isEmpty()) { + sendImmediately = true; + accumulatedWindowUpdateSidestreamToUpstream += body.size(); + } else { + pendingUpstreamBodyMessages.add(body); } - } else { + } + if (sendImmediately) { super.sendMessage(new KnownLengthInputStream(body)); + trySendAccumulatedWindowUpdates(); } } if (streamed.getEndOfStream() || streamed.getEndOfStreamWithoutMessage()) { @@ -1148,19 +1138,16 @@ private void handleResponseBodyResponse( StreamedBodyResponse streamed = mutation.getStreamedResponse(); com.google.protobuf.ByteString body = streamed.getBody(); final int bodySize = body.size(); - if (!config.getObservabilityMode() - && currentProcessingMode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC) { - synchronized (streamLock) { - if (sidestreamToDownstreamWindow <= 0) { - internalOnError(Status.INTERNAL - .withDescription( - "Flow control violation: received server body from ext_proc " - + "when window is closed") - .asRuntimeException()); - return; - } - sidestreamToDownstreamWindow -= bodySize; + synchronized (streamLock) { + if (sidestreamToDownstreamWindow <= 0) { + internalOnError(Status.INTERNAL + .withDescription( + "Flow control violation: received server body from ext_proc " + + "when window is closed") + .asRuntimeException()); + return; } + sidestreamToDownstreamWindow -= bodySize; } deliverResponseBody(body, listener); } @@ -1169,12 +1156,6 @@ private void handleResponseBodyResponse( private void deliverResponseBody(ByteString body, DataPlaneListener listener) { synchronized (streamLock) { - if (config.getObservabilityMode() - || currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC) { - callContext.run(() -> listener.onExternalBody(body)); - return; - } - if (downstreamRequestsPending > 0) { downstreamRequestsPending--; final int bodySize = body.size(); @@ -1199,23 +1180,18 @@ private void drainPendingMutatedResponseBodies() { while (downstreamRequestsPending > 0 && !pendingMutatedResponseBodies.isEmpty()) { ByteString body = pendingMutatedResponseBodies.poll(); downstreamRequestsPending--; - if (pendingRequests.get() > 0) { - pendingRequests.decrementAndGet(); - } + pendingRequests.decrementAndGet(); final int bodySize = body.size(); - final DataPlaneListener listener = wrappedListener; - if (listener != null) { - callContext.run(() -> { - try { - listener.onExternalBody(body); - } finally { - synchronized (streamLock) { - accumulatedWindowUpdateSidestreamToDownstream += bodySize; - } - trySendAccumulatedWindowUpdates(); + callContext.run(() -> { + try { + wrappedListener.onExternalBody(body); + } finally { + synchronized (streamLock) { + accumulatedWindowUpdateSidestreamToDownstream += bodySize; } - }); - } + trySendAccumulatedWindowUpdates(); + } + }); } } } @@ -1227,9 +1203,7 @@ void drainPendingUpstreamBodyMessages() { synchronized (streamLock) { if (super.isReady() && !pendingUpstreamBodyMessages.isEmpty()) { body = pendingUpstreamBodyMessages.poll(); - if (body != null) { - accumulatedWindowUpdateSidestreamToUpstream += body.size(); - } + accumulatedWindowUpdateSidestreamToUpstream += body.size(); if (pendingUpstreamBodyMessages.isEmpty() && pendingUpstreamHalfClose.compareAndSet(true, false)) { triggerHalfClose = true; } diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index 56a7ff48dd9..0ab9624768d 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -71,11 +71,8 @@ import io.grpc.stub.StreamObserver; import io.grpc.testing.GrpcCleanupRule; import io.grpc.util.MutableHandlerRegistry; -import io.grpc.xds.ConfigOrError; import io.grpc.xds.ExternalProcessorFilter.ExternalProcessorFilterConfig; import io.grpc.xds.ExternalProcessorFilter.ExternalProcessorFilterOverrideConfig; -import io.grpc.xds.Filter; -import io.grpc.xds.XdsNameResolver; import io.grpc.xds.client.Bootstrapper; import io.grpc.xds.client.EnvoyProtoData.Node; import io.grpc.xds.internal.grpcservice.CachedChannelManager; @@ -6131,7 +6128,8 @@ public void testRequestBodyDrainingBypassedWhenRequestBodyModeNone() throws Exce .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) @@ -6139,18 +6137,20 @@ public void testRequestBodyDrainingBypassedWhenRequestBodyModeNone() throws Exce .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final CountDownLatch sidecarActionLatch = new CountDownLatch(1); + final AtomicReference> responseObserverRef = + new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( final StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override @@ -6168,9 +6168,7 @@ public void onNext(ProcessingRequest request) { public void onError(Throwable t) {} @Override - public void onCompleted() { - responseObserver.onCompleted(); - } + public void onCompleted() {} }; } }; @@ -6187,45 +6185,87 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello " + request); - responseObserver.onCompleted(); + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } })) .build()); - final List dataPlaneSentMessages = new CopyOnWriteArrayList<>(); + final List dataPlaneSentMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName) .directExecutor() .intercept(new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, CallOptions callOptions, Channel next) { - return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void sendMessage(ReqT message) { - try { - InputStream stream = (InputStream) message; - byte[] bytes = com.google.common.io.ByteStreams.toByteArray(stream); - dataPlaneSentMessages.add( - new String(bytes, java.nio.charset.StandardCharsets.UTF_8)); - super.sendMessage((ReqT) new java.io.ByteArrayInputStream(bytes)); - } catch (IOException e) { - throw new RuntimeException(e); - } + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void sendMessage(ReqT message) { + try { + InputStream stream = (InputStream) message; + byte[] bytes = com.google.common.io.ByteStreams.toByteArray(stream); + dataPlaneSentMessages.add( + new String(bytes, java.nio.charset.StandardCharsets.UTF_8)); + super.sendMessage((ReqT) new java.io.ByteArrayInputStream(bytes)); + } catch (IOException e) { + throw new RuntimeException(e); } - }; - } + } + }; + } }) .build()); + final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch appMessageLatch = new CountDownLatch(2); + final CountDownLatch appCloseLatch = new CountDownLatch(1); + final AtomicReference appReceivedHeaders = new AtomicReference<>(); + final AtomicReference appReceivedStatus = new AtomicReference<>(); + + ClientCall.Listener appListener = new ClientCall.Listener() { + @Override + public void onHeaders(Metadata headers) { + appReceivedHeaders.set(headers); + } + + @Override + public void onMessage(String message) { + appReceivedMessages.add(message); + appMessageLatch.countDown(); + } + + @Override + public void onClose(Status status, Metadata trailers) { + appReceivedStatus.set(status); + appCloseLatch.countDown(); + } + }; + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); - proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + interceptCall(interceptor, METHOD_BIDI_STREAMING, callOptions, dataPlaneChannel); + proxyCall.start(appListener, new Metadata()); + proxyCall.request(10); assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Wait for the drain signal to be received and processed by client call @@ -6237,7 +6277,30 @@ public void sendMessage(ReqT message) { assertThat(dataPlaneSentMessages).containsExactly("Hello ExtProc"); - proxyCall.cancel("Cleanup", null); + // Server sends response headers and messages back. + // Since sendResponseHeaders is true and call is DRAINING, the response headers and messages + // should be saved. + StreamObserver upstreamResponseObserver = dataPlaneResponseObserverRef.get(); + upstreamResponseObserver.onNext("Dummy for headers"); + upstreamResponseObserver.onNext("Hello Downstream"); + + // Verify that app has not received headers or messages yet (because they are saved + // since stream is still DRAINING) + assertThat(appReceivedHeaders.get()).isNull(); + assertThat(appReceivedMessages).isEmpty(); + + // Now complete the ext_proc stream + responseObserverRef.get().onCompleted(); + upstreamResponseObserver.onCompleted(); + + // Verify that app receives headers, message, and close + assertThat(appMessageLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appReceivedHeaders.get()).isNotNull(); + assertThat(appReceivedMessages).containsExactly("Dummy for headers", "Hello Downstream"); + + assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appReceivedStatus.get().isOk()).isTrue(); + channelManager.close(); } @@ -7706,6 +7769,9 @@ public void drainingStartsAfterResponseHeaders_whenUpstreamResponds_thenBuffered final CountDownLatch sidecarFinishLatch = new CountDownLatch(1); final CountDownLatch drainCompletedLatch = new CountDownLatch(1); + final ByteString mutated1 = ByteString.copyFromUtf8("Mutated Message 1"); + final ByteString mutated2 = ByteString.copyFromUtf8("Mutated Message 2"); + // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -7718,66 +7784,85 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); reqHeadersLatch.countDown(); } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); respHeadersLatch.countDown(); } else if (request.hasResponseBody()) { String msgStr = request.getResponseBody().getBody().toStringUtf8(); if ("Original Message 1".equals(msgStr)) { - new Thread(() -> { - try { - // Wait until M2 is received by ext_proc server so both M1 and M2 are in flight - if (m2ReceivedLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8("Mutated Message 1")) - .build()) - .build()) - .build()) - .build()) - .setRequestDrain(true) - .build()); - respBody1Latch.countDown(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }).start(); + new Thread( + () -> { + try { + // Wait until M2 is received by ext_proc server so both M1 and M2 + // are in flight + if (m2ReceivedLatch.await(5, TimeUnit.SECONDS)) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse + .newBuilder() + .setBody(mutated1) + .build()) + .build()) + .build()) + .build()) + .setRequestDrain(true) + .build()); + respBody1Latch.countDown(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }) + .start(); } else if ("Original Message 2".equals(msgStr)) { m2ReceivedLatch.countDown(); - new Thread(() -> { - try { - // Wait until M3 is sent by upstream concurrently during drain - if (m3SentLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8("Mutated Message 2")) - .build()) - .build()) - .build()) - .build()) - .build()); - respBody2Latch.countDown(); - } - if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onCompleted(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }).start(); + new Thread( + () -> { + try { + // Wait until M3 is sent by upstream concurrently during drain + if (m3SentLatch.await(5, TimeUnit.SECONDS)) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse + .newBuilder() + .setBody(mutated2) + .build()) + .build()) + .build()) + .build()) + .build()); + respBody2Latch.countDown(); + } + if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { + responseObserver.onCompleted(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }) + .start(); } } } @@ -9002,16 +9087,19 @@ public ServerCall.Listener interceptCall( } }; - dataPlaneServiceRegistry.addService(ServerInterceptors.intercept( - ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello " + request); - responseObserver.onCompleted(); - dataPlaneLatch.countDown(); - })) - .build(), - dataPlaneInterceptor)); + dataPlaneServiceRegistry.addService( + ServerInterceptors.intercept( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_SAY_HELLO, + ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + dataPlaneLatch.countDown(); + })) + .build(), + dataPlaneInterceptor)); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); @@ -9034,7 +9122,8 @@ public void onClose(Status status, Metadata trailers) { // Trigger unary call. request(1) starts it. proxyCall.request(1); - // Wait for the async ext_proc server thread to enter activateCall() and block inside interceptCall + // Wait for the async ext_proc server thread to enter activateCall() and block inside + // interceptCall assertThat(headersReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Now, while the async thread is blocked (and passThroughMode is still false), @@ -9367,60 +9456,72 @@ public void givenUnsupportedCompressionInResponseBody_whenReceived_thenStreamErr // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - @SuppressWarnings("unchecked") - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { + extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder().build()) - .build()) - .build()); - } else if (request.hasRequestBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder().build()) - .build()) - .build()); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder().build()) - .build()) - .build()); - } else if (request.hasResponseBody()) { - // Simulate ext_proc server sending compressed body mutation (unsupported) for response body - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setGrpcMessageCompressed(true) + @SuppressWarnings("unchecked") + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders( + HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder().build()) .build()) - .build()) - .build()) - .build()) - .build()); - } - } + .build()); + } else if (request.hasRequestBody()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder().build()) + .build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseHeaders( + HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder().build()) + .build()) + .build()); + } else if (request.hasResponseBody()) { + // Simulate ext_proc server sending compressed body mutation (unsupported) for + // response body + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setGrpcMessageCompressed(true) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } - @Override - public void onError(Throwable t) { - } + @Override + public void onError(Throwable t) {} - @Override - public void onCompleted() { - responseObserver.onCompleted(); + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; } }; - } - }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -9631,53 +9732,180 @@ public void onCompleted() { } } - // --- Category 17: Immediate Response Handling --- - @Test @SuppressWarnings("unchecked") - public void givenImmediateResponse_whenReceived_thenDataPlaneCallCancelled() + public void testObservabilityMode_ProceedsWithoutBlockingOnExtProcResponseHeaders() throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); ExternalProcessor proto = ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) + .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) + .setObservabilityMode(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - // External Processor Server - ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { - @Override - @SuppressWarnings("unchecked") - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { + final CountDownLatch extProcReceivedHeadersLatch = new CountDownLatch(1); + final AtomicReference extProcReceivedRequest = new AtomicReference<>(); + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setImmediateResponse(ImmediateResponse.newBuilder() - .setGrpcStatus( - io.envoyproxy.envoy.service.ext_proc.v3.GrpcStatus.newBuilder() - .setStatus(Status.UNAUTHENTICATED.getCode().value()) - .build()) - .setDetails("Custom security rejection") - .build()) - .build()); - responseObserver.onCompleted(); - } - } + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasResponseHeaders()) { + extProcReceivedRequest.set(request); + extProcReceivedHeadersLatch.countDown(); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() {} + }; + } + }; + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final CountDownLatch dataPlaneLatch = new CountDownLatch(1); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + dataPlaneLatch.countDown(); + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); + final CountDownLatch appMessageLatch = new CountDownLatch(1); + final CountDownLatch appCloseLatch = new CountDownLatch(1); + final AtomicReference appReceivedHeaders = new AtomicReference<>(); + final AtomicReference appReceivedStatus = new AtomicReference<>(); + + ClientCall.Listener appListener = new ClientCall.Listener() { + @Override + public void onHeaders(Metadata headers) { + appReceivedHeaders.set(headers); + } + + @Override + public void onMessage(String message) { + appReceivedMessages.add(message); + appMessageLatch.countDown(); + } + + @Override + public void onClose(Status status, Metadata trailers) { + appReceivedStatus.set(status); + appCloseLatch.countDown(); + } + }; + + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); + ClientCall proxyCall = + interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); + proxyCall.start(appListener, new Metadata()); + proxyCall.request(1); + + proxyCall.sendMessage("test"); + proxyCall.halfClose(); + + // Verify data plane server received the request and processed it + assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // In observability mode, the app should receive response headers and messages immediately + // without waiting for the external processor stream to complete. + assertThat(appMessageLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appReceivedHeaders.get()).isNotNull(); + assertThat(appReceivedMessages).containsExactly("Hello test"); + + assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(appReceivedStatus.get().isOk()).isTrue(); + + // Also verify that the external processor received the response headers in the background + assertThat(extProcReceivedHeadersLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(extProcReceivedRequest.get().hasResponseHeaders()).isTrue(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + // --- Category 17: Immediate Response Handling --- + + @Test + @SuppressWarnings("unchecked") + public void givenImmediateResponse_whenReceived_thenDataPlaneCallCancelled() + throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") + .build()) + .build()) + .build()) + .build(); + ConfigOrError + configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + // External Processor Server + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + @SuppressWarnings("unchecked") + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setImmediateResponse(ImmediateResponse.newBuilder() + .setGrpcStatus( + io.envoyproxy.envoy.service.ext_proc.v3.GrpcStatus.newBuilder() + .setStatus(Status.UNAUTHENTICATED.getCode().value()) + .build()) + .setDetails("Custom security rejection") + .build()) + .build()); + responseObserver.onCompleted(); + } + } @Override public void onError(Throwable t) { @@ -9694,12 +9922,12 @@ public void onCompleted() { .directExecutor() .build().start()); - CachedChannelManager channelManager = new CachedChannelManager(config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(extProcServerName) - .directExecutor() - .build()); - }); + CachedChannelManager channelManager = + new CachedChannelManager( + config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(extProcServerName).directExecutor().build()); + }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); @@ -9736,10 +9964,11 @@ public void onClose(Status status, Metadata trailers) { assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.UNAUTHENTICATED); assertThat(closedStatus.get().getDescription()).isEqualTo("Custom security rejection"); - - // Data plane call should NOT have been started as ext_proc server rejected immediately on headers + + // Data plane call should NOT have been started as ext_proc server rejected immediately on + // headers assertThat(dataPlaneStarted.get()).isFalse(); - + proxyCall.cancel("Cleanup", null); channelManager.close(); } @@ -10445,13 +10674,16 @@ public boolean isReady() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })) - .build()); + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_SAY_HELLO, + ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); + })) + .build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName) @@ -10488,7 +10720,8 @@ public boolean isReady() { sidecarReady.set(false); assertThat(proxyCall.isReady()).isFalse(); - // Ext_proc server becomes ready, but Data Plane is busy -> proxyCall is STILL ready because Normal Mode + // Ext_proc server becomes ready, but Data Plane is busy -> proxyCall is STILL ready because + // Normal Mode sidecarReady.set(true); dataPlaneReady.set(false); assertThat(proxyCall.isReady()).isTrue(); @@ -10618,25 +10851,29 @@ public boolean isReady() { final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( - new ServerCalls.BidiStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - dataPlaneResponseObserverRef.set(responseObserver); - return new StreamObserver() { - @Override - public void onNext(String value) {} + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_BIDI_STREAMING, + ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} - @Override - public void onError(Throwable t) {} + @Override + public void onError(Throwable t) {} - @Override - public void onCompleted() {} - }; - } - })) - .build()); + @Override + public void onCompleted() {} + }; + } + })) + .build()); final AtomicInteger dataPlaneRequestCount = new AtomicInteger(0); ManagedChannel dataPlaneChannel = grpcCleanup.register( @@ -10678,7 +10915,8 @@ public void request(int numMessages) { assertThat(dataPlaneRequestCount.get()).isEqualTo(1); // (Only the initial bootstrap request went through) - // Ext_proc server becomes ready -> buffered requests should start draining (pulling next message) + // Ext_proc server becomes ready -> buffered requests should start draining (pulling next + // message) sidecarReady.set(true); extProcListenerRef.get().onReady(); @@ -11037,9 +11275,9 @@ public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Except .build()) .setProcessingMode(ProcessingMode.newBuilder() .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); ConfigOrError @@ -11071,6 +11309,20 @@ public void onNext(ProcessingRequest request) { .build()); firstBodyLatch.countDown(); } else if (request.hasRequestBody()) { + if (request.getRequestBody().getEndOfStreamWithoutMessage()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setEndOfStreamWithoutMessage(true) + .build()) + .build()) + .build()) + .build()) + .build()); + return; + } if (firstBodyLatch.getCount() > 0) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() @@ -11152,6 +11404,10 @@ public void onCompleted() { })) .build()); + final List dataPlaneResponseMessages = new CopyOnWriteArrayList<>(); + final CountDownLatch callClosedLatch = new CountDownLatch(1); + final AtomicReference callClosedStatus = new AtomicReference<>(); + ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); @@ -11160,7 +11416,19 @@ public void onCompleted() { DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.start(new ClientCall.Listener() { + @Override + public void onMessage(String message) { + dataPlaneResponseMessages.add(message); + } + + @Override + public void onClose(Status status, Metadata trailers) { + callClosedStatus.set(status); + callClosedLatch.countDown(); + } + }, new Metadata()); + proxyCall.request(1); // Generate large messages String largeMessage70k = new String(new char[70000]).replace('\0', 'a'); @@ -11174,6 +11442,10 @@ public void onCompleted() { // Send second message (30000 bytes) - total 100000 > 65536, should buffer proxyCall.sendMessage(largeMessage30k); + // Call halfClose() while the second message is still buffered. + // This should NOT trigger immediate half-close, but mark pendingHalfClose = true. + proxyCall.halfClose(); + // Assert that it is NOT delivered to ext_proc (delivery is synchronous on // directExecutor, so we can check immediately) assertThat(receivedRequests).hasSize(3); @@ -11187,23 +11459,33 @@ public void onCompleted() { .build()) .build()); - // The second body should now be flushed and received by ext_proc + // The second body should now be flushed and received by ext_proc, and then half-closed assertThat(secondBodyLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(receivedRequests.size()).isAtLeast(4); + // Verify both messages reached the backend service + assertThat(dataPlaneReceivedMessages) + .containsExactly(largeMessage70k, largeMessage30k).inOrder(); + + // Wait for the call to close successfully. + assertThat(callClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(callClosedStatus.get().isOk()).isTrue(); + assertThat(dataPlaneResponseMessages).containsExactly("Response"); + + // The mock ext_proc should have received 5 requests: + // 1. Headers + // 2. First Body (70000) + // 3. Client Window Update + // 4. Second Body (30000) + // 5. EndOfStreamWithoutMessage (half-close) + assertThat(receivedRequests.size()).isEqualTo(5); assertThat(receivedRequests.get(0).hasRequestHeaders()).isTrue(); assertThat(receivedRequests.get(1).hasRequestBody()).isTrue(); assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); assertThat(receivedRequests.get(3).hasRequestBody()).isTrue(); assertThat(receivedRequests.get(3).getRequestBody().getBody().size()).isEqualTo(30000); + assertThat(receivedRequests.get(4).hasRequestBody()).isTrue(); + assertThat(receivedRequests.get(4).getRequestBody().getEndOfStreamWithoutMessage()).isTrue(); - assertThat(proxyCall.isReady()).isTrue(); - - // Verify both messages reached the backend service - assertThat(dataPlaneReceivedMessages) - .containsExactly(largeMessage70k, largeMessage30k).inOrder(); - - proxyCall.cancel("Cleanup", null); channelManager.close(); } @@ -11319,25 +11601,29 @@ public void onCompleted() { final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( - new ServerCalls.BidiStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - dataPlaneResponseObserverRef.set(responseObserver); - return new StreamObserver() { - @Override - public void onNext(String value) {} + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_BIDI_STREAMING, + ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} - @Override - public void onError(Throwable t) {} + @Override + public void onError(Throwable t) {} - @Override - public void onCompleted() {} - }; - } - })) - .build()); + @Override + public void onCompleted() {} + }; + } + })) + .build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); @@ -11371,7 +11657,8 @@ public void onMessage(String message) { String largeMessage70k = new String(new char[70000]).replace('\0', 'a'); String largeMessage30k = new String(new char[30000]).replace('\0', 'b'); - // Wait for the initialization (headers, request body, response headers) to reach the ext_proc server + // Wait for the initialization (headers, request body, response headers) to reach the ext_proc + // server assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Upstream sends 70k response chunk. Since window is 65,536, this drives the window @@ -11776,30 +12063,33 @@ public void onClose(Status status, Metadata trailers) { @Test @SuppressWarnings("unchecked") public void testSidestreamToUpstreamFlowControl_QueuingAndDraining() throws Exception { - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) + ExternalProcessor proto = + ExternalProcessor.newBuilder() + .setGrpcService( + GrpcService.newBuilder() + .setGoogleGrpc( + GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin( + Any.newBuilder().setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL).build()) + .build()) .build()) - .build()) - .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) - .build()) - .build(); + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) + .build()) + .build(); ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final CountDownLatch finishLatch = new CountDownLatch(1); - final AtomicReference serverReceivedBody = new AtomicReference<>(); - final CountDownLatch serverReceivedLatch = new CountDownLatch(1); + final List serverReceivedBodies = new CopyOnWriteArrayList<>(); + final CountDownLatch serverReceivedLatch = new CountDownLatch(2); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -11811,28 +12101,44 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); } else if (request.hasRequestBody()) { - boolean eos = request.getRequestBody().getEndOfStream() - || request.getRequestBody().getEndOfStreamWithoutMessage(); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8(eos ? "" : "MutatedRequest")) - .setEndOfStream(eos) - .build()) + ByteString original = request.getRequestBody().getBody(); + boolean eos = + request.getRequestBody().getEndOfStream() + || request.getRequestBody().getEndOfStreamWithoutMessage(); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + ByteString.copyFromUtf8( + eos + ? "" + : "Mutated" + + original + .toStringUtf8())) + .setEndOfStream(eos) + .build()) + .build()) + .build()) .build()) - .build()) - .build()) - .build()); + .build()); } } + @Override public void onError(Throwable t) {} + @Override public void onCompleted() { responseObserver.onCompleted(); @@ -11855,30 +12161,34 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( - new ServerCalls.ClientStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - return new StreamObserver() { - @Override - public void onNext(String value) { - serverReceivedBody.set(value); - serverReceivedLatch.countDown(); - } + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_CLIENT_STREAMING, + ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) { + serverReceivedBodies.add(value); + serverReceivedLatch.countDown(); + } - @Override - public void onError(Throwable t) {} + @Override + public void onError(Throwable t) {} - @Override - public void onCompleted() { - responseObserver.onNext("Response"); - responseObserver.onCompleted(); - } - }; - } - })) - .build()); + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } + })) + .build()); final AtomicBoolean transportReady = new AtomicBoolean(false); final AtomicReference> capturedListenerRef = new AtomicReference<>(); @@ -11888,11 +12198,13 @@ class TriggerableForwardingCall TriggerableForwardingCall(ClientCall delegate) { super(delegate); } + @Override public void start(Listener responseListener, Metadata headers) { capturedListenerRef.set(responseListener); super.start(responseListener, headers); } + @Override public boolean isReady() { return transportReady.get(); @@ -11915,30 +12227,48 @@ public ClientCall interceptCall( interceptCall(interceptor, METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - proxyCall.start(new ClientCall.Listener() { - @Override - public void onClose(Status status, Metadata trailers) { - finishLatch.countDown(); - } - }, new Metadata()); + proxyCall.start( + new ClientCall.Listener() { + @Override + public void onClose(Status status, Metadata trailers) { + finishLatch.countDown(); + } + }, + new Metadata()); proxyCall.request(1); - // Send a message. This gets mutated to "MutatedRequest" by ext_proc. - proxyCall.sendMessage("OriginalRequest"); + // Send first message. This gets mutated to "MutatedOriginalRequest 1" by ext_proc. + proxyCall.sendMessage("OriginalRequest 1"); - // Give some time to process and ensure the message is NOT received on the server side because transport is not ready + // Give some time to process and ensure the message is NOT received on the server side because + // transport is not ready assertThat(serverReceivedLatch.await(500, TimeUnit.MILLISECONDS)).isFalse(); - assertThat(serverReceivedBody.get()).isNull(); + assertThat(serverReceivedBodies).isEmpty(); - // Now make the transport ready and trigger onReady callback + // Now make the transport ready (super.isReady() returns true), but do NOT trigger onReady drain + // yet. transportReady.set(true); + + // Send second message. This gets mutated to "MutatedOriginalRequest 2" by ext_proc. + // Since transportReady is true but there's still a pending message in the queue, + // the second message should also be queued (to preserve order). + proxyCall.sendMessage("OriginalRequest 2"); + + // Ensure still no message is received on the server side (since we haven't triggered drain via + // onReady) + assertThat(serverReceivedLatch.await(500, TimeUnit.MILLISECONDS)).isFalse(); + assertThat(serverReceivedBodies).isEmpty(); + + // Now trigger onReady callback to drain the queue. ClientCall.Listener listener = capturedListenerRef.get(); assertThat(listener).isNotNull(); listener.onReady(); - // The message should be drained and forwarded to the backend server + // Both messages should be drained and forwarded to the backend server in order. assertThat(serverReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(serverReceivedBody.get()).isEqualTo("MutatedRequest"); + assertThat(serverReceivedBodies) + .containsExactly("MutatedOriginalRequest 1", "MutatedOriginalRequest 2") + .inOrder(); proxyCall.halfClose(); assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); @@ -11948,61 +12278,69 @@ public void onClose(Status status, Metadata trailers) { @Test @SuppressWarnings("unchecked") - public void testThresholdBasedWindowUpdates() throws Exception { - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) + public void testSidestreamToUpstreamFlowControl_DelayedHalfClose() throws Exception { + ExternalProcessor proto = + ExternalProcessor.newBuilder() + .setGrpcService( + GrpcService.newBuilder() + .setGoogleGrpc( + GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin( + Any.newBuilder().setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL).build()) + .build()) .build()) - .build()) - .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) - .build(); - ConfigOrError - configOrError = + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) + .build()) + .build(); + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch extProcLatch = new CountDownLatch(1); - final List> observers = new ArrayList<>(); + final List serverReceivedBodies = new CopyOnWriteArrayList<>(); + final CountDownLatch serverReceivedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override public StreamObserver process( final StreamObserver responseObserver) { - observers.add(responseObserver); ((ServerCallStreamObserver) responseObserver).request(100); return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - receivedRequests.add(request); if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - extProcLatch.countDown(); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); } else if (request.hasRequestBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getRequestBody().getBody()) - .build()) + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + ByteString.copyFromUtf8("Mutated1")) + .setEndOfStream(true) + .build()) + .build()) + .build()) .build()) - .build()) - .build()) - .build()); + .build()); + extProcLatch.countDown(); } } @@ -12031,92 +12369,1843 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( - new ServerCalls.BidiStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - return new StreamObserver() { - @Override - public void onNext(String value) {} + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_CLIENT_STREAMING, + ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) { + serverReceivedBodies.add(value); + serverReceivedLatch.countDown(); + } - @Override - public void onError(Throwable t) {} + @Override + public void onError(Throwable t) {} - @Override - public void onCompleted() {} - }; - } - })) - .build()); + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } + })) + .build()); + + final AtomicBoolean transportReady = new AtomicBoolean(false); + final AtomicReference> capturedListenerRef = new AtomicReference<>(); + final AtomicInteger halfCloseCallCount = new AtomicInteger(0); + + class DelayedHalfCloseForwardingCall + extends io.grpc.ForwardingClientCall.SimpleForwardingClientCall { + DelayedHalfCloseForwardingCall(ClientCall delegate) { + super(delegate); + } + + @Override + public void start(Listener responseListener, Metadata headers) { + capturedListenerRef.set(responseListener); + super.start(responseListener, headers); + } + + @Override + public boolean isReady() { + return transportReady.get(); + } + + @Override + public void halfClose() { + halfCloseCallCount.incrementAndGet(); + super.halfClose(); + } + } ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new DelayedHalfCloseForwardingCall<>(next.newCall(method, callOptions)); + } + }) + .directExecutor() + .build()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); + interceptCall( + interceptor, + METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.request(1); + + // Send the first client message. This gets mutated to "Mutated1" by ext_proc. + proxyCall.sendMessage("OriginalRequest 1"); assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(filterClientRequests(receivedRequests)).hasSize(1); - assertThat(filterClientRequests(receivedRequests).get(0).hasRequestHeaders()).isTrue(); - // 1. Send body message. It should be sent immediately. - proxyCall.sendMessage("Msg 1"); // size = 5 bytes + // Since transportReady is false, the mutated body is queued in pendingUpstreamBodyMessages. + // And since it was unilateral half-close, pendingUpstreamHalfClose is set to true. + // Verify that the call is NOT half-closed on transport yet. + assertThat(halfCloseCallCount.get()).isEqualTo(0); + assertThat(serverReceivedBodies).isEmpty(); - assertThat(filterClientRequests(receivedRequests)).hasSize(2); - assertThat(filterClientRequests(receivedRequests).get(1).hasRequestBody()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(1) - .getRequestBody().getBody().toStringUtf8()) + // Now make the transport ready and trigger onReady callback + transportReady.set(true); + ClientCall.Listener listener = capturedListenerRef.get(); + assertThat(listener).isNotNull(); + listener.onReady(); + + // The queued message should be drained, forwarded to backend server, + // and the delayed half-close should be triggered. + assertThat(serverReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(serverReceivedBodies).containsExactly("Mutated1"); + assertThat(halfCloseCallCount.get()).isEqualTo(1); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testSidestreamToUpstreamFlowControl_FailOpenDuringDelayedHalfClose() + throws Exception { + ExternalProcessor proto = + ExternalProcessor.newBuilder() + .setGrpcService( + GrpcService.newBuilder() + .setGoogleGrpc( + GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin( + Any.newBuilder().setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL).build()) + .build()) + .build()) + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) + .build()) + .setFailureModeAllow(true) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final CountDownLatch extProcLatch = new CountDownLatch(1); + final AtomicReference> responseObserverRef = + new AtomicReference<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + responseObserverRef.set(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + // Unilaterally send a request body response containing mutated body and + // endOfStream = true + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + ByteString.copyFromUtf8("Mutated1")) + .setEndOfStream(true) + .build()) + .build()) + .build()) + .build()) + .build()); + extProcLatch.countDown(); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register( + InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build() + .start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = + new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_CLIENT_STREAMING, + ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } + })) + .build()); + + final AtomicBoolean transportReady = new AtomicBoolean(false); + final AtomicReference> capturedListenerRef = new AtomicReference<>(); + final AtomicInteger halfCloseCallCount = new AtomicInteger(0); + final AtomicInteger sendMessageCount = new AtomicInteger(0); + + class FailOpenDelayedHalfCloseForwardingCall + extends io.grpc.ForwardingClientCall.SimpleForwardingClientCall { + FailOpenDelayedHalfCloseForwardingCall(ClientCall delegate) { + super(delegate); + } + + @Override + public void start(Listener responseListener, Metadata headers) { + capturedListenerRef.set(responseListener); + super.start(responseListener, headers); + } + + @Override + public boolean isReady() { + return transportReady.get(); + } + + @Override + public void sendMessage(ReqT message) { + sendMessageCount.incrementAndGet(); + super.sendMessage(message); + } + + @Override + public void halfClose() { + halfCloseCallCount.incrementAndGet(); + super.halfClose(); + } + } + + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept( + new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + return new FailOpenDelayedHalfCloseForwardingCall<>( + next.newCall(method, callOptions)); + } + }) + .directExecutor() + .build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.request(1); + + // Call halfClose immediately. This sets pendingHalfClose = true. + proxyCall.halfClose(); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // Since transportReady is false, the mutated body is queued. + // And since it was unilateral half-close, pendingUpstreamHalfClose is set to true. + // Verify that the call is NOT half-closed on transport yet. + assertThat(halfCloseCallCount.get()).isEqualTo(0); + + // Fail the ext_proc stream to trigger fail-open. + responseObserverRef.get().onError(Status.INTERNAL.asRuntimeException()); + + // Fail-open will see pendingHalfClose = true, set requestSideClosed = true, + // and immediately invoke transport.halfClose() + assertThat(halfCloseCallCount.get()).isEqualTo(1); + + // Now make transport ready and trigger onReady callback + transportReady.set(true); + ClientCall.Listener listener = capturedListenerRef.get(); + assertThat(listener).isNotNull(); + listener.onReady(); + + // The queued message should be drained, forwarded to backend server call. + // The delayed half-close is triggered, but since requestSideClosed was already true (via + // fail-open), + // it will evaluate to false in compareAndSet and NOT invoke transport.halfClose() again. + assertThat(sendMessageCount.get()).isEqualTo(1); + // Verify that transport.halfClose() was still called exactly once + assertThat(halfCloseCallCount.get()).isEqualTo(1); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testSidestreamToDownstreamFlowControl_QueuingAndWithholdingWindowUpdates() + throws Exception { + ExternalProcessor proto = + ExternalProcessor.newBuilder() + .setGrpcService( + GrpcService.newBuilder() + .setGoogleGrpc( + GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin( + Any.newBuilder().setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL).build()) + .build()) + .build()) + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = + Collections.synchronizedList(new ArrayList<>()); + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch firstBodyResponseLatch = new CountDownLatch(1); + final CountDownLatch secondBodyResponseLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + headersLatch.countDown(); + } else if (request.hasResponseBody()) { + ByteString body = request.getResponseBody().getBody(); + boolean eos = request.getResponseBody().getEndOfStream(); + if (body.size() == 40001) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(body) + .setEndOfStream(eos) + .build()) + .build()) + .build()) + .build()) + .build()); + firstBodyResponseLatch.countDown(); + + // Send second body (40002) - spoofed + ByteString body2 = + ByteString.copyFromUtf8(new String(new char[40002]).replace('\0', 'y')); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(body2) + .setEndOfStream(eos) + .build()) + .build()) + .build()) + .build()) + .build()); + secondBodyResponseLatch.countDown(); + } + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = + new CachedChannelManager( + config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); + }); + + ExternalProcessorClientInterceptor interceptor = + new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_BIDI_STREAMING, + ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + final List receivedResponses = Collections.synchronizedList(new ArrayList<>()); + proxyCall.start(new ClientCall.Listener() { + @Override + public void onMessage(String message) { + receivedResponses.add(message); + } + }, new Metadata()); + + // Wait for the headers handshake to complete and activate the call + assertThat(headersLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + proxyCall.request(1); + + String resp1 = new String(new char[40001]).replace('\0', 'x'); + dataPlaneResponseObserverRef.get().onNext(resp1); + + assertThat(firstBodyResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(secondBodyResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(receivedResponses).containsExactly(resp1); + + List windowUpdates = new ArrayList<>(); + for (ProcessingRequest req : receivedRequests) { + if (req.hasClientWindowUpdate()) { + windowUpdates.add(req); + } + } + assertThat(windowUpdates).hasSize(1); + assertThat( + windowUpdates.get(0).getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) + .isEqualTo(40001); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testSidestreamToDownstreamFlowControl_DrainingAndSendingWindowUpdates() + throws Exception { + ExternalProcessor proto = + ExternalProcessor.newBuilder() + .setGrpcService( + GrpcService.newBuilder() + .setGoogleGrpc( + GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin( + Any.newBuilder().setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL).build()) + .build()) + .build()) + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = + Collections.synchronizedList(new ArrayList<>()); + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch firstBodyResponseLatch = new CountDownLatch(1); + final CountDownLatch secondBodyResponseLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + headersLatch.countDown(); + } else if (request.hasResponseBody()) { + ByteString body = request.getResponseBody().getBody(); + boolean eos = request.getResponseBody().getEndOfStream(); + if (body.size() == 40001) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(body) + .setEndOfStream(eos) + .build()) + .build()) + .build()) + .build()) + .build()); + firstBodyResponseLatch.countDown(); + + // Send second body (40002) - spoofed + ByteString body2 = + ByteString.copyFromUtf8(new String(new char[40002]).replace('\0', 'y')); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(body2) + .setEndOfStream(eos) + .build()) + .build()) + .build()) + .build()) + .build()); + secondBodyResponseLatch.countDown(); + } + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register( + InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build() + .start()); + + CachedChannelManager channelManager = + new CachedChannelManager( + config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); + }); + + ExternalProcessorClientInterceptor interceptor = + new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() {} + }; + } + })).build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + final List receivedResponses = Collections.synchronizedList(new ArrayList<>()); + proxyCall.start(new ClientCall.Listener() { + @Override + public void onMessage(String message) { + receivedResponses.add(message); + } + }, new Metadata()); + + // Wait for the headers handshake to complete and activate the call + assertThat(headersLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + proxyCall.request(1); + + String resp1 = new String(new char[40001]).replace('\0', 'x'); + String resp2 = new String(new char[40002]).replace('\0', 'y'); + dataPlaneResponseObserverRef.get().onNext(resp1); + + assertThat(firstBodyResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(secondBodyResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + assertThat(receivedResponses).containsExactly(resp1); + + List windowUpdates = new ArrayList<>(); + for (ProcessingRequest req : receivedRequests) { + if (req.hasClientWindowUpdate()) { + windowUpdates.add(req); + } + } + assertThat(windowUpdates).hasSize(1); + assertThat( + windowUpdates.get(0).getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) + .isEqualTo(40001); + + proxyCall.request(1); + assertThat(receivedResponses).containsExactly(resp1, resp2); + + windowUpdates.clear(); + for (ProcessingRequest req : receivedRequests) { + if (req.hasClientWindowUpdate()) { + windowUpdates.add(req); + } + } + assertThat(windowUpdates).hasSize(2); + assertThat( + windowUpdates.get(1).getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) + .isEqualTo(40002); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testThresholdBasedWindowUpdates() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError + configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(1); + final List> observers = new ArrayList<>(); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + observers.add(responseObserver); + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasRequestBody()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + request.getRequestBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_BIDI_STREAMING, ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, DEFAULT_CALL_OPTIONS, dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(filterClientRequests(receivedRequests)).hasSize(1); + assertThat(filterClientRequests(receivedRequests).get(0).hasRequestHeaders()).isTrue(); + + // 1. Send body message. It should be sent immediately. + proxyCall.sendMessage("Msg 1"); // size = 5 bytes + + assertThat(filterClientRequests(receivedRequests)).hasSize(2); + assertThat(filterClientRequests(receivedRequests).get(1).hasRequestBody()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(1) + .getRequestBody().getBody().toStringUtf8()) + + .isEqualTo("Msg 1"); + // No window updates were accumulated yet. + assertThat(filterClientRequests(receivedRequests).get(1).hasClientWindowUpdate()).isFalse(); + + // 2. Trigger window replenishment below threshold (e.g. 5 bytes from Msg 1 response). + // The interceptor processes the response, forwards it upstream, and increments + // accumulatedWindowUpdateSidestreamToUpstream. Since 5 < 32768, it won't send + // standalone updates. + // We send another message "Msg 2" to trigger piggybacking. + proxyCall.sendMessage("Msg 2"); + + assertThat(filterClientRequests(receivedRequests)).hasSize(3); + assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2) + .getRequestBody().getBody().toStringUtf8()) + + .isEqualTo("Msg 2"); + // Verify accumulated 5 bytes update is piggybacked. + assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); + assertThat(filterClientRequests(receivedRequests).get(2) + .getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + + .isEqualTo(5); + + // 3. Accumulate past threshold (e.g. 35,000 bytes) without sending body messages. + // This should trigger an immediate standalone ClientWindowUpdate. + StreamObserver responseObserver = observers.get(0); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFrom(new byte[35000])) + .build()) + .build()) + .build()) + .build()) + .build()); + + // standalone client window update received. + assertThat(filterClientRequests(receivedRequests)).hasSize(4); + assertThat(filterClientRequests(receivedRequests).get(3).hasClientWindowUpdate()).isTrue(); + assertThat( + filterClientRequests(receivedRequests) + .get(3) + .getClientWindowUpdate() + .getWindowIncrementSidestreamToUpstream()) + .isEqualTo(35005); + assertThat(filterClientRequests(receivedRequests).get(3).hasRequestBody()).isFalse(); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testWindowUpdateWithheldWhenUpstreamCapacityExistsAndBelowThreshold() + throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = new CopyOnWriteArrayList<>(); + final CountDownLatch extProcLatch = new CountDownLatch(2); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasRequestBody()) { + // Mutate request body and send back 10000 bytes (below threshold 32768) + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + ByteString.copyFrom(new byte[10000])) + .build()) + .build()) + .build()) + .build()) + .build()); + extProcLatch.countDown(); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build().start()); + + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName).directExecutor().build()); + }); + + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke(StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall( + interceptor, + METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + // Send 10k message to ext_proc. + String body10k = new String(new char[10000]).replace('\0', 'a'); + proxyCall.sendMessage(body10k); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // Since the window has capacity (65536 - 10000 = 55536 > 0) and the increment (10000) + // is below the threshold, NO window update should be sent. + // receivedRequests should only contain Headers and RequestBody (size = 2). + assertThat(receivedRequests).hasSize(2); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testWindowUpdateWithheldWhenDownstreamCapacityExistsAndBelowThreshold() + throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + ExternalProcessor proto = + createBaseProto(uniqueExtProcServerName) + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = + Collections.synchronizedList(new ArrayList<>()); + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch bodyResponseLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + headersLatch.countDown(); + } else if (request.hasResponseBody()) { + boolean eos = request.getResponseBody().getEndOfStream(); + // Mutate response body and send back 10000 bytes (below threshold 32768) + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + ByteString.copyFrom(new byte[10000])) + .setEndOfStream(eos) + .build()) + .build()) + .build()) + .build()) + .build()); + bodyResponseLatch.countDown(); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + grpcCleanup.register( + InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build() + .start()); + + CachedChannelManager channelManager = + new CachedChannelManager( + config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); + }); + + ExternalProcessorClientInterceptor interceptor = + new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_BIDI_STREAMING, + ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + final List receivedResponses = Collections.synchronizedList(new ArrayList<>()); + proxyCall.start(new ClientCall.Listener() { + @Override + public void onMessage(String message) { + receivedResponses.add(message); + } + }, new Metadata()); + + assertThat(headersLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + proxyCall.request(1); + + String resp1 = new String(new char[10000]).replace('\0', 'x'); + dataPlaneResponseObserverRef.get().onNext(resp1); + + assertThat(bodyResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // The response body (10000) should be delivered. + assertThat(receivedResponses).hasSize(1); + + // Since the window has capacity (65536 - 10000 = 55536 > 0) and the increment (10000) + // is below the threshold, NO window update should be sent to the ext_proc. + // receivedRequests should only contain Headers and ResponseBody (size = 2). + assertThat(receivedRequests).hasSize(2); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testWindowUpdateWithheldOnUpstreamWindowExhaustionWithZeroIncrement() + throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + ExternalProcessor proto = + createBaseProto(uniqueExtProcServerName) + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = + Collections.synchronizedList(new ArrayList<>()); + final CountDownLatch extProcLatch = new CountDownLatch(2); + final CountDownLatch responseBodyProcessedLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasRequestBody()) { + extProcLatch.countDown(); + // Mutate request body and send back 70000 bytes (exhausts upstream return + // window). + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + ByteString.copyFrom(new byte[70000])) + .build()) + .build()) + .build()) + .build()) + .build()); + } else if (request.hasResponseBody()) { + // Mutate response body and send back 20000 bytes (below threshold) + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + ByteString.copyFrom(new byte[20000])) + .build()) + .build()) + .build()) + .build()) + .build()); + responseBodyProcessedLatch.countDown(); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + grpcCleanup.register( + InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build() + .start()); + + CachedChannelManager channelManager = + new CachedChannelManager( + config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); + }); + + ExternalProcessorClientInterceptor interceptor = + new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_BIDI_STREAMING, + ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + final AtomicBoolean transportReady = new AtomicBoolean(false); + final AtomicReference> dataPlaneListenerRef = new AtomicReference<>(); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + dataPlaneListenerRef.set(responseListener); + super.start(responseListener, headers); + } + + @Override + public boolean isReady() { + return transportReady.get(); + } + }; + } + }) + .directExecutor() + .build()); + + ClientCall proxyCall = + interceptCall( + interceptor, + METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + // Send 10k message to ext_proc. + String body10k = new String(new char[10000]).replace('\0', 'a'); + proxyCall.sendMessage(body10k); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(receivedRequests).hasSize(2); // Headers + RequestBody + + // Request 1 response body message to deliver. + proxyCall.request(1); + + // Send a response from data plane to trigger downstream delivery. + String responseMsg = new String(new char[20000]).replace('\0', 'y'); + dataPlaneResponseObserverRef.get().onNext(responseMsg); + + assertThat(responseBodyProcessedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // Since the window is exhausted but isReady() is false, and the drained upstream increment is + // 0, + // the trySendAccumulatedWindowUpdates() logic evaluates "iu > 0" (2nd) to False. + // So no window update is sent. + // receivedRequests should only contain Headers, RequestBody, and ResponseBody (size = 3). + assertThat(receivedRequests).hasSize(3); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testWindowUpdateWithheldOnDownstreamWindowExhaustionWithZeroIncrement() + throws Exception { + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + ExternalProcessor proto = + createBaseProto(uniqueExtProcServerName) + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = + Collections.synchronizedList(new ArrayList<>()); + final CountDownLatch extProcLatch = new CountDownLatch(1); // Headers + final CountDownLatch responseBodyReceivedLatch = new CountDownLatch(1); + final CountDownLatch requestBodyProcessedLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + extProcLatch.countDown(); + } else if (request.hasResponseBody()) { + // Mutate response body and send back 70000 bytes (exhausts downstream return + // window). + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + ByteString.copyFrom(new byte[70000])) + .build()) + .build()) + .build()) + .build()) + .build()); + responseBodyReceivedLatch.countDown(); + } else if (request.hasRequestBody()) { + // Mutate request body and send back 20000 bytes (below threshold). + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody( + ByteString.copyFrom(new byte[20000])) + .build()) + .build()) + .build()) + .build()) + .build()); + requestBodyProcessedLatch.countDown(); + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + grpcCleanup.register( + InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build() + .start()); + + CachedChannelManager channelManager = + new CachedChannelManager( + config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); + }); + + ExternalProcessorClientInterceptor interceptor = + new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_BIDI_STREAMING, + ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + final AtomicBoolean transportReady = new AtomicBoolean(true); + @SuppressWarnings("rawtypes") + final AtomicReference dataPlaneListenerRef = new AtomicReference<>(); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + @SuppressWarnings("unchecked") + public void start(Listener responseListener, Metadata headers) { + dataPlaneListenerRef.set(responseListener); + super.start(responseListener, headers); + } + + @Override + public boolean isReady() { + return transportReady.get(); + } + }; + } + }) + .directExecutor() + .build()); + + ClientCall proxyCall = + interceptCall( + interceptor, + METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), + dataPlaneChannel); + + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + + assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // 1. Manually trigger onMessage on the client listener. + // Since proxyCall.request() was never called, downstreamRequestsPending is 0. + // So the mutated body from ext_proc will be queued in pendingMutatedResponseBodies, + // and accumulated downstream increment (id) will remain 0. + String responseMsg = new String(new char[10000]).replace('\0', 'y'); + InputStream responseStream = METHOD_BIDI_STREAMING.streamResponse(responseMsg); + dataPlaneListenerRef.get().onMessage(responseStream); + + assertThat(responseBodyReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + // receivedRequests contains Headers + ResponseBody + assertThat(receivedRequests).hasSize(2); + + // 2. Now send a request body of size 10000. + // It will be sent to ext_proc, mutated to 20000 bytes (below threshold), and delivered + // immediately to raw call. + // During trySendAccumulatedWindowUpdates(), RHS evaluates to False (since id = 0, wd <= 0, but + // id > 0 (2nd) is False). + // So no window update is sent. + String requestMsg = new String(new char[10000]).replace('\0', 'a'); + proxyCall.sendMessage(requestMsg); + + assertThat(requestBodyProcessedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // receivedRequests should only contain Headers, ResponseBody, and RequestBody (size = 3). + assertThat(receivedRequests).hasSize(3); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testExtProcUnilateralHalfClose_PreventsDuplicateHalfClose() throws Exception { + ExternalProcessor proto = ExternalProcessor.newBuilder() + .setGrpcService(GrpcService.newBuilder() + .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin(Any.newBuilder() + .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) + .build()) + .build()) + .build()) + .setProcessingMode(ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final CountDownLatch extProcUnilateralLatch = new CountDownLatch(1); + final CountDownLatch clientHalfCloseProcessedLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } else if (request.hasRequestBody()) { + HttpBody requestBody = request.getRequestBody(); + if (requestBody.getEndOfStreamWithoutMessage()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setEndOfStream(true) + .build()) + .build()) + .build()) + .build()) + .build()); + clientHalfCloseProcessedLatch.countDown(); + } else { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(requestBody.getBody()) + .setEndOfStream(true) + .build()) + .build()) + .build()) + .build()) + .build()); + extProcUnilateralLatch.countDown(); + } + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register( + InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build() + .start()); + + CachedChannelManager channelManager = + new CachedChannelManager( + config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); + }); + + ExternalProcessorClientInterceptor interceptor = + new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicInteger halfCloseCallCount = new AtomicInteger(0); + final CountDownLatch serverResponseLatch = new CountDownLatch(1); + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_CLIENT_STREAMING, + ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + new Thread( + () -> { + try { + serverResponseLatch.await(5, TimeUnit.SECONDS); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + }) + .start(); + } + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept(new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, CallOptions callOptions, Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( + next.newCall(method, callOptions)) { + @Override + public void halfClose() { + halfCloseCallCount.incrementAndGet(); + super.halfClose(); + } + }; + } + }) + .directExecutor() + .build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_CLIENT_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); - .isEqualTo("Msg 1"); - // No window updates were accumulated yet. - assertThat(filterClientRequests(receivedRequests).get(1).hasClientWindowUpdate()).isFalse(); + proxyCall.start(new ClientCall.Listener() {}, new Metadata()); + proxyCall.request(1); - // 2. Trigger window replenishment below threshold (e.g. 5 bytes from Msg 1 response). - // The interceptor processes the response, forwards it upstream, and increments - // accumulatedWindowUpdateSidestreamToUpstream. Since 5 < 32768, it won't send - // standalone updates. - // We send another message "Msg 2" to trigger piggybacking. - proxyCall.sendMessage("Msg 2"); + // Send Message 1. This will trigger the unilateral half-close from mock ext_proc. + proxyCall.sendMessage("Message 1"); - assertThat(filterClientRequests(receivedRequests)).hasSize(3); - assertThat(filterClientRequests(receivedRequests).get(2).hasRequestBody()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2) - .getRequestBody().getBody().toStringUtf8()) + assertThat(extProcUnilateralLatch.await(5, TimeUnit.SECONDS)).isTrue(); - .isEqualTo("Msg 2"); - // Verify accumulated 5 bytes update is piggybacked. - assertThat(filterClientRequests(receivedRequests).get(2).hasClientWindowUpdate()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(2) - .getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + // Verify that the transport halfClose was called once due to unilateral half-close + assertThat(halfCloseCallCount.get()).isEqualTo(1); - .isEqualTo(5); + // Now call halfClose() on proxyCall. + // This should send endOfStreamWithoutMessage to ext_proc, and receive endOfStream response. + // However, it should NOT trigger a second halfClose() on the transport. + proxyCall.halfClose(); - // 3. Accumulate past threshold (e.g. 35,000 bytes) without sending body messages. - // This should trigger an immediate standalone ClientWindowUpdate. - StreamObserver responseObserver = observers.get(0); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFrom(new byte[35000])) - .build()) - .build()) - .build()) - .build()) - .build()); + assertThat(clientHalfCloseProcessedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // standalone client window update received. - assertThat(filterClientRequests(receivedRequests)).hasSize(4); - assertThat(filterClientRequests(receivedRequests).get(3).hasClientWindowUpdate()).isTrue(); - assertThat(filterClientRequests(receivedRequests).get(3) - .getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) + // Verify that the transport halfClose count remains 1 + assertThat(halfCloseCallCount.get()).isEqualTo(1); - .isEqualTo(35005); - assertThat(filterClientRequests(receivedRequests).get(3).hasRequestBody()).isFalse(); + // Let the server complete now + serverResponseLatch.countDown(); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -13050,9 +15139,11 @@ public void onMessage(String message) { } assertThat(receivedRequests).hasSize(5); assertThat(receivedRequests.get(4).hasClientWindowUpdate()).isTrue(); - assertThat(receivedRequests.get(4) - .getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) - + assertThat( + receivedRequests + .get(4) + .getClientWindowUpdate() + .getWindowIncrementSidestreamToDownstream()) .isEqualTo(40000); proxyCall.cancel("Cleanup", null); @@ -13061,23 +15152,27 @@ public void onMessage(String message) { @Test @SuppressWarnings("unchecked") - public void testClientWindowUpdateSentImmediatelyOnWindowExhaustion() throws Exception { - ExternalProcessor proto = ExternalProcessor.newBuilder() - .setGrpcService(GrpcService.newBuilder() - .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() - .setTargetUri("in-process:///" + extProcServerName) - .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL) + public void testClientWindowUpdateSentImmediatelyOnSidestreamToUpstreamWindowExhaustion() + throws Exception { + ExternalProcessor proto = + ExternalProcessor.newBuilder() + .setGrpcService( + GrpcService.newBuilder() + .setGoogleGrpc( + GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin( + Any.newBuilder().setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL).build()) + .build()) .build()) - .build()) - .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) - .build(); + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); @@ -13086,6 +15181,7 @@ public void testClientWindowUpdateSentImmediatelyOnWindowExhaustion() throws Exc final List receivedRequests = new CopyOnWriteArrayList<>(); final CountDownLatch extProcLatch = new CountDownLatch(2); // Headers + Request Body + final CountDownLatch windowUpdateLatch = new CountDownLatch(1); // Window Update final AtomicReference> responseObserverRef = new AtomicReference<>(); @@ -13107,19 +15203,33 @@ public void onNext(ProcessingRequest request) { .build()); } else if (request.hasRequestBody()) { extProcLatch.countDown(); - // Mutate request body and send back 65536 bytes. This completely exhausts the - // return window (starts at 65536). + // Mutate request body and send back 20000 bytes. + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFrom(new byte[20000])) + .build()) + .build()) + .build()) + .build()) + .build()); + // Mutate request body and send back 50000 bytes. + // Total 70000 bytes completely exhausts the return window (starts at 65536). responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() .setBodyMutation(BodyMutation.newBuilder() .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFrom(new byte[65536])) + .setBody(ByteString.copyFrom(new byte[50000])) .build()) .build()) .build()) .build()) .build()); + } else if (request.hasClientWindowUpdate()) { + windowUpdateLatch.countDown(); } } @@ -13148,57 +15258,73 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") - .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( - new ServerCalls.ClientStreamingMethod() { - @Override - public StreamObserver invoke(StreamObserver responseObserver) { - return new StreamObserver() { - @Override - public void onNext(String value) {} + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_CLIENT_STREAMING, + ServerCalls.asyncClientStreamingCall( + new ServerCalls.ClientStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + return new StreamObserver() { + @Override + public void onNext(String value) {} - @Override - public void onError(Throwable t) {} + @Override + public void onError(Throwable t) {} - @Override - public void onCompleted() { - responseObserver.onNext("Response"); - responseObserver.onCompleted(); - } - }; - } - })) - .build()); + @Override + public void onCompleted() { + responseObserver.onNext("Response"); + responseObserver.onCompleted(); + } + }; + } + })) + .build()); final AtomicBoolean transportReady = new AtomicBoolean(false); final AtomicReference> dataPlaneListenerRef = new AtomicReference<>(); - ManagedChannel dataPlaneChannel = grpcCleanup.register( - InProcessChannelBuilder.forName(dataPlaneServerName) - .intercept(new ClientInterceptor() { - @Override - public ClientCall interceptCall( - MethodDescriptor method, CallOptions callOptions, Channel next) { - return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall( - next.newCall(method, callOptions)) { - @Override - public void start(Listener responseListener, Metadata headers) { - dataPlaneListenerRef.set(responseListener); - super.start(responseListener, headers); - } + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName) + .intercept( + new ClientInterceptor() { + @Override + public ClientCall interceptCall( + MethodDescriptor method, + CallOptions callOptions, + Channel next) { + return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall< + ReqT, RespT>(next.newCall(method, callOptions)) { + @Override + public void start(Listener responseListener, Metadata headers) { + dataPlaneListenerRef.set(responseListener); + super.start(responseListener, headers); + } - @Override - public boolean isReady() { - return transportReady.get(); - } - }; - } - }) - .directExecutor() - .build()); + @Override + public void sendMessage(ReqT message) { + transportReady.set(false); + super.sendMessage(message); + } + + @Override + public boolean isReady() { + return transportReady.get(); + } + }; + } + }) + .directExecutor() + .build()); ClientCall proxyCall = - interceptCall(interceptor, METHOD_CLIENT_STREAMING, + interceptCall( + interceptor, + METHOD_CLIENT_STREAMING, DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); @@ -13211,23 +15337,274 @@ public boolean isReady() { assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Since the window is exhausted but isReady() is false, no window update should be sent. - // Wait a brief moment to ensure no window update is sent. - Thread.sleep(200); assertThat(receivedRequests).hasSize(2); - // Now, trigger transport ready. This should flush the accumulated increment of 65536. + // Now, trigger transport ready. This should flush only the first message (20000) and set ready + // to false. transportReady.set(true); dataPlaneListenerRef.get().onReady(); - long startTime = System.currentTimeMillis(); - while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { - Thread.sleep(10); - } + // Wait for the window update to be received by ext_proc + assertThat(windowUpdateLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(receivedRequests).hasSize(3); assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); assertThat(receivedRequests.get(2) .getClientWindowUpdate().getWindowIncrementSidestreamToUpstream()) - .isEqualTo(65536); + .isEqualTo(20000); + + proxyCall.cancel("Cleanup", null); + channelManager.close(); + } + + @Test + @SuppressWarnings("unchecked") + public void testClientWindowUpdateSentImmediatelyOnSidestreamToDownstreamWindowExhaustion() + throws Exception { + ExternalProcessor proto = + ExternalProcessor.newBuilder() + .setGrpcService( + GrpcService.newBuilder() + .setGoogleGrpc( + GrpcService.GoogleGrpc.newBuilder() + .setTargetUri("in-process:///" + extProcServerName) + .addChannelCredentialsPlugin( + Any.newBuilder().setTypeUrl(INSECURE_CREDENTIALS_TYPE_URL).build()) + .build()) + .build()) + .setProcessingMode( + ProcessingMode.newBuilder() + .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) + .setRequestBodyMode(ProcessingMode.BodySendMode.NONE) + .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) + .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) + .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) + .build()) + .build(); + ConfigOrError configOrError = + provider.parseFilterConfig(Any.pack(proto), filterContext); + assertThat(configOrError.errorDetail).isNull(); + ExternalProcessorFilterConfig filterConfig = configOrError.config; + + final List receivedRequests = + Collections.synchronizedList(new ArrayList<>()); + final CountDownLatch headersLatch = new CountDownLatch(1); + final CountDownLatch firstBodyResponseLatch = new CountDownLatch(1); + final CountDownLatch secondBodyResponseLatch = new CountDownLatch(1); + final CountDownLatch thirdBodyResponseLatch = new CountDownLatch(1); + + ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = + new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { + @Override + public void onNext(ProcessingRequest request) { + receivedRequests.add(request); + if (request.hasRequestHeaders()) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + headersLatch.countDown(); + } else if (request.hasResponseBody()) { + ByteString body = request.getResponseBody().getBody(); + boolean eos = request.getResponseBody().getEndOfStream(); + if (body.size() == 50000) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(body) + .setEndOfStream(eos) + .build()) + .build()) + .build()) + .build()) + .build()); + firstBodyResponseLatch.countDown(); + + // Send second body (20000) - spoofed + ByteString body2 = ByteString.copyFrom(new byte[20000]); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(body2) + .setEndOfStream(eos) + .build()) + .build()) + .build()) + .build()) + .build()); + secondBodyResponseLatch.countDown(); + + // Send third body (50000) - spoofed + ByteString body3 = ByteString.copyFrom(new byte[50000]); + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse.newBuilder() + .setBody(body3) + .setEndOfStream(eos) + .build()) + .build()) + .build()) + .build()) + .build()); + thirdBodyResponseLatch.countDown(); + } + } + } + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() { + responseObserver.onCompleted(); + } + }; + } + }; + + String uniqueExtProcServerName = InProcessServerBuilder.generateName(); + grpcCleanup.register( + InProcessServerBuilder.forName(uniqueExtProcServerName) + .addService(extProcImpl) + .directExecutor() + .build() + .start()); + + CachedChannelManager channelManager = + new CachedChannelManager( + config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(uniqueExtProcServerName) + .directExecutor() + .build()); + }); + + ExternalProcessorClientInterceptor interceptor = + new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); + + final AtomicReference> dataPlaneResponseObserverRef = + new AtomicReference<>(); + dataPlaneServiceRegistry.addService( + ServerServiceDefinition.builder("test.TestService") + .addMethod( + METHOD_BIDI_STREAMING, + ServerCalls.asyncBidiStreamingCall( + new ServerCalls.BidiStreamingMethod() { + @Override + public StreamObserver invoke( + StreamObserver responseObserver) { + dataPlaneResponseObserverRef.set(responseObserver); + return new StreamObserver() { + @Override + public void onNext(String value) {} + + @Override + public void onError(Throwable t) {} + + @Override + public void onCompleted() {} + }; + } + })) + .build()); + + ManagedChannel dataPlaneChannel = + grpcCleanup.register( + InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); + + ClientCall proxyCall = + interceptCall(interceptor, METHOD_BIDI_STREAMING, + DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()), dataPlaneChannel); + + final List receivedResponses = Collections.synchronizedList(new ArrayList<>()); + proxyCall.start( + new ClientCall.Listener() { + @Override + public void onMessage(String message) { + receivedResponses.add(message); + } + }, + new Metadata()); + + // Wait for the headers handshake to complete and activate the call + assertThat(headersLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + proxyCall.request(1); + + String resp1 = new String(new char[50000]).replace('\0', 'x'); + dataPlaneResponseObserverRef.get().onNext(resp1); + + assertThat(firstBodyResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(secondBodyResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(thirdBodyResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); + + // The first response body (50000) should be delivered. + assertThat(receivedResponses).containsExactly(resp1); + + // Let's filter out window updates received by mock ext_proc. + // There should be exactly 1 window update (for the first response body, since 50000 >= 32768). + List windowUpdates = new ArrayList<>(); + for (ProcessingRequest req : receivedRequests) { + if (req.hasClientWindowUpdate()) { + windowUpdates.add(req); + } + } + assertThat(windowUpdates).hasSize(1); + assertThat( + windowUpdates.get(0).getClientWindowUpdate().getWindowIncrementSidestreamToDownstream()) + .isEqualTo(50000); + + // Now, request another message. + // This will deliver the second body (20000) which was queued. + // Since the window was exhausted (initial 65536 - 50000 - 20000 - 50000 = -54464 <= 0), + // delivering the second body (20000) should immediately trigger a window update of 20000, + // even though 20000 is less than the threshold (32768). + proxyCall.request(1); + + assertThat(receivedResponses).hasSize(2); + assertThat(receivedResponses.get(1).length()).isEqualTo(20000); + + windowUpdates.clear(); + for (ProcessingRequest req : receivedRequests) { + if (req.hasClientWindowUpdate()) { + windowUpdates.add(req); + } + } + // We should now have 2 window updates. + assertThat(windowUpdates).hasSize(2); + assertThat( + windowUpdates + .get(1) + .getClientWindowUpdate() + .getWindowIncrementSidestreamToDownstream()) + .isEqualTo(20000); proxyCall.cancel("Cleanup", null); channelManager.close(); From a16d5de2a3fb4e03ea83d0b789c381515f0a0bb2 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Wed, 29 Jul 2026 14:25:36 +0000 Subject: [PATCH 11/13] Re-introduce lost synchronization fixes in the tests during merge. --- .../ExternalProcessorClientInterceptor.java | 32 +- ...xternalProcessorClientInterceptorTest.java | 1309 ++++++++--------- 2 files changed, 668 insertions(+), 673 deletions(-) diff --git a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java index 99ce8368e3a..69439f24cb2 100644 --- a/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java +++ b/xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java @@ -228,7 +228,6 @@ public ClientCall interceptCall( io.grpc.stub.MetadataUtils.newAttachHeadersInterceptor(extraHeaders)); } - // The filter chain is preceded by RawMessageClientInterceptor, so ReqT and RespT are // InputStream. MethodDescriptor rawMethod = @@ -379,8 +378,6 @@ protected DataPlaneClientCall( this.backendService = checkNotNull(backendService, "backendService"); } - - private void activateCall() { if ((extProcStreamState.get() == ExtProcStreamState.FAILED && !config.getFailureModeAllow() @@ -447,8 +444,6 @@ private boolean validateCompressionSupport(BodyResponse bodyResponse) { return true; } - - @Override public void start(Listener responseListener, Metadata headers) { this.callContext = Context.current(); @@ -686,7 +681,7 @@ private void sendToExtProc(ProcessingRequest request) { if (extProcStreamState.get().isCompleted()) { return; } - + if (request.hasRequestHeaders()) { expectedRequestResponse = EventType.REQUEST_HEADERS; } else if (request.hasResponseHeaders()) { @@ -806,7 +801,7 @@ void drainPendingRequests() { } // Normal mode flow control: pull 1 message at a time - if (isExtProcReady() && upstreamToSidestreamWindow > 0 && pendingRequests.get() > 0) { + if (isSidecarReady() && upstreamToSidestreamWindow > 0 && pendingRequests.get() > 0) { super.request(1); pendingRequests.decrementAndGet(); } @@ -858,7 +853,7 @@ private void onReadyNotify() { wrappedListener.onReadyNotify(); } - boolean isExtProcReady() { + boolean isSidecarReady() { ExtProcStreamState state = extProcStreamState.get(); if (state.isCompleted()) { return true; @@ -884,11 +879,11 @@ public boolean isReady() { return false; } synchronized (streamLock) { - boolean extProcReady = isExtProcReady(); + boolean sidecarReady = isSidecarReady(); if (config.getObservabilityMode()) { - return super.isReady() && extProcReady; + return super.isReady() && sidecarReady; } - return downstreamToSidestreamWindow > 0 && extProcReady + return downstreamToSidestreamWindow > 0 && sidecarReady && pendingRequestBodyMessages.isEmpty(); } } @@ -909,20 +904,22 @@ public void request(int numMessages) { return; } - // We send response bodies to ext_proc server (either in normal GRPC mode or observability mode). + // We send response bodies to ext_proc server (either in normal GRPC mode or + // observability mode). // Gated by ext_proc server readiness. - boolean normalFlowControl = !config.getObservabilityMode(); // i.e. normal GRPC response body mode + // i.e. normal GRPC response body mode + boolean normalFlowControl = !config.getObservabilityMode(); if (normalFlowControl) { pendingRequests.addAndGet(numMessages); downstreamRequestsPending += numMessages; drainPendingMutatedResponseBodies(); - if (isExtProcReady()) { + if (isSidecarReady()) { drainPendingRequests(); } } else { // Observability mode: gate on readiness but pull all at once - if (isExtProcReady()) { + if (isSidecarReady()) { super.request(numMessages); } else { pendingRequests.addAndGet(numMessages); @@ -1204,7 +1201,8 @@ void drainPendingUpstreamBodyMessages() { if (super.isReady() && !pendingUpstreamBodyMessages.isEmpty()) { body = pendingUpstreamBodyMessages.poll(); accumulatedWindowUpdateSidestreamToUpstream += body.size(); - if (pendingUpstreamBodyMessages.isEmpty() && pendingUpstreamHalfClose.compareAndSet(true, false)) { + if (pendingUpstreamBodyMessages.isEmpty() + && pendingUpstreamHalfClose.compareAndSet(true, false)) { triggerHalfClose = true; } } @@ -1473,7 +1471,7 @@ public void onMessage(InputStream message) { void drainSavedMessages() { synchronized (dataPlaneClientCall.getStreamLock()) { - while (dataPlaneClientCall.isExtProcReady() + while (dataPlaneClientCall.isSidecarReady() && dataPlaneClientCall.upstreamToSidestreamWindow > 0 && !savedMessages.isEmpty()) { InputStream msg = savedMessages.poll(); diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index 0ab9624768d..f709b166462 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -94,7 +94,6 @@ import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; @@ -450,9 +449,7 @@ public void givenOverrideConfig_whenProcessingModeOverridden_thenReplacesWholeMo // Full replacement: requestBodyMode becomes GRPC, others become defaults (0/DEFAULT/NONE) assertThat(mergedMode.getRequestBodyMode()).isEqualTo(ProcessingMode.BodySendMode.GRPC); assertThat(mergedMode.getRequestHeaderMode()).isEqualTo(ProcessingMode.HeaderSendMode.DEFAULT); - assertThat(mergedMode.getResponseHeaderMode()) - - .isEqualTo(ProcessingMode.HeaderSendMode.DEFAULT); + assertThat(mergedMode.getResponseHeaderMode()).isEqualTo(ProcessingMode.HeaderSendMode.DEFAULT); assertThat(mergedMode.getResponseBodyMode()).isEqualTo(ProcessingMode.BodySendMode.NONE); } @@ -642,8 +639,7 @@ public void givenInterceptor_whenCallIntercepted_thenExtProcStubUsesSerializingE .build()) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -738,8 +734,7 @@ public void givenGrpcServiceWithTimeout_whenCallIntercepted_thenExtProcStubHasCo .setTimeout(com.google.protobuf.Duration.newBuilder().setSeconds(5).build()) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -824,7 +819,7 @@ public void protocolConfig_onHeaders() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch extProcLatch = new CountDownLatch(3); + final CountDownLatch sidecarLatch = new CountDownLatch(3); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -851,7 +846,7 @@ public void onNext(ProcessingRequest request) { .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } - extProcLatch.countDown(); + sidecarLatch.countDown(); } @Override @@ -906,7 +901,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(2); @@ -934,7 +929,7 @@ public void protocolConfig_onBody() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch extProcLatch = new CountDownLatch(2); + final CountDownLatch sidecarLatch = new CountDownLatch(2); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -957,7 +952,7 @@ public void onNext(ProcessingRequest request) { .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } - extProcLatch.countDown(); + sidecarLatch.countDown(); } @Override @@ -1012,7 +1007,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); @@ -1040,7 +1035,7 @@ public void protocolConfig_onResponseHeaders() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch extProcLatch = new CountDownLatch(2); + final CountDownLatch sidecarLatch = new CountDownLatch(2); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1063,7 +1058,7 @@ public void onNext(ProcessingRequest request) { .setResponseBody(BodyResponse.newBuilder().build()) .build()); } - extProcLatch.countDown(); + sidecarLatch.countDown(); } @Override @@ -1118,7 +1113,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); @@ -1146,7 +1141,7 @@ public void protocolConfig_onResponseBody() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1165,7 +1160,7 @@ public void onNext(ProcessingRequest request) { .setResponseBody(BodyResponse.newBuilder().build()) .build()); } - extProcLatch.countDown(); + sidecarLatch.countDown(); } @Override @@ -1220,7 +1215,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); @@ -1248,7 +1243,7 @@ public void protocolConfig_onResponseTrailers() String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = dataPlaneServerName; - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1267,7 +1262,7 @@ public void onNext(ProcessingRequest request) { .setResponseTrailers(TrailersResponse.newBuilder().build()) .build()); } - extProcLatch.countDown(); + sidecarLatch.countDown(); } @Override @@ -1322,7 +1317,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); @@ -1371,8 +1366,7 @@ public void givenGrpcServiceWithInitialMetadata_whenCallIntercepted_thenSendsMet .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -1470,7 +1464,7 @@ public void requestAttributes_onHeaders() .addRequestAttributes("request.host") .build(); - final CountDownLatch extProcLatch = new CountDownLatch(2); + final CountDownLatch sidecarLatch = new CountDownLatch(2); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1492,7 +1486,7 @@ public void onNext(ProcessingRequest request) { .setRequestBody(BodyResponse.newBuilder().build()) .build()); } - extProcLatch.countDown(); + sidecarLatch.countDown(); } @Override @@ -1541,7 +1535,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(2); // First request should be RequestHeaders and should have attributes @@ -1582,7 +1576,7 @@ public void requestAttributes_onBody() .addRequestAttributes("request.host") .build(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1600,7 +1594,7 @@ public void onNext(ProcessingRequest request) { .setRequestBody(BodyResponse.newBuilder().build()) .build()); } - extProcLatch.countDown(); + sidecarLatch.countDown(); } @Override @@ -1649,7 +1643,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); // First request should be RequestBody and should have attributes @@ -1690,7 +1684,7 @@ public void requestAttributes_notSent() .addRequestAttributes("request.host") .build(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final List capturedRequests = Collections.synchronizedList(new ArrayList<>()); @@ -1708,7 +1702,7 @@ public void onNext(ProcessingRequest request) { .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); } - extProcLatch.countDown(); + sidecarLatch.countDown(); } @Override @@ -1757,7 +1751,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequests.size()).isAtLeast(1); // First request should be ResponseHeaders, and should NOT have attributes @@ -1790,8 +1784,7 @@ public void givenRequestHeaderModeSend_whenStartCalled_thenCallIsBuffered() .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -1891,8 +1884,7 @@ public void givenRequestHeaderModeSend_whenExtProcRespondsWithMutations_thenCall .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -1917,24 +1909,26 @@ public void onNext(ProcessingRequest request) { } catch (InterruptedException e) { Thread.currentThread().interrupt(); } - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setHeaderMutation(HeaderMutation.newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("x-mutated") - .setValue("true") - .build()) - .build()) - .build()) - .build()) - .build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setHeaderMutation(HeaderMutation.newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("x-mutated") + .setValue("true") + .build()) + .build()) + .build()) + .build()) + .build()) + .build()); + } } }).start(); } @@ -1945,7 +1939,11 @@ public void onError(Throwable t) { @Override public void onCompleted() { - new Thread(() -> responseObserver.onCompleted()).start(); + new Thread(() -> { + synchronized (responseObserver) { + responseObserver.onCompleted(); + } + }).start(); } }; } @@ -2037,8 +2035,7 @@ public void givenHeaderModeSend_whenCallHasBinaryHeaders_thenBinaryHeadersForwar .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2061,10 +2058,12 @@ public void onNext(ProcessingRequest request) { capturedRequest.set(request); } new Thread(() -> { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + synchronized (responseObserver) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } } extProcLatch.countDown(); }).start(); @@ -2076,7 +2075,9 @@ public void onError(Throwable t) { @Override public void onCompleted() { - responseObserver.onCompleted(); + synchronized (responseObserver) { + responseObserver.onCompleted(); + } } }; } @@ -2163,14 +2164,13 @@ public void givenRequestHeaderModeSkip_whenStartCalled_thenCallIsActivated() thr .setProcessingMode(ProcessingMode.newBuilder() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; // External Processor Server - final AtomicInteger extProcMessages = new AtomicInteger(0); + final AtomicInteger sidecarMessages = new AtomicInteger(0); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override @@ -2181,7 +2181,7 @@ public StreamObserver process( return new StreamObserver() { @Override public void onNext(ProcessingRequest request) { - extProcMessages.incrementAndGet(); + sidecarMessages.incrementAndGet(); } @Override @@ -2241,8 +2241,8 @@ public void onCompleted() { // Verify main call started immediately assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // Verify ext_proc server RECEIVED message about headers because default is SEND - assertThat(extProcMessages.get()).isEqualTo(1); + // Verify sidecar RECEIVED message about headers because default is SEND + assertThat(sidecarMessages.get()).isEqualTo(1); proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -2270,8 +2270,7 @@ public void onCompleted() { .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2386,8 +2385,7 @@ public void givenRequestBodyModeGrpc_whenSendMessageCalled_thenMessageSentToExtP .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2405,38 +2403,40 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { new Thread(() -> { - if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); - } else if (request.hasRequestBody()) { - if (capturedRequest.get() == null - && !request.getRequestBody().getBody().isEmpty()) { - capturedRequest.set(request); - bodySentLatch.countDown(); - } - BodyResponse.Builder bodyResponse = BodyResponse.newBuilder(); - if (request.getRequestBody().getBody().isEmpty() - && request.getRequestBody().getEndOfStreamWithoutMessage()) { - bodyResponse.setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setEndOfStream(true) - .build()) - .build()) + synchronized (responseObserver) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - } else { - bodyResponse.setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setEndOfStream(request.getRequestBody().getEndOfStream()) - .build()) - .build()) + } else if (request.hasRequestBody()) { + if (capturedRequest.get() == null + && !request.getRequestBody().getBody().isEmpty()) { + capturedRequest.set(request); + bodySentLatch.countDown(); + } + BodyResponse.Builder bodyResponse = BodyResponse.newBuilder(); + if (request.getRequestBody().getBody().isEmpty() + && request.getRequestBody().getEndOfStreamWithoutMessage()) { + bodyResponse.setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setEndOfStream(true) + .build()) + .build()) + .build()); + } else { + bodyResponse.setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setEndOfStream(request.getRequestBody().getEndOfStream()) + .build()) + .build()) + .build()); + } + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(bodyResponse.build()) .build()); } - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(bodyResponse.build()) - .build()); } }).start(); } @@ -2447,7 +2447,11 @@ public void onError(Throwable t) { @Override public void onCompleted() { - new Thread(() -> responseObserver.onCompleted()).start(); + new Thread(() -> { + synchronized (responseObserver) { + responseObserver.onCompleted(); + } + }).start(); } }; } @@ -2521,8 +2525,7 @@ public void givenRequestBodyModeGrpc_whenExtProcRespondsWithMutatedBody_thenMuta .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2657,8 +2660,7 @@ public void givenRequestBodyModeGrpc_whenExtProcRespondsEmpty_thenEmptyMsgDelive .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -2798,14 +2800,13 @@ public void givenExtProcSignaledEndOfStream_whenClientSendsMoreMessages_thenMess .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; // External Processor Server - final AtomicInteger extProcMessages = new AtomicInteger(0); + final AtomicInteger sidecarMessages = new AtomicInteger(0); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override @@ -2821,7 +2822,7 @@ public void onNext(ProcessingRequest request) { .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasRequestBody()) { - extProcMessages.incrementAndGet(); + sidecarMessages.incrementAndGet(); boolean triggerEos = request.getRequestBody().getBody().toStringUtf8().equals("Trigger EOS"); BodyResponse.Builder bodyResponse = BodyResponse.newBuilder(); @@ -2910,7 +2911,7 @@ public void onCompleted() { proxyCall.sendMessage("Too late"); assertThat(dataPlaneMessages.get()).isEqualTo(1); - // Verify ext_proc server received Trigger EOS and half-close + // Verify sidecar received Trigger EOS and half-close proxyCall.cancel("Cleanup", null); channelManager.close(); @@ -2937,8 +2938,7 @@ public void givenRequestBodyModeNone_whenSendMessageCalled_thenMessageSentDirect .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.NONE).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -3044,8 +3044,7 @@ public void givenResponseHeaderModeSend_whenExtProcRespondsWithMutatedHeaders_th .setProcessingMode(ProcessingMode.newBuilder() .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -3184,8 +3183,7 @@ public void givenResponseHeaderModeSkip_responseHeadersSentDirectlyUpstream() .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SEND) .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -3194,10 +3192,10 @@ public void givenResponseHeaderModeSkip_responseHeadersSentDirectlyUpstream() Metadata.Key.of("custom-response-header", Metadata.ASCII_STRING_MARSHALLER); // External Processor Server - final AtomicBoolean responseHeadersReceived = - new AtomicBoolean(false); - final CountDownLatch requestHeadersLatch = - new CountDownLatch(1); + final java.util.concurrent.atomic.AtomicBoolean responseHeadersReceived = + new java.util.concurrent.atomic.AtomicBoolean(false); + final java.util.concurrent.CountDownLatch requestHeadersLatch = + new java.util.concurrent.CountDownLatch(1); final CountDownLatch extProcCompletedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -3337,14 +3335,13 @@ public void givenResponseBodyModeGrpc_whenOnMessageCalled_thenMessageSentToExtPr .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; // External Processor Server - final CountDownLatch extProcBodyLatch = new CountDownLatch(1); + final CountDownLatch sidecarBodyLatch = new CountDownLatch(1); final AtomicReference capturedRequest = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -3367,7 +3364,7 @@ public void onNext(ProcessingRequest request) { } else if (request.hasResponseBody()) { if (capturedRequest.get() == null && !request.getResponseBody().getBody().isEmpty()) { capturedRequest.set(request); - extProcBodyLatch.countDown(); + sidecarBodyLatch.countDown(); } responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder() @@ -3454,7 +3451,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.halfClose(); long startTime = System.currentTimeMillis(); - while (extProcBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { + while (sidecarBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); Thread.sleep(10); } @@ -3500,7 +3497,7 @@ public void givenResponseBodyModeGrpc_whenExtProcRespondsWithMutatedBody_thenMut // External Processor Server MutableHandlerRegistry extProcRegistry = new MutableHandlerRegistry(); - final CountDownLatch extProcBodyLatch = new CountDownLatch(1); + final CountDownLatch sidecarBodyLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override @@ -3531,7 +3528,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - extProcBodyLatch.countDown(); + sidecarBodyLatch.countDown(); } else if (request.hasResponseTrailers()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseTrailers(TrailersResponse.newBuilder().build()) @@ -3612,7 +3609,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.halfClose(); long startTime = System.currentTimeMillis(); - while (extProcBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { + while (sidecarBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); Thread.sleep(10); } @@ -3639,7 +3636,7 @@ public void onClose(Status status, Metadata trailers) { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final AtomicReference capturedRequest = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -3674,7 +3671,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); completeResponse(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() @@ -3779,7 +3776,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Verify status was propagated correctly @@ -3804,7 +3801,7 @@ public void givenResponseTrailerModeSend_whenCallCloses_thenResponseTrailersSent String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final AtomicReference capturedRequest = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -3830,7 +3827,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseTrailers(TrailersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); completeResponse(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() @@ -3928,7 +3925,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(capturedRequest.get().hasResponseTrailers()).isTrue(); assertThat(capturedRequest.get().getResponseTrailers().getTrailers().getHeadersList()) @@ -3943,9 +3940,9 @@ public void givenResponseTrailerModeDefault_whenCallCloses_thenResponseTrailersN String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final AtomicInteger extProcTrailerCount = new AtomicInteger(0); - final CountDownLatch extProcLatch = new CountDownLatch(1); - final CountDownLatch extProcHeadersLatch = new CountDownLatch(1); + final AtomicInteger sidecarTrailerCount = new AtomicInteger(0); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch sidecarHeadersLatch = new CountDownLatch(1); final CountDownLatch extProcCompletedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -3967,17 +3964,17 @@ private void completeResponse() { @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { - extProcTrailerCount.incrementAndGet(); + sidecarTrailerCount.incrementAndGet(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcHeadersLatch.countDown(); + sidecarHeadersLatch.countDown(); } } @@ -4075,12 +4072,12 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Wait for the ext_proc stream to complete assertThat(extProcCompletedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcTrailerCount.get()).isEqualTo(0); + assertThat(sidecarTrailerCount.get()).isEqualTo(0); // Verify status was propagated correctly assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INVALID_ARGUMENT); @@ -4101,9 +4098,9 @@ public void givenResponseTrailerModeSkip_whenCallCloses_thenResponseTrailersNotS String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final AtomicInteger extProcTrailerCount = new AtomicInteger(0); - final CountDownLatch extProcLatch = new CountDownLatch(1); - final CountDownLatch extProcHeadersLatch = new CountDownLatch(1); + final AtomicInteger sidecarTrailerCount = new AtomicInteger(0); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch sidecarHeadersLatch = new CountDownLatch(1); final CountDownLatch extProcCompletedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -4125,17 +4122,17 @@ private void completeResponse() { @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { - extProcTrailerCount.incrementAndGet(); + sidecarTrailerCount.incrementAndGet(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcHeadersLatch.countDown(); + sidecarHeadersLatch.countDown(); } } @@ -4233,12 +4230,12 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Wait for the ext_proc stream to complete assertThat(extProcCompletedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcTrailerCount.get()).isEqualTo(0); + assertThat(sidecarTrailerCount.get()).isEqualTo(0); // Verify status was propagated correctly assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INVALID_ARGUMENT); @@ -4262,7 +4259,7 @@ public void onClose(Status status, Metadata trailers) { String myExtProcServerName = InProcessServerBuilder.generateName(); final AtomicReference capturedResponseHeadersRequest = new AtomicReference<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); class MyExtProcImpl extends io.envoyproxy.envoy.service.ext_proc.v3.ExternalProcessorGrpc .ExternalProcessorImplBase { @@ -4291,7 +4288,7 @@ public void onNext( .build()); } else if (request.hasResponseHeaders()) { capturedResponseHeadersRequest.set(request); - // Ext_proc server mutates the trailers-only headers (which are the trailers) + // Sidecar mutates the trailers-only headers (which are the trailers) responseObserver.onNext( io.envoyproxy.envoy.service.ext_proc.v3.ProcessingResponse.newBuilder() .setResponseHeaders( @@ -4316,7 +4313,7 @@ public void onNext( .build()) .build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onCompleted(); } } @@ -4394,7 +4391,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); ProcessingRequest req = capturedResponseHeadersRequest.get(); @@ -4417,7 +4414,7 @@ public void onClose(Status status, Metadata trailers) { String myExtProcServerName = InProcessServerBuilder.generateName(); final AtomicReference capturedResponseHeadersRequest = new AtomicReference<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); class MyExtProcImpl extends io.envoyproxy.envoy.service.ext_proc.v3.ExternalProcessorGrpc .ExternalProcessorImplBase { @@ -4446,7 +4443,7 @@ public void onNext( .build()); } else if (request.hasResponseHeaders()) { capturedResponseHeadersRequest.set(request); - // Ext_proc server mutates the trailers-only headers (which are the trailers) + // Sidecar mutates the trailers-only headers (which are the trailers) responseObserver.onNext( io.envoyproxy.envoy.service.ext_proc.v3.ProcessingResponse.newBuilder() .setResponseHeaders( @@ -4471,7 +4468,7 @@ public void onNext( .build()) .build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onCompleted(); } } @@ -4549,7 +4546,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); ProcessingRequest req = capturedResponseHeadersRequest.get(); @@ -4572,8 +4569,8 @@ public void onClose(Status status, Metadata trailers) { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final AtomicInteger extProcTrailerCount = new AtomicInteger(0); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final AtomicInteger sidecarTrailerCount = new AtomicInteger(0); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final CountDownLatch extProcCompletedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -4586,14 +4583,14 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasResponseTrailers()) { - extProcTrailerCount.incrementAndGet(); + sidecarTrailerCount.incrementAndGet(); } else if (request.hasResponseHeaders()) { - extProcTrailerCount.incrementAndGet(); + sidecarTrailerCount.incrementAndGet(); } else if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } } @@ -4677,11 +4674,11 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Wait for the ext_proc stream to complete assertThat(extProcCompletedLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcTrailerCount.get()).isEqualTo(0); + assertThat(sidecarTrailerCount.get()).isEqualTo(0); // Verify status was propagated correctly assertThat(capturedStatus.get().getCode()).isEqualTo(Status.Code.INVALID_ARGUMENT); @@ -4716,8 +4713,7 @@ public void givenRequestBodyModeGrpc_whenHalfCloseCalled_thenSuperHalfCloseDefer .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -4767,7 +4763,7 @@ public void onCompleted() { dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( (request, responseObserver) -> { - // Should only be called AFTER ext_proc server response + // Should only be called AFTER sidecar response dataPlaneHalfCloseLatch.countDown(); responseObserver.onNext("Hello"); responseObserver.onCompleted(); @@ -4784,7 +4780,7 @@ public void onCompleted() { proxyCall.halfClose(); - // Verify ext_proc server received end_of_stream_without_message + // Verify sidecar received end_of_stream_without_message assertThat(halfCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Verify main call NOT yet started (data plane server NOT yet reached) @@ -4814,8 +4810,7 @@ public void deferredHalfClose_whenExtProcRespondsWithEosWithoutMessage_thenSuper .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -4894,7 +4889,7 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final List serverReceivedMessages = new CopyOnWriteArrayList<>(); + final List serverReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) .fallbackHandlerRegistry(uniqueRegistry) @@ -4925,8 +4920,8 @@ public void onCompleted() { })) .build()); - final CountDownLatch dataPlaneHalfClosedLatch = - new CountDownLatch(1); + final java.util.concurrent.CountDownLatch dataPlaneHalfClosedLatch = + new java.util.concurrent.CountDownLatch(1); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .intercept(new ClientInterceptor() { @@ -4954,7 +4949,7 @@ public void halfClose() { proxyCall.request(1); proxyCall.halfClose(); - assertThat(dataPlaneHalfClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(dataPlaneHalfClosedLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); assertThat(serverReceivedMessages).containsExactly("mutated1", "mutated2"); proxyCall.cancel("Cleanup", null); @@ -4981,8 +4976,7 @@ public void givenDeferredHalfClose_whenExtProcRespondsWithEndOfStream_thenSuperH .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5051,7 +5045,7 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final List serverReceivedMessages = new CopyOnWriteArrayList<>(); + final List serverReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) .fallbackHandlerRegistry(uniqueRegistry) @@ -5082,8 +5076,8 @@ public void onCompleted() { })) .build()); - final CountDownLatch dataPlaneHalfClosedLatch = - new CountDownLatch(1); + final java.util.concurrent.CountDownLatch dataPlaneHalfClosedLatch = + new java.util.concurrent.CountDownLatch(1); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .intercept(new ClientInterceptor() { @@ -5111,7 +5105,7 @@ public void halfClose() { proxyCall.request(1); proxyCall.halfClose(); - assertThat(dataPlaneHalfClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(dataPlaneHalfClosedLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); assertThat(serverReceivedMessages).containsExactly("mutated1", "mutated2"); proxyCall.cancel("Cleanup", null); @@ -5139,14 +5133,13 @@ public void halfClose() { .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new CopyOnWriteArrayList<>(); + new java.util.concurrent.CopyOnWriteArrayList<>(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -5220,7 +5213,7 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final List serverReceivedMessages = new CopyOnWriteArrayList<>(); + final List serverReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) .fallbackHandlerRegistry(uniqueRegistry) @@ -5251,8 +5244,8 @@ public void onCompleted() { })) .build()); - final CountDownLatch dataPlaneHalfClosedLatch = - new CountDownLatch(1); + final java.util.concurrent.CountDownLatch dataPlaneHalfClosedLatch = + new java.util.concurrent.CountDownLatch(1); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .intercept(new ClientInterceptor() { @@ -5280,7 +5273,7 @@ public void halfClose() { proxyCall.request(1); proxyCall.sendMessage("req1"); - assertThat(dataPlaneHalfClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(dataPlaneHalfClosedLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); assertThat(serverReceivedMessages).containsExactly("mutated1", "mutated2"); // Client app continues to send messages after super half close propagated. @@ -5324,14 +5317,13 @@ public void halfClose() { .setRequestHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new CopyOnWriteArrayList<>(); + new java.util.concurrent.CopyOnWriteArrayList<>(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -5395,7 +5387,7 @@ public void onCompleted() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - final List serverReceivedMessages = new CopyOnWriteArrayList<>(); + final List serverReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); MutableHandlerRegistry uniqueRegistry = new MutableHandlerRegistry(); grpcCleanup.register(InProcessServerBuilder.forName(uniqueDataPlaneServerName) .fallbackHandlerRegistry(uniqueRegistry) @@ -5426,8 +5418,8 @@ public void onCompleted() { })) .build()); - final CountDownLatch dataPlaneHalfClosedLatch = - new CountDownLatch(1); + final java.util.concurrent.CountDownLatch dataPlaneHalfClosedLatch = + new java.util.concurrent.CountDownLatch(1); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(uniqueDataPlaneServerName) .intercept(new ClientInterceptor() { @@ -5455,7 +5447,7 @@ public void halfClose() { proxyCall.request(1); proxyCall.sendMessage("req1"); - assertThat(dataPlaneHalfClosedLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(dataPlaneHalfClosedLatch.await(5, java.util.concurrent.TimeUnit.SECONDS)).isTrue(); assertThat(serverReceivedMessages).containsExactly("mutated1", "mutated2"); // Client app continues to send messages after super half close propagated. @@ -5497,14 +5489,13 @@ public void givenObservabilityTrue_whenExtProcBusy_thenIsReadyReturnsFalse() .build()) .setObservabilityMode(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new CopyOnWriteArrayList<>(); + new java.util.concurrent.CopyOnWriteArrayList<>(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -5586,11 +5577,11 @@ public void onReady() { } }, new Metadata()); - // Wait for activation (ext_proc server needs to respond to headers) + // Wait for activation (sidecar needs to respond to headers) assertThat(readyLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isTrue(); - // Ext_proc server busy + // Sidecar busy sidecarReady.set(false); assertThat(proxyCall.isReady()).isFalse(); @@ -5620,14 +5611,13 @@ public void givenObservabilityMode_whenUpstreamBusy_thenIsReadyReturnsFalse() .build()) .setObservabilityMode(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new CopyOnWriteArrayList<>(); + new java.util.concurrent.CopyOnWriteArrayList<>(); // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -5711,7 +5701,7 @@ public void onReady() { } }, new Metadata()); - // Wait for activation (ext_proc server needs to respond to headers) + // Wait for activation (sidecar needs to respond to headers) assertThat(readyLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isTrue(); @@ -5745,8 +5735,7 @@ public void givenNormalMode_whenUpstreamBusy_thenIsReadyReturnsTrue() .build()) .setObservabilityMode(false) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5833,9 +5822,9 @@ public void onReady() { } }, new Metadata()); - // Wait for activation (ext_proc server needs to respond to headers) + // Wait for activation (sidecar needs to respond to headers) assertThat(readyLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // Since ext_proc server is ready, proxyCall.isReady() should return true, + // Since sidecar is ready, proxyCall.isReady() should return true, // ignoring that upstream is busy assertThat(proxyCall.isReady()).isTrue(); @@ -5859,8 +5848,7 @@ public void givenCongestionInExtProc_whenExtProcBecomesReady_thenTriggersOnReady .build()) .setObservabilityMode(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -5893,7 +5881,7 @@ public void onCompleted() { .directExecutor() .build().start()); - final AtomicReference> extProcListenerRef = + final AtomicReference> sidecarListenerRef = new AtomicReference<>(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( @@ -5907,7 +5895,7 @@ public ClientCall interceptCall( next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - extProcListenerRef.set((Listener) responseListener); + sidecarListenerRef.set((Listener) responseListener); super.start(responseListener, headers); } }; @@ -5942,15 +5930,15 @@ public void onReady() { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(appListener, new Metadata()); - // Wait for ext_proc server call to start and listener to be captured + // Wait for sidecar call to start and listener to be captured long startTime = System.currentTimeMillis(); - while (extProcListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { + while (sidecarListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { Thread.sleep(10); } - assertThat(extProcListenerRef.get()).isNotNull(); + assertThat(sidecarListenerRef.get()).isNotNull(); - // Trigger ext_proc server onReady - extProcListenerRef.get().onReady(); + // Trigger sidecar onReady + sidecarListenerRef.get().onReady(); // Verify app listener notified assertThat(onReadyLatch.await(5, TimeUnit.SECONDS)).isTrue(); @@ -6063,7 +6051,7 @@ public boolean isReady() { proxyCall.start(new ClientCall.Listener() {}, new Metadata()); proxyCall.request(1); - // 3. Wait for the ext_proc server response to complete the external processor stream + // 3. Wait for the sidecar response to complete the external processor stream assertThat(sidecarResponseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // 4. Assert that proxyCall.isReady() delegates directly to the downstream call @@ -6128,8 +6116,8 @@ public void testRequestBodyDrainingBypassedWhenRequestBodyModeNone() throws Exce .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder() .setTargetUri("in-process:///" + uniqueExtProcServerName) .addChannelCredentialsPlugin(Any.newBuilder() - .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service." - + "channel_credentials.insecure.v3.InsecureCredentials") + .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service." + + "channel_credentials.insecure.v3.InsecureCredentials") .build()) .build()) .build()) @@ -6165,7 +6153,8 @@ public void onNext(ProcessingRequest request) { } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override public void onCompleted() {} @@ -6322,8 +6311,7 @@ public void testResponseBodyDrainingBypassedWhenResponseBodyModeNone() throws Ex .setResponseBodyMode(ProcessingMode.BodySendMode.NONE) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6348,7 +6336,8 @@ public void onNext(ProcessingRequest request) { } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override public void onCompleted() { @@ -6449,8 +6438,7 @@ public void testResponseHeadersDrainingBypassedWhenResponseHeadersSkip() throws .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6570,8 +6558,7 @@ public void testResponseTrailersDrainingBypassedWhenResponseTrailersSkip() throw .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6596,7 +6583,8 @@ public void onNext(ProcessingRequest request) { } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override public void onCompleted() { @@ -6689,8 +6677,7 @@ public void givenRequestDrainActive_whenIsReadyCalled_thenReturnsFalse() throws .build()) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6791,8 +6778,7 @@ public void givenDrainingStream_whenExtProcStreamCompletes_thenOnReady() throws .build()) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -6813,14 +6799,18 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { new Thread(() -> { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestDrain(true) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestDrain(true) + .build()); + } sidecarOnNextLatch.countDown(); try { if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { sidecarOnCompletedLatch.countDown(); - responseObserver.onCompleted(); + synchronized (responseObserver) { + responseObserver.onCompleted(); + } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -6884,7 +6874,7 @@ public void onReady() { onReadyLatch.countDown(); } }; - + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); @@ -6953,8 +6943,7 @@ public void givenDrainingStream_whenObserverIsNull_thenSendMessageDoesNotQueue() .build()) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7059,8 +7048,7 @@ public void givenDrainingStream_whenExtProcStreamCompletes_thenMessagesProceed() .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7079,12 +7067,16 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { new Thread(() -> { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestDrain(true) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestDrain(true) + .build()); + } try { if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onCompleted(); + synchronized (responseObserver) { + responseObserver.onCompleted(); + } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -7216,8 +7208,7 @@ public void onMessage(String message) { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7238,13 +7229,17 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { new Thread(() -> { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .setRequestDrain(true) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .setRequestDrain(true) + .build()); + } try { if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onCompleted(); + synchronized (responseObserver) { + responseObserver.onCompleted(); + } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -7294,8 +7289,7 @@ public void onNext(String value) { } @Override - public void onError(Throwable t) { - } + public void onError(Throwable t) {} @Override public void onCompleted() { @@ -7320,7 +7314,7 @@ public void onMessage(String message) { appLatch.countDown(); } }; - + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = interceptCall(interceptor, METHOD_CLIENT_STREAMING, callOptions, dataPlaneChannel); @@ -7384,8 +7378,7 @@ public void drainingStartsAfterRequestHeaders_whenAppSendsAndHalfCloses_thenBuff .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7407,27 +7400,33 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } } else if (request.hasRequestBody()) { extProcReceivedBodyLatch.countDown(); new Thread(() -> { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8("Mutated Message 1")) - .build()) - .build()) - .build()) - .build()) - .setRequestDrain(true) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8("Mutated Message 1")) + .build()) + .build()) + .build()) + .build()) + .setRequestDrain(true) + .build()); + } try { if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onCompleted(); + synchronized (responseObserver) { + responseObserver.onCompleted(); + } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -7578,8 +7577,7 @@ public void drainingStartsBeforeResponseHeaders_whenUpstreamResponds_thenBuffere .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7599,13 +7597,17 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { new Thread(() -> { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .setRequestDrain(true) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .setRequestDrain(true) + .build()); + } try { if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onCompleted(); + synchronized (responseObserver) { + responseObserver.onCompleted(); + } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -7754,8 +7756,7 @@ public void drainingStartsAfterResponseHeaders_whenUpstreamResponds_thenBuffered .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -7784,16 +7785,20 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } reqHeadersLatch.countDown(); } else if (request.hasResponseHeaders()) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } respHeadersLatch.countDown(); } else if (request.hasResponseBody()) { String msgStr = request.getResponseBody().getBody().toStringUtf8(); @@ -7801,27 +7806,29 @@ public void onNext(ProcessingRequest request) { new Thread( () -> { try { - // Wait until M2 is received by ext_proc server so both M1 and M2 + // Wait until M2 is received by sidecar so both M1 and M2 // are in flight if (m2ReceivedLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setResponseBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse - .newBuilder() - .setBody(mutated1) - .build()) - .build()) - .build()) - .build()) - .setRequestDrain(true) - .build()); + synchronized (responseObserver) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse + .newBuilder() + .setBody(mutated1) + .build()) + .build()) + .build()) + .build()) + .setRequestDrain(true) + .build()); + } respBody1Latch.countDown(); } } catch (InterruptedException e) { @@ -7836,27 +7843,31 @@ public void onNext(ProcessingRequest request) { try { // Wait until M3 is sent by upstream concurrently during drain if (m3SentLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setResponseBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse - .newBuilder() - .setBody(mutated2) - .build()) - .build()) - .build()) - .build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setResponseBody( + BodyResponse.newBuilder() + .setResponse( + CommonResponse.newBuilder() + .setBodyMutation( + BodyMutation.newBuilder() + .setStreamedResponse( + StreamedBodyResponse + .newBuilder() + .setBody(mutated2) + .build()) + .build()) + .build()) + .build()) + .build()); + } respBody2Latch.countDown(); } if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { - responseObserver.onCompleted(); + synchronized (responseObserver) { + responseObserver.onCompleted(); + } } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -7868,7 +7879,8 @@ public void onNext(ProcessingRequest request) { } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override public void onCompleted() { @@ -8036,8 +8048,7 @@ public void givenObservabilityTrue_whenExtProcBusy_thenAppRequestsBuffered() .build()) .setObservabilityMode(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8071,7 +8082,7 @@ public void onCompleted() { .build().start()); final AtomicBoolean sidecarReady = new AtomicBoolean(true); - final AtomicReference> extProcListenerRef = + final AtomicReference> sidecarListenerRef = new AtomicReference<>(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( @@ -8085,7 +8096,7 @@ public ClientCall interceptCall( ReqT, RespT>(next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - extProcListenerRef.set((Listener) responseListener); + sidecarListenerRef.set((Listener) responseListener); super.start(responseListener, headers); } @@ -8150,14 +8161,14 @@ public void request(int numMessages) { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - // Wait for ext_proc server call to start + // Wait for sidecar call to start long startTime = System.currentTimeMillis(); - while (extProcListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { + while (sidecarListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { Thread.sleep(10); } - assertThat(extProcListenerRef.get()).isNotNull(); + assertThat(sidecarListenerRef.get()).isNotNull(); - // Ext_proc server is busy + // Sidecar server is busy sidecarReady.set(false); assertThat(proxyCall.isReady()).isFalse(); @@ -8166,9 +8177,9 @@ public void request(int numMessages) { // Verify data plane call NOT requested yet (due to observability mode and ext_proc server busy) assertThat(dataPlaneRequestCount.get()).isEqualTo(0); - // Ext_proc server becomes ready + // Sidecar server becomes ready sidecarReady.set(true); - extProcListenerRef.get().onReady(); + sidecarListenerRef.get().onReady(); // After ext_proc server becomes ready, pending requests should be drained to data plane. assertThat(dataPlaneRequestCount.get()).isEqualTo(5); @@ -8197,8 +8208,7 @@ public void givenRequestDrainActive_whenAppRequestsMessages_thenRequestsBuffered .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8312,8 +8322,7 @@ public void givenBufferedRequests_whenExtProcStreamBecomesReady_thenDataPlaneDra .build()) .setObservabilityMode(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8352,7 +8361,7 @@ public void onCompleted() { .build().start()); final AtomicBoolean sidecarReady = new AtomicBoolean(true); - final AtomicReference> extProcListenerRef = + final AtomicReference> sidecarListenerRef = new AtomicReference<>(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( @@ -8366,7 +8375,7 @@ public ClientCall interceptCall( ReqT, RespT>(next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - extProcListenerRef.set((Listener) responseListener); + sidecarListenerRef.set((Listener) responseListener); super.start(responseListener, headers); } @@ -8418,21 +8427,21 @@ public void request(int numMessages) { // Wait for ext_proc server call to start long startTime = System.currentTimeMillis(); - while (extProcListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { + while (sidecarListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { Thread.sleep(10); } - assertThat(extProcListenerRef.get()).isNotNull(); + assertThat(sidecarListenerRef.get()).isNotNull(); - // Ext_proc server is busy initially + // Sidecar server is busy initially sidecarReady.set(false); // Request from application proxyCall.request(10); assertThat(dataPlaneRequestCount.get()).isEqualTo(0); - // Ext_proc server becomes ready + // Sidecar server becomes ready sidecarReady.set(true); - extProcListenerRef.get().onReady(); + sidecarListenerRef.get().onReady(); // Verify buffered request drained assertThat(dataPlaneRequestCount.get()).isEqualTo(10); @@ -8457,8 +8466,7 @@ public void givenExtProcStreamCompleted_whenAppRequestsMessages_thenRequestsForw .build()) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8550,7 +8558,7 @@ public void onReady() { // Verify request forwarded immediately assertThat(dataPlaneRequestCount.get()).isEqualTo(7); - // proxyCall.isReady() should remain true as ext_proc server is gone + // proxyCall.isReady() should remain true as sidecar is gone assertThat(proxyCall.isReady()).isTrue(); proxyCall.cancel("Cleanup", null); @@ -8651,8 +8659,7 @@ public void onCompleted() { .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8825,8 +8832,7 @@ public void onCompleted() { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8930,8 +8936,7 @@ public void givenFailureModeAllowFalse_whenExtProcStreamFails_thenDataPlaneCallC .build()) .setFailureModeAllow(false) // Fail Closed .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -8951,7 +8956,7 @@ public void onNext(ProcessingRequest request) { // Fail the stream immediately on headers responseObserver.onError( Status.INTERNAL - .withDescription("Simulated ext_proc server failure") + .withDescription("Simulated sidecar failure") .asRuntimeException()); } } @@ -8997,7 +9002,7 @@ public void onClose(Status status, Metadata trailers) { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(appListener, new Metadata()); - // Verify application receives INTERNAL due to ext_proc server failure + // Verify application receives INTERNAL due to sidecar failure assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); assertThat(closedStatus.get().getDescription()).contains("External processor stream failed"); @@ -9022,8 +9027,7 @@ public void givenFailureModeAllowTrue_whenExtProcStreamFails_thenCallFailsOpen() .build()) .setFailureModeAllow(true) // Fail Open .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9041,7 +9045,9 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { new Thread(() -> { - responseObserver.onError(Status.INTERNAL.asRuntimeException()); + synchronized (responseObserver) { + responseObserver.onError(Status.INTERNAL.asRuntimeException()); + } }).start(); } } @@ -9087,19 +9093,16 @@ public ServerCall.Listener interceptCall( } }; - dataPlaneServiceRegistry.addService( - ServerInterceptors.intercept( - ServerServiceDefinition.builder("test.TestService") - .addMethod( - METHOD_SAY_HELLO, - ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello " + request); - responseObserver.onCompleted(); - dataPlaneLatch.countDown(); - })) - .build(), - dataPlaneInterceptor)); + dataPlaneServiceRegistry.addService(ServerInterceptors.intercept( + ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello " + request); + responseObserver.onCompleted(); + dataPlaneLatch.countDown(); + })) + .build(), + dataPlaneInterceptor)); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); @@ -9162,8 +9165,7 @@ public void givenObservabilityMode_whenDataPlaneClosed_thenSidecarCloseIsDeferre .setDeferredCloseTimeout( com.google.protobuf.Duration.newBuilder().setSeconds(10).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9247,7 +9249,7 @@ public void onCompleted() { } assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // At this point, app received onClose, but ext_proc server should NOT be completed yet + // At this point, app received onClose, but sidecar should NOT be completed yet assertThat(sidecarCompletedLatch.getCount()).isEqualTo(1); // Fast forward time to trigger deferred close @@ -9292,8 +9294,7 @@ public void givenUnsupportedCompressionInResponse_whenReceived_thenStreamErrored .setProcessingMode(ProcessingMode.newBuilder() .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9310,24 +9311,28 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder().build()) - .build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder().build()) + .build()) + .build()); + } } else if (request.hasRequestBody()) { - // Simulate ext_proc server sending compressed body mutation (unsupported) - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setGrpcMessageCompressed(true) - .build()) - .build()) - .build()) - .build()) - .build()); + // Simulate sidecar sending compressed body mutation (unsupported) + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setGrpcMessageCompressed(true) + .build()) + .build()) + .build()) + .build()) + .build()); + } } } @@ -9337,7 +9342,11 @@ public void onError(Throwable t) { @Override public void onCompleted() { - new Thread(() -> responseObserver.onCompleted()).start(); + new Thread(() -> { + synchronized (responseObserver) { + responseObserver.onCompleted(); + } + }).start(); } }; } @@ -9401,7 +9410,7 @@ public void onClose(Status status, Metadata trailers) { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(appListener, new Metadata()); - // Wait for ext_proc server to receive headers and filter to activate call + // Wait for sidecar to receive headers and filter to activate call for (int i = 0; i < 5000 && closedLatch.getCount() > 0; i++) { fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); Thread.sleep(1); @@ -9448,8 +9457,7 @@ public void givenUnsupportedCompressionInResponseBody_whenReceived_thenStreamErr .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9491,7 +9499,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()); } else if (request.hasResponseBody()) { - // Simulate ext_proc server sending compressed body mutation (unsupported) for + // Simulate sidecar sending compressed body mutation (unsupported) for // response body responseObserver.onNext( ProcessingResponse.newBuilder() @@ -9513,7 +9521,8 @@ public void onNext(ProcessingRequest request) { } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override public void onCompleted() { @@ -9600,8 +9609,7 @@ public void givenHeaderSendModeDefault_whenProcessing_thenFollowsDefaultBehavior .setResponseHeaderMode(ProcessingMode.HeaderSendMode.DEFAULT) .setResponseTrailerMode(ProcessingMode.HeaderSendMode.DEFAULT).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9876,8 +9884,7 @@ public void givenImmediateResponse_whenReceived_thenDataPlaneCallCancelled() .build()) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -9922,12 +9929,12 @@ public void onCompleted() { .directExecutor() .build().start()); - CachedChannelManager channelManager = - new CachedChannelManager( - config -> { - return grpcCleanup.register( - InProcessChannelBuilder.forName(extProcServerName).directExecutor().build()); - }); + CachedChannelManager channelManager = new CachedChannelManager(config -> { + return grpcCleanup.register( + InProcessChannelBuilder.forName(extProcServerName) + .directExecutor() + .build()); + }); ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); @@ -9968,7 +9975,7 @@ public void onClose(Status status, Metadata trailers) { // Data plane call should NOT have been started as ext_proc server rejected immediately on // headers assertThat(dataPlaneStarted.get()).isFalse(); - + proxyCall.cancel("Cleanup", null); channelManager.close(); } @@ -9989,8 +9996,7 @@ public void givenImmediateResponseAndObservabilityTrue_whenReceived_thenImmediat .build()) .setObservabilityMode(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10101,8 +10107,7 @@ public void givenImmediateResponseDisabled_whenReceivedBeforeActivation_thenSide .build()) .setDisableImmediateResponse(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10180,7 +10185,7 @@ public void onCompleted() { fakeClock.forwardTime(1, TimeUnit.SECONDS); Thread.sleep(1); } - // Verify app listener notified with an error (not the ext_proc server's UNAUTHENTICATED) + // Verify app listener notified with an error (not the sidecar's UNAUTHENTICATED) assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.INTERNAL); @@ -10214,8 +10219,7 @@ public void givenImmediateResponseDisabled_whenReceivedAfterActivation_thenSidec .build()) .setDisableImmediateResponse(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10241,7 +10245,7 @@ public void onNext(ProcessingRequest request) { // 2. Schedule the immediate response to be sent after 2 seconds @SuppressWarnings("unused") - ScheduledFuture unused = + java.util.concurrent.ScheduledFuture unused = fakeClock.getScheduledExecutorService().schedule(() -> { responseObserver.onNext(ProcessingResponse.newBuilder() .setImmediateResponse( @@ -10341,8 +10345,7 @@ public void givenImmediateResponseInTrailers_whenReceived_thenDataPlaneCallStatu .setProcessingMode(ProcessingMode.newBuilder() .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND).build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10359,45 +10362,52 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder().build()) - .build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder().build()) + .build()) + .build()); + } } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder().build()) - .build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder().build()) + .build()) + .build()); + } } else if (request.hasResponseTrailers()) { new Thread(() -> { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setImmediateResponse( - ImmediateResponse.newBuilder() - .setGrpcStatus( - io.envoyproxy.envoy.service.ext_proc.v3.GrpcStatus.newBuilder() - .setStatus(Status.DATA_LOSS.getCode().value()) - .build()) - .setDetails("Ext_proc server detected data loss") - .setHeaders( - io.envoyproxy.envoy.service.ext_proc.v3.HeaderMutation - .newBuilder() - .addSetHeaders( - io.envoyproxy.envoy.config.core.v3.HeaderValueOption - .newBuilder() - .setHeader( - io.envoyproxy.envoy.config.core.v3.HeaderValue - .newBuilder() - .setKey("x-ext-proc-server-extra") - .setValue("true") - .build()) - .build()) - .build()) - .build()) - .build()); - responseObserver.onCompleted(); + synchronized (responseObserver) { + responseObserver.onNext( + ProcessingResponse.newBuilder() + .setImmediateResponse( + ImmediateResponse.newBuilder() + .setGrpcStatus( + io.envoyproxy.envoy.service.ext_proc.v3.GrpcStatus + .newBuilder() + .setStatus(Status.DATA_LOSS.getCode().value()) + .build()) + .setDetails("Sidecar detected data loss") + .setHeaders( + io.envoyproxy.envoy.service.ext_proc.v3.HeaderMutation + .newBuilder() + .addSetHeaders( + io.envoyproxy.envoy.config.core.v3.HeaderValueOption + .newBuilder() + .setHeader( + io.envoyproxy.envoy.config.core.v3.HeaderValue + .newBuilder() + .setKey("x-sidecar-extra") + .setValue("true") + .build()) + .build()) + .build()) + .build()) + .build()); + responseObserver.onCompleted(); + } }).start(); } } @@ -10461,11 +10471,11 @@ public void onCompleted() { assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.DATA_LOSS); - assertThat(closedStatus.get().getDescription()).isEqualTo("Ext_proc server detected data loss"); + assertThat(closedStatus.get().getDescription()).isEqualTo("Sidecar detected data loss"); assertThat( closedTrailers .get() - .get(Metadata.Key.of("x-ext-proc-server-extra", Metadata.ASCII_STRING_MARSHALLER))) + .get(Metadata.Key.of("x-sidecar-extra", Metadata.ASCII_STRING_MARSHALLER))) .isEqualTo("true"); proxyCall.cancel("Cleanup", null); @@ -10502,8 +10512,7 @@ public void givenActiveRpc_whenDataPlaneCallCancelled_thenExtProcStreamIsErrored .build()) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -10601,15 +10610,14 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenIsReadyReturnsFalse( .build()) .setObservabilityMode(false) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = new CopyOnWriteArrayList<>(); - // Ext_proc server server + // Sidecar server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -10625,9 +10633,11 @@ public void onNext(ProcessingRequest request) { new Thread(() -> { if (request.hasRequestHeaders()) { sidecarActionLatch.countDown(); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } } }).start(); } @@ -10638,7 +10648,11 @@ public void onError(Throwable t) { @Override public void onCompleted() { - new Thread(() -> responseObserver.onCompleted()).start(); + new Thread(() -> { + synchronized (responseObserver) { + responseObserver.onCompleted(); + } + }).start(); } }; } @@ -10716,11 +10730,11 @@ public boolean isReady() { } assertThat(proxyCall.isReady()).isTrue(); - // Ext_proc server becomes busy -> proxyCall becomes busy + // Sidecar server becomes busy -> proxyCall becomes busy sidecarReady.set(false); assertThat(proxyCall.isReady()).isFalse(); - // Ext_proc server becomes ready, but Data Plane is busy -> proxyCall is STILL ready because + // Sidecar server becomes ready, but Data Plane is busy -> proxyCall is STILL ready because // Normal Mode sidecarReady.set(true); dataPlaneReady.set(false); @@ -10757,13 +10771,12 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenAppRequestsAreBuffer .build()) .setObservabilityMode(false) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - // Ext_proc server server + // Sidecar server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -10807,7 +10820,11 @@ public void onError(Throwable t) { @Override public void onCompleted() { - new Thread(() -> responseObserver.onCompleted()).start(); + new Thread(() -> { + synchronized (responseObserver) { + responseObserver.onCompleted(); + } + }).start(); } }; } @@ -10818,7 +10835,7 @@ public void onCompleted() { .build().start()); final AtomicBoolean sidecarReady = new AtomicBoolean(true); - final AtomicReference> extProcListenerRef = + final AtomicReference> sidecarListenerRef = new AtomicReference<>(); CachedChannelManager channelManager = new CachedChannelManager(config -> { return grpcCleanup.register( @@ -10832,7 +10849,7 @@ public ClientCall interceptCall( next.newCall(method, callOptions)) { @Override public void start(Listener responseListener, Metadata headers) { - extProcListenerRef.set((Listener) responseListener); + sidecarListenerRef.set((Listener) responseListener); super.start(responseListener, headers); } @@ -10909,16 +10926,16 @@ public void request(int numMessages) { } assertThat(proxyCall.isReady()).isTrue(); - // Ext_proc server busy -> request(5) should be buffered + // Sidecar server busy -> request(5) should be buffered sidecarReady.set(false); proxyCall.request(5); assertThat(dataPlaneRequestCount.get()).isEqualTo(1); // (Only the initial bootstrap request went through) - // Ext_proc server becomes ready -> buffered requests should start draining (pulling next + // Sidecar server becomes ready -> buffered requests should start draining (pulling next // message) sidecarReady.set(true); - extProcListenerRef.get().onReady(); + sidecarListenerRef.get().onReady(); long startTime2 = System.currentTimeMillis(); while (dataPlaneRequestCount.get() < 2 && System.currentTimeMillis() - startTime2 < 5000) { @@ -10995,13 +11012,12 @@ public void givenResponseBodyModeNone_whenExtProcBusy_thenAppRequestsAreNotBuffe .build()) .setObservabilityMode(false) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - // Ext_proc server server + // Sidecar server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -11016,9 +11032,11 @@ public void onNext(ProcessingRequest request) { new Thread(() -> { if (request.hasRequestHeaders()) { sidecarActionLatch.countDown(); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } } }).start(); } @@ -11121,7 +11139,7 @@ public void request(int numMessages) { } assertThat(proxyCall.isReady()).isTrue(); - // Ext_proc server busy + // Sidecar server busy sidecarReady.set(false); // Since responseBodyMode is NONE and not in observabilityMode, request(5) should @@ -11158,14 +11176,13 @@ public void testFlowControlStateInitialization() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(2); + final CountDownLatch sidecarLatch = new CountDownLatch(2); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -11177,7 +11194,7 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { receivedRequests.add(request); - extProcLatch.countDown(); + sidecarLatch.countDown(); if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) @@ -11241,7 +11258,7 @@ public void onCompleted() { proxyCall.start(new ClientCall.Listener() {}, new Metadata()); proxyCall.sendMessage("Message 1"); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(receivedRequests).hasSize(2); ProcessingRequest firstRequest = receivedRequests.get(0); @@ -11280,8 +11297,7 @@ public void testDownstreamToSidestreamFlowControl_EnforcesWindow() throws Except .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SKIP) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -11508,14 +11524,13 @@ public void testUpstreamToSidestreamFlowControl_EnforcesWindow() throws Exceptio .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(4); + final CountDownLatch sidecarLatch = new CountDownLatch(4); // (Headers, Request Body, Response Headers, Response Body 1) final CountDownLatch secondResponseBodyLatch = new CountDownLatch(1); final AtomicReference> @@ -11532,7 +11547,7 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { receivedRequests.add(request); - extProcLatch.countDown(); + sidecarLatch.countDown(); if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) @@ -11659,7 +11674,7 @@ public void onMessage(String message) { // Wait for the initialization (headers, request body, response headers) to reach the ext_proc // server - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Upstream sends 70k response chunk. Since window is 65,536, this drives the window // negative (-4,464). @@ -11678,7 +11693,7 @@ public void onMessage(String message) { assertThat(secondResponseBodyLatch.getCount()).isEqualTo(1); assertThat(appReceivedMessages).hasSize(2); - // Ext_proc server sends a ServerWindowUpdate of 40k to the filter, unblocking the window. + // Sidecar server sends a ServerWindowUpdate of 40k to the filter, unblocking the window. responseObserverRef.get().onNext(ProcessingResponse.newBuilder() .setServerWindowUpdate(ProcessingResponse.ServerWindowUpdate.newBuilder() .setWindowIncrementUpstreamToSidestream(40000) @@ -11718,8 +11733,7 @@ public void testSidestreamToDownstreamFlowControl_Violations() throws Exception .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -11904,8 +11918,7 @@ public void testSidestreamToUpstreamFlowControl_Violations() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -12303,7 +12316,7 @@ public void testSidestreamToUpstreamFlowControl_DelayedHalfClose() throws Except assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final List serverReceivedBodies = new CopyOnWriteArrayList<>(); final CountDownLatch serverReceivedLatch = new CountDownLatch(1); @@ -12340,7 +12353,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } } @@ -12451,7 +12464,7 @@ public ClientCall interceptCall( // Send the first client message. This gets mutated to "Mutated1" by ext_proc. proxyCall.sendMessage("OriginalRequest 1"); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Since transportReady is false, the mutated body is queued in pendingUpstreamBodyMessages. // And since it was unilateral half-close, pendingUpstreamHalfClose is set to true. @@ -12504,7 +12517,7 @@ public void testSidestreamToUpstreamFlowControl_FailOpenDuringDelayedHalfClose() assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final AtomicReference> responseObserverRef = new AtomicReference<>(); @@ -12543,7 +12556,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } } @@ -12663,7 +12676,7 @@ public ClientCall interceptCall( // Call halfClose immediately. This sets pendingHalfClose = true. proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Since transportReady is false, the mutated body is queued. // And since it was unilateral half-close, pendingUpstreamHalfClose is set to true. @@ -13122,14 +13135,13 @@ public void testThresholdBasedWindowUpdates() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final List> observers = new ArrayList<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -13148,7 +13160,7 @@ public void onNext(ProcessingRequest request) { ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasRequestBody()) { responseObserver.onNext( ProcessingResponse.newBuilder() @@ -13222,7 +13234,7 @@ public void onCompleted() {} proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(filterClientRequests(receivedRequests)).hasSize(1); assertThat(filterClientRequests(receivedRequests).get(0).hasRequestHeaders()).isTrue(); @@ -13312,7 +13324,7 @@ public void testWindowUpdateWithheldWhenUpstreamCapacityExistsAndBelowThreshold( ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(2); + final CountDownLatch sidecarLatch = new CountDownLatch(2); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -13329,7 +13341,7 @@ public void onNext(ProcessingRequest request) { ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasRequestBody()) { // Mutate request body and send back 10000 bytes (below threshold 32768) responseObserver.onNext( @@ -13349,7 +13361,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } } @@ -13416,7 +13428,7 @@ public void onCompleted() { String body10k = new String(new char[10000]).replace('\0', 'a'); proxyCall.sendMessage(body10k); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Since the window has capacity (65536 - 10000 = 55536 > 0) and the increment (10000) // is below the threshold, NO window update should be sent. @@ -13610,7 +13622,7 @@ public void testWindowUpdateWithheldOnUpstreamWindowExhaustionWithZeroIncrement( final List receivedRequests = Collections.synchronizedList(new ArrayList<>()); - final CountDownLatch extProcLatch = new CountDownLatch(2); + final CountDownLatch sidecarLatch = new CountDownLatch(2); final CountDownLatch responseBodyProcessedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -13628,9 +13640,9 @@ public void onNext(ProcessingRequest request) { ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasRequestBody()) { - extProcLatch.countDown(); + sidecarLatch.countDown(); // Mutate request body and send back 70000 bytes (exhausts upstream return // window). responseObserver.onNext( @@ -13770,7 +13782,7 @@ public boolean isReady() { String body10k = new String(new char[10000]).replace('\0', 'a'); proxyCall.sendMessage(body10k); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(receivedRequests).hasSize(2); // Headers + RequestBody // Request 1 response body message to deliver. @@ -13816,7 +13828,7 @@ public void testWindowUpdateWithheldOnDownstreamWindowExhaustionWithZeroIncremen final List receivedRequests = Collections.synchronizedList(new ArrayList<>()); - final CountDownLatch extProcLatch = new CountDownLatch(1); // Headers + final CountDownLatch sidecarLatch = new CountDownLatch(1); // Headers final CountDownLatch responseBodyReceivedLatch = new CountDownLatch(1); final CountDownLatch requestBodyProcessedLatch = new CountDownLatch(1); @@ -13835,7 +13847,7 @@ public void onNext(ProcessingRequest request) { ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasResponseBody()) { // Mutate response body and send back 70000 bytes (exhausts downstream return // window). @@ -13975,7 +13987,7 @@ public boolean isReady() { proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); // 1. Manually trigger onMessage on the client listener. // Since proxyCall.request() was never called, downstreamRequestsPending is 0. @@ -14230,14 +14242,13 @@ public void testHalfClosePiggybacking() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -14253,7 +14264,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasRequestBody()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() @@ -14321,7 +14332,7 @@ public void onCompleted() {} proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(filterClientRequests(receivedRequests)).hasSize(1); proxyCall.sendMessage("Last Message"); @@ -14367,14 +14378,13 @@ public void testPiggybackingOnRequestBody() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -14390,7 +14400,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasRequestBody()) { // Mutate Msg 1 to be 15 bytes if (request.getRequestBody().getBody().toStringUtf8().equals("Msg 1")) { @@ -14494,7 +14504,7 @@ public void onCompleted() {} proxyCall.start(new ClientCall.Listener() {}, new Metadata()); proxyCall.request(10); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(filterClientRequests(receivedRequests)).hasSize(1); // Headers request // Send Msg 1 (5 bytes). It is processed by ext_proc server and mutated to 15 bytes. @@ -14589,14 +14599,13 @@ public void testPiggybackingOnResponseBody() throws Exception { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -14612,7 +14621,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasRequestBody()) { // Mutate Msg 1 to be 15 bytes responseObserver.onNext(ProcessingResponse.newBuilder() @@ -14702,7 +14711,7 @@ public void onCompleted() {} proxyCall.start(new ClientCall.Listener() {}, new Metadata()); proxyCall.request(10); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(filterClientRequests(receivedRequests)).hasSize(1); // Headers request // Send Msg 1 (5 bytes) from app. It is processed by ext_proc server and mutated to 15 bytes. @@ -14810,14 +14819,13 @@ public void testClientWindowUpdateDeferredUntilRequestBodySendMessage() throws E .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(2); // Headers + Request Body + final CountDownLatch sidecarLatch = new CountDownLatch(2); // Headers + Request Body final AtomicReference> responseObserverRef = new AtomicReference<>(); @@ -14833,14 +14841,14 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { receivedRequests.add(request); if (request.hasRequestHeaders()) { - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasRequestBody()) { // Mutate request body and send back 40000 bytes. This triggers client window // update replenishment. - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() .setResponse(CommonResponse.newBuilder() @@ -14943,7 +14951,7 @@ public void sendMessage(ReqT message) { // Wait until interceptor's super.sendMessage() enters the custom interceptor and blocks assertThat(sendMessageEnteredLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Wait a brief moment to make sure no standalone ClientWindowUpdate is sent while blocked Thread.sleep(200); @@ -14987,14 +14995,13 @@ public void testClientWindowUpdateDeferredUntilResponseBodyOnMessage() throws Ex .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(3); + final CountDownLatch sidecarLatch = new CountDownLatch(3); // (Request Headers, Response Headers, Response Body 1) final AtomicReference> responseObserverRef = new AtomicReference<>(); @@ -15010,7 +15017,7 @@ public StreamObserver process( @Override public void onNext(ProcessingRequest request) { receivedRequests.add(request); - extProcLatch.countDown(); + sidecarLatch.countDown(); if (request.hasRequestHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) @@ -15122,7 +15129,7 @@ public void onMessage(String message) { // Wait until client app's onMessage enters and blocks assertThat(onMessageEnteredLatch.await(5, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Wait a brief moment to make sure no standalone ClientWindowUpdate is sent while blocked Thread.sleep(200); @@ -15173,14 +15180,13 @@ public void testClientWindowUpdateSentImmediatelyOnSidestreamToUpstreamWindowExh .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; final List receivedRequests = new CopyOnWriteArrayList<>(); - final CountDownLatch extProcLatch = new CountDownLatch(2); // Headers + Request Body + final CountDownLatch sidecarLatch = new CountDownLatch(2); // Headers + Request Body final CountDownLatch windowUpdateLatch = new CountDownLatch(1); // Window Update final AtomicReference> responseObserverRef = new AtomicReference<>(); @@ -15197,12 +15203,12 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { receivedRequests.add(request); if (request.hasRequestHeaders()) { - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } else if (request.hasRequestBody()) { - extProcLatch.countDown(); + sidecarLatch.countDown(); // Mutate request body and send back 20000 bytes. responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestBody(BodyResponse.newBuilder() @@ -15234,7 +15240,8 @@ public void onNext(ProcessingRequest request) { } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override public void onCompleted() { @@ -15334,7 +15341,7 @@ public boolean isReady() { String body10k = new String(new char[10000]).replace('\0', 'a'); proxyCall.sendMessage(body10k); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Since the window is exhausted but isReady() is false, no window update should be sent. assertThat(receivedRequests).hasSize(2); @@ -15478,7 +15485,8 @@ public void onNext(ProcessingRequest request) { } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override public void onCompleted() { @@ -15505,9 +15513,8 @@ public void onCompleted() { .build()); }); - ExternalProcessorClientInterceptor interceptor = - new ExternalProcessorClientInterceptor( - filterConfig, channelManager, scheduler, FAKE_CONTEXT); + ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( + filterConfig, channelManager, scheduler, FAKE_CONTEXT); final AtomicReference> dataPlaneResponseObserverRef = new AtomicReference<>(); @@ -15629,8 +15636,7 @@ public void givenClientStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveM .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -15883,7 +15889,7 @@ public void onCompleted() { requestObserver.onCompleted(); if (!sidecarActionLatch.await(10, TimeUnit.SECONDS)) { - throw new AssertionError("Ext_proc server actions failed. Received: " + receivedPhases); + throw new AssertionError("Sidecar actions failed. Received: " + receivedPhases); } assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); if (errorRef.get() != null) { @@ -15928,8 +15934,7 @@ public void givenBidiStreamingRpc_whenExtProcMutatesAll_thenAllTargetsReceiveMut .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -16018,8 +16023,8 @@ public void onNext(ProcessingRequest request) { BodyMutation.newBuilder() .setStreamedResponse( StreamedBodyResponse.newBuilder() - .setBody(ByteString.copyFromUtf8( - "MutatedBidiReq")) + .setBody( + ByteString.copyFromUtf8("MutatedBidiReq")) .build()) .build()) .build()) @@ -16191,7 +16196,7 @@ public void onCompleted() { bidiRequestObserver.onCompleted(); if (!sidecarBidiLatch.await(10, TimeUnit.SECONDS)) { - throw new AssertionError("Ext_proc server bidi actions failed. Received: " + receivedPhases); + throw new AssertionError("Sidecar bidi actions failed. Received: " + receivedPhases); } assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); if (errorRef.get() != null) { @@ -16228,7 +16233,7 @@ public void onCompleted() { final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -16244,7 +16249,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -16336,7 +16341,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); List headerNames = new ArrayList<>(); @@ -16359,7 +16364,7 @@ public void onClose(Status status, Metadata trailers) { final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -16379,7 +16384,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } } @@ -16480,7 +16485,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); List headerNames = new ArrayList<>(); @@ -16502,7 +16507,7 @@ public void givenDisallowedHeaders_whenHeadersForwarded_thenSkipped() throws Exc final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -16518,7 +16523,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -16602,7 +16607,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); List headerNames = new ArrayList<>(); @@ -16624,7 +16629,7 @@ public void givenBothRules_whenHeadersForwarded_thenBothAreApplied() throws Exce final AtomicReference capturedHeaders = new AtomicReference<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -16640,7 +16645,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -16727,7 +16732,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); List headerNames = new ArrayList<>(); @@ -16783,7 +16788,7 @@ public void givenRequestAttributes_whenHeaderPhase_thenAttributesSent() throws E .build(); final AtomicReference capturedRequest = new AtomicReference<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final CountDownLatch callLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -16800,7 +16805,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -16862,7 +16867,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(5, TimeUnit.SECONDS)).isTrue(); ProcessingRequest request = capturedRequest.get(); @@ -16890,7 +16895,7 @@ public void givenMetadataAttributes_whenHeadersPresent_thenAttributesSent() thro .build(); final AtomicReference capturedRequest = new AtomicReference<>(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final CountDownLatch callLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -16907,7 +16912,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) @@ -16978,7 +16983,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(callLatch.await(5, TimeUnit.SECONDS)).isTrue(); ProcessingRequest request = capturedRequest.get(); @@ -17005,7 +17010,7 @@ public void givenOutOfOrderReqResponses_whenMessageArrivesBeforeHeaders_thenFail String uniqueExtProcServerName = InProcessServerBuilder.generateName(); String uniqueDataPlaneServerName = InProcessServerBuilder.generateName(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; @@ -17030,7 +17035,7 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onCompleted(); // Complete stream to allow cleanup } } @@ -17097,7 +17102,7 @@ public void onCompleted() { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // The call should fail with INTERNAL status @@ -17113,7 +17118,7 @@ public void givenUnexpectedResponseHeaders_whenHeadersArriveBeforeServerHeaders_ throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -17130,7 +17135,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onCompleted(); } } @@ -17160,8 +17165,7 @@ public void onCompleted() { .setResponseHeaderMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -17205,7 +17209,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // The call should fail with INTERNAL status due to protocol error @@ -17225,7 +17229,7 @@ public void givenUnexpectedResponseTrailers_whenTrailersArriveBeforeServerTraile throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -17242,7 +17246,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseTrailers(TrailersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onCompleted(); } } @@ -17272,8 +17276,7 @@ public void onCompleted() { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -17317,7 +17320,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // The call should fail with INTERNAL status due to protocol error @@ -17337,7 +17340,7 @@ public void givenOutOfOrderRespResponses_whenResponseBodyArrivesBeforeResponseHe throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); final AtomicReference extProcError = new AtomicReference<>(); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -17359,7 +17362,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseBody(BodyResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onCompleted(); } } @@ -17392,8 +17395,7 @@ public void onCompleted() { .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -17437,7 +17439,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.sendMessage("test"); proxyCall.halfClose(); - assertThat(extProcLatch.await(5, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(5, TimeUnit.SECONDS)).isTrue(); // The call should fail with INTERNAL status due to protocol error @@ -17456,7 +17458,7 @@ public void onClose(Status status, Metadata trailers) { public void givenValidOrder_whenResponsesArriveInOrder_thenSucceeds() throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -17471,7 +17473,7 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } } @@ -17546,7 +17548,7 @@ public void onClose(Status status, Metadata trailers) { proxyCall.halfClose(); // Verify that headers are processed correctly and the ordering check passes - assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Verify that the call completes successfully assertThat(callLatch.await(10, TimeUnit.SECONDS)).isTrue(); @@ -17750,8 +17752,8 @@ public void givenRequestHeadersResponse_whenStatusIsContinueAndReplace_thenFails throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch extProcLatch = new CountDownLatch(1); - final CountDownLatch extProcFinishedLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch sidecarFinishedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -17770,19 +17772,19 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onCompleted(); } } @Override public void onError(Throwable t) { - extProcFinishedLatch.countDown(); + sidecarFinishedLatch.countDown(); } @Override public void onCompleted() { - extProcFinishedLatch.countDown(); + sidecarFinishedLatch.countDown(); responseObserver.onCompleted(); } }; @@ -17841,8 +17843,8 @@ public void onCompleted() { // ignore } - assertThat(extProcLatch.await(30, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(30, TimeUnit.SECONDS)).isTrue(); // Call should succeed due to fail-open @@ -17856,8 +17858,8 @@ public void givenResponseHeadersResponse_whenStatusIsContinueAndReplace_thenFail throws Exception { String uniqueExtProcServerName = InProcessServerBuilder.generateName(); - final CountDownLatch extProcLatch = new CountDownLatch(1); - final CountDownLatch extProcFinishedLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); + final CountDownLatch sidecarFinishedLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -17880,19 +17882,19 @@ public void onNext(ProcessingRequest request) { .build()) .build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); responseObserver.onCompleted(); } } @Override public void onError(Throwable t) { - extProcFinishedLatch.countDown(); + sidecarFinishedLatch.countDown(); } @Override public void onCompleted() { - extProcFinishedLatch.countDown(); + sidecarFinishedLatch.countDown(); responseObserver.onCompleted(); } }; @@ -17954,8 +17956,8 @@ public void onCompleted() { // ignore } - assertThat(extProcLatch.await(30, TimeUnit.SECONDS)).isTrue(); - assertThat(extProcFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(30, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarFinishedLatch.await(30, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(30, TimeUnit.SECONDS)).isTrue(); // The call should succeed due to fail-open @@ -17967,8 +17969,8 @@ public void onCompleted() { @Test public void givenExtProcCall_whenExecutionSucceeds_thenAll4MetricsAreRecorded() throws Exception { final String uniqueExtProcServerName = "ext-proc-server-metrics-" + java.util.UUID.randomUUID(); - final CountDownLatch extProcRequestHeadersLatch = new CountDownLatch(1); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarRequestHeadersLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); // In-process mock server for External Processor ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -17985,12 +17987,12 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcRequestHeadersLatch.countDown(); + sidecarRequestHeadersLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } } @@ -18077,13 +18079,13 @@ public void onCompleted() { proxyCall.halfClose(); // 1. Wait for mock Ext Proc to receive and process client request headers - assertThat(extProcRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); // 2. Release the data plane server to respond back to the client call dataPlaneLatch.countDown(); // 3. Assert that all stages complete in sequence deterministically - assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); // Clean up and close the Ext Proc stream to release in-process server/channel resources cleanly @@ -18122,8 +18124,8 @@ public void onCompleted() { public void givenExtProcCall_whenExecutionFails_thenAll4MetricsAreRecorded() throws Exception { final String uniqueExtProcServerName = "ext-proc-server-metrics-fail-" + java.util.UUID.randomUUID(); - final CountDownLatch extProcRequestHeadersLatch = new CountDownLatch(1); - final CountDownLatch extProcLatch = new CountDownLatch(1); + final CountDownLatch sidecarRequestHeadersLatch = new CountDownLatch(1); + final CountDownLatch sidecarLatch = new CountDownLatch(1); // In-process mock server for External Processor ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = @@ -18140,12 +18142,12 @@ public void onNext(ProcessingRequest request) { responseObserver.onNext(ProcessingResponse.newBuilder() .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcRequestHeadersLatch.countDown(); + sidecarRequestHeadersLatch.countDown(); } else if (request.hasResponseHeaders()) { responseObserver.onNext(ProcessingResponse.newBuilder() .setResponseHeaders(HeadersResponse.newBuilder().build()) .build()); - extProcLatch.countDown(); + sidecarLatch.countDown(); } } @@ -18245,13 +18247,13 @@ public ServerCall.Listener interceptCall( proxyCall.halfClose(); // 1. Wait for mock Ext Proc to receive and process client request headers - assertThat(extProcRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarRequestHeadersLatch.await(10, TimeUnit.SECONDS)).isTrue(); // 2. Release the data plane server to respond back with error dataPlaneLatch.countDown(); // 3. Assert that all stages complete - assertThat(extProcLatch.await(10, TimeUnit.SECONDS)).isTrue(); + assertThat(sidecarLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appCloseLatch.await(10, TimeUnit.SECONDS)).isTrue(); assertThat(appStatus.get().getCode()).isEqualTo(Status.Code.UNAUTHENTICATED); @@ -18299,8 +18301,7 @@ public ServerCall.Listener interceptCall( ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) .setFailureModeAllow(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -18403,8 +18404,7 @@ public void onClose(Status status, Metadata trailers) { ExternalProcessor proto = createBaseProto(uniqueExtProcServerName) .setFailureModeAllow(false) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -18508,8 +18508,7 @@ public void givenFailureModeAllowTrue_whenExtProcStreamFailsAfterRequestBodySent .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -18627,8 +18626,7 @@ public void givenFailureModeAllowTrue_whenExtProcStreamFailsAfterResponseBodySen .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -18747,8 +18745,7 @@ public void givenObservabilityTrue_whenExtProcStreamFails_thenCallContinues() .setFailureModeAllow(false) .setObservabilityMode(true) .build(); - ConfigOrError - configOrError = + ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); assertThat(configOrError.errorDetail).isNull(); ExternalProcessorFilterConfig filterConfig = configOrError.config; @@ -18767,7 +18764,7 @@ public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { // Fail the stream immediately on receiving headers responseObserver.onError( - Status.INTERNAL.withDescription("Simulated ext_proc server failure") + Status.INTERNAL.withDescription("Simulated sidecar failure") .asRuntimeException()); } } From 1a0d210e03c787e6b0f01da3c35fec6a77de9722 Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 30 Jul 2026 06:32:36 +0000 Subject: [PATCH 12/13] Merge. --- .../grpc/xds/ExternalProcessorClientInterceptorTest.java | 8 -------- 1 file changed, 8 deletions(-) diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index da78b6f15df..af46866b0d9 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -8562,10 +8562,6 @@ public void givenRequestDrainActive_whenAppRequestsMessages_thenRequestsBuffered .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) .build(); ConfigOrError configOrError = provider.parseFilterConfig(Any.pack(proto), filterContext); @@ -11122,10 +11118,6 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenAppRequestsAreBuffer .build()) .build()) .build()) - .setProcessingMode(ProcessingMode.newBuilder() - .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) - .setResponseTrailerMode(ProcessingMode.HeaderSendMode.SEND) - .build()) .setProcessingMode(ProcessingMode.newBuilder() .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC) .setResponseBodyMode(ProcessingMode.BodySendMode.GRPC) From 98ec5beee5d006021ada2504a3234edcb2313cbd Mon Sep 17 00:00:00 2001 From: Kannan J Date: Thu, 30 Jul 2026 08:25:18 +0000 Subject: [PATCH 13/13] Revert nit changes causing diffs. Revert nit changes causing diffs. Revert nit changes causing diffs. Revert nit changes causing diffs. --- .../ext_proc/v3/ExternalProcessorGrpc.java | 286 +++++----- ...xternalProcessorClientInterceptorTest.java | 515 ++++++++---------- 2 files changed, 348 insertions(+), 453 deletions(-) diff --git a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/ext_proc/v3/ExternalProcessorGrpc.java b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/ext_proc/v3/ExternalProcessorGrpc.java index fc3ce3a2723..20064af7844 100644 --- a/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/ext_proc/v3/ExternalProcessorGrpc.java +++ b/xds/src/generated/thirdparty/grpc/io/envoyproxy/envoy/service/ext_proc/v3/ExternalProcessorGrpc.java @@ -4,31 +4,25 @@ /** *
- * A service that can access and modify HTTP requests and responses
- * as part of a filter chain.
+ * A service that can access and modify HTTP requests and responses as part of a filter chain.
  * The overall external processing protocol works like this:
  * 1. The data plane sends to the service information about the HTTP request.
- * 2. The service sends back a ProcessingResponse message that directs
- *    the data plane to either stop processing, continue without it, or send
- *    it the next chunk of the message body.
- * 3. If so requested, the data plane sends the server the message body in
- *    chunks, or the entire body at once. In either case, the server may send
- *    back a ProcessingResponse for each message it receives, or wait for
- *    a certain amount of body chunks received before streaming back the
- *    ProcessingResponse messages.
- * 4. If so requested, the data plane sends the server the HTTP trailers,
- *    and the server sends back a ProcessingResponse.
- * 5. At this point, request processing is done, and we pick up again
- *    at step 1 when the data plane receives a response from the upstream
- *    server.
- * 6. At any point above, if the server closes the gRPC stream cleanly,
- *    then the data plane proceeds without consulting the server.
- * 7. At any point above, if the server closes the gRPC stream with an error,
- *    then the data plane returns a 500 error to the client, unless the filter
- *    was configured to ignore errors.
- * In other words, the process is a request/response conversation, but
- * using a gRPC stream to make it easier for the server to
- * maintain state.
+ * 2. The service sends back a ``ProcessingResponse`` message that directs the data plane to either
+ *    stop processing, continue without it, or send it the next chunk of the message body.
+ * 3. If so requested, the data plane sends the server the message body in chunks, or the entire
+ *    body at once. In either case, the server may send back a ``ProcessingResponse`` for each
+ *    message it receives, or wait for a certain amount of body chunks to be received before
+ *    streaming back the ``ProcessingResponse`` messages.
+ * 4. If so requested, the data plane sends the server the HTTP trailers, and the server sends back
+ *    a ``ProcessingResponse``.
+ * 5. At this point, request processing is done, and we pick up again at step 1 when the data plane
+ *    receives a response from the upstream server.
+ * 6. At any point above, if the server closes the gRPC stream cleanly, then the data plane
+ *    proceeds without consulting the server.
+ * 7. At any point above, if the server closes the gRPC stream with an error, then the data plane
+ *    returns a ``500`` error to the client, unless the filter was configured to ignore errors.
+ * In other words, the process is a request/response conversation, but using a gRPC stream to make
+ * it easier for the server to maintain state.
  * 
*/ @io.grpc.stub.annotations.GrpcGenerated @@ -131,31 +125,25 @@ public ExternalProcessorFutureStub newStub(io.grpc.Channel channel, io.grpc.Call /** *
-   * A service that can access and modify HTTP requests and responses
-   * as part of a filter chain.
+   * A service that can access and modify HTTP requests and responses as part of a filter chain.
    * The overall external processing protocol works like this:
    * 1. The data plane sends to the service information about the HTTP request.
-   * 2. The service sends back a ProcessingResponse message that directs
-   *    the data plane to either stop processing, continue without it, or send
-   *    it the next chunk of the message body.
-   * 3. If so requested, the data plane sends the server the message body in
-   *    chunks, or the entire body at once. In either case, the server may send
-   *    back a ProcessingResponse for each message it receives, or wait for
-   *    a certain amount of body chunks received before streaming back the
-   *    ProcessingResponse messages.
-   * 4. If so requested, the data plane sends the server the HTTP trailers,
-   *    and the server sends back a ProcessingResponse.
-   * 5. At this point, request processing is done, and we pick up again
-   *    at step 1 when the data plane receives a response from the upstream
-   *    server.
-   * 6. At any point above, if the server closes the gRPC stream cleanly,
-   *    then the data plane proceeds without consulting the server.
-   * 7. At any point above, if the server closes the gRPC stream with an error,
-   *    then the data plane returns a 500 error to the client, unless the filter
-   *    was configured to ignore errors.
-   * In other words, the process is a request/response conversation, but
-   * using a gRPC stream to make it easier for the server to
-   * maintain state.
+   * 2. The service sends back a ``ProcessingResponse`` message that directs the data plane to either
+   *    stop processing, continue without it, or send it the next chunk of the message body.
+   * 3. If so requested, the data plane sends the server the message body in chunks, or the entire
+   *    body at once. In either case, the server may send back a ``ProcessingResponse`` for each
+   *    message it receives, or wait for a certain amount of body chunks to be received before
+   *    streaming back the ``ProcessingResponse`` messages.
+   * 4. If so requested, the data plane sends the server the HTTP trailers, and the server sends back
+   *    a ``ProcessingResponse``.
+   * 5. At this point, request processing is done, and we pick up again at step 1 when the data plane
+   *    receives a response from the upstream server.
+   * 6. At any point above, if the server closes the gRPC stream cleanly, then the data plane
+   *    proceeds without consulting the server.
+   * 7. At any point above, if the server closes the gRPC stream with an error, then the data plane
+   *    returns a ``500`` error to the client, unless the filter was configured to ignore errors.
+   * In other words, the process is a request/response conversation, but using a gRPC stream to make
+   * it easier for the server to maintain state.
    * 
*/ public interface AsyncService { @@ -164,7 +152,7 @@ public interface AsyncService { *
      * This begins the bidirectional stream that the data plane will use to
      * give the server control over what the filter does. The actual
-     * protocol is described by the ProcessingRequest and ProcessingResponse
+     * protocol is described by the ``ProcessingRequest`` and ``ProcessingResponse``
      * messages below.
      * 
*/ @@ -177,31 +165,25 @@ default io.grpc.stub.StreamObserver - * A service that can access and modify HTTP requests and responses - * as part of a filter chain. + * A service that can access and modify HTTP requests and responses as part of a filter chain. * The overall external processing protocol works like this: * 1. The data plane sends to the service information about the HTTP request. - * 2. The service sends back a ProcessingResponse message that directs - * the data plane to either stop processing, continue without it, or send - * it the next chunk of the message body. - * 3. If so requested, the data plane sends the server the message body in - * chunks, or the entire body at once. In either case, the server may send - * back a ProcessingResponse for each message it receives, or wait for - * a certain amount of body chunks received before streaming back the - * ProcessingResponse messages. - * 4. If so requested, the data plane sends the server the HTTP trailers, - * and the server sends back a ProcessingResponse. - * 5. At this point, request processing is done, and we pick up again - * at step 1 when the data plane receives a response from the upstream - * server. - * 6. At any point above, if the server closes the gRPC stream cleanly, - * then the data plane proceeds without consulting the server. - * 7. At any point above, if the server closes the gRPC stream with an error, - * then the data plane returns a 500 error to the client, unless the filter - * was configured to ignore errors. - * In other words, the process is a request/response conversation, but - * using a gRPC stream to make it easier for the server to - * maintain state. + * 2. The service sends back a ``ProcessingResponse`` message that directs the data plane to either + * stop processing, continue without it, or send it the next chunk of the message body. + * 3. If so requested, the data plane sends the server the message body in chunks, or the entire + * body at once. In either case, the server may send back a ``ProcessingResponse`` for each + * message it receives, or wait for a certain amount of body chunks to be received before + * streaming back the ``ProcessingResponse`` messages. + * 4. If so requested, the data plane sends the server the HTTP trailers, and the server sends back + * a ``ProcessingResponse``. + * 5. At this point, request processing is done, and we pick up again at step 1 when the data plane + * receives a response from the upstream server. + * 6. At any point above, if the server closes the gRPC stream cleanly, then the data plane + * proceeds without consulting the server. + * 7. At any point above, if the server closes the gRPC stream with an error, then the data plane + * returns a ``500`` error to the client, unless the filter was configured to ignore errors. + * In other words, the process is a request/response conversation, but using a gRPC stream to make + * it easier for the server to maintain state. * */ public static abstract class ExternalProcessorImplBase @@ -215,31 +197,25 @@ public static abstract class ExternalProcessorImplBase /** * A stub to allow clients to do asynchronous rpc calls to service ExternalProcessor. *
-   * A service that can access and modify HTTP requests and responses
-   * as part of a filter chain.
+   * A service that can access and modify HTTP requests and responses as part of a filter chain.
    * The overall external processing protocol works like this:
    * 1. The data plane sends to the service information about the HTTP request.
-   * 2. The service sends back a ProcessingResponse message that directs
-   *    the data plane to either stop processing, continue without it, or send
-   *    it the next chunk of the message body.
-   * 3. If so requested, the data plane sends the server the message body in
-   *    chunks, or the entire body at once. In either case, the server may send
-   *    back a ProcessingResponse for each message it receives, or wait for
-   *    a certain amount of body chunks received before streaming back the
-   *    ProcessingResponse messages.
-   * 4. If so requested, the data plane sends the server the HTTP trailers,
-   *    and the server sends back a ProcessingResponse.
-   * 5. At this point, request processing is done, and we pick up again
-   *    at step 1 when the data plane receives a response from the upstream
-   *    server.
-   * 6. At any point above, if the server closes the gRPC stream cleanly,
-   *    then the data plane proceeds without consulting the server.
-   * 7. At any point above, if the server closes the gRPC stream with an error,
-   *    then the data plane returns a 500 error to the client, unless the filter
-   *    was configured to ignore errors.
-   * In other words, the process is a request/response conversation, but
-   * using a gRPC stream to make it easier for the server to
-   * maintain state.
+   * 2. The service sends back a ``ProcessingResponse`` message that directs the data plane to either
+   *    stop processing, continue without it, or send it the next chunk of the message body.
+   * 3. If so requested, the data plane sends the server the message body in chunks, or the entire
+   *    body at once. In either case, the server may send back a ``ProcessingResponse`` for each
+   *    message it receives, or wait for a certain amount of body chunks to be received before
+   *    streaming back the ``ProcessingResponse`` messages.
+   * 4. If so requested, the data plane sends the server the HTTP trailers, and the server sends back
+   *    a ``ProcessingResponse``.
+   * 5. At this point, request processing is done, and we pick up again at step 1 when the data plane
+   *    receives a response from the upstream server.
+   * 6. At any point above, if the server closes the gRPC stream cleanly, then the data plane
+   *    proceeds without consulting the server.
+   * 7. At any point above, if the server closes the gRPC stream with an error, then the data plane
+   *    returns a ``500`` error to the client, unless the filter was configured to ignore errors.
+   * In other words, the process is a request/response conversation, but using a gRPC stream to make
+   * it easier for the server to maintain state.
    * 
*/ public static final class ExternalProcessorStub @@ -259,7 +235,7 @@ protected ExternalProcessorStub build( *
      * This begins the bidirectional stream that the data plane will use to
      * give the server control over what the filter does. The actual
-     * protocol is described by the ProcessingRequest and ProcessingResponse
+     * protocol is described by the ``ProcessingRequest`` and ``ProcessingResponse``
      * messages below.
      * 
*/ @@ -273,31 +249,25 @@ public io.grpc.stub.StreamObserver - * A service that can access and modify HTTP requests and responses - * as part of a filter chain. + * A service that can access and modify HTTP requests and responses as part of a filter chain. * The overall external processing protocol works like this: * 1. The data plane sends to the service information about the HTTP request. - * 2. The service sends back a ProcessingResponse message that directs - * the data plane to either stop processing, continue without it, or send - * it the next chunk of the message body. - * 3. If so requested, the data plane sends the server the message body in - * chunks, or the entire body at once. In either case, the server may send - * back a ProcessingResponse for each message it receives, or wait for - * a certain amount of body chunks received before streaming back the - * ProcessingResponse messages. - * 4. If so requested, the data plane sends the server the HTTP trailers, - * and the server sends back a ProcessingResponse. - * 5. At this point, request processing is done, and we pick up again - * at step 1 when the data plane receives a response from the upstream - * server. - * 6. At any point above, if the server closes the gRPC stream cleanly, - * then the data plane proceeds without consulting the server. - * 7. At any point above, if the server closes the gRPC stream with an error, - * then the data plane returns a 500 error to the client, unless the filter - * was configured to ignore errors. - * In other words, the process is a request/response conversation, but - * using a gRPC stream to make it easier for the server to - * maintain state. + * 2. The service sends back a ``ProcessingResponse`` message that directs the data plane to either + * stop processing, continue without it, or send it the next chunk of the message body. + * 3. If so requested, the data plane sends the server the message body in chunks, or the entire + * body at once. In either case, the server may send back a ``ProcessingResponse`` for each + * message it receives, or wait for a certain amount of body chunks to be received before + * streaming back the ``ProcessingResponse`` messages. + * 4. If so requested, the data plane sends the server the HTTP trailers, and the server sends back + * a ``ProcessingResponse``. + * 5. At this point, request processing is done, and we pick up again at step 1 when the data plane + * receives a response from the upstream server. + * 6. At any point above, if the server closes the gRPC stream cleanly, then the data plane + * proceeds without consulting the server. + * 7. At any point above, if the server closes the gRPC stream with an error, then the data plane + * returns a ``500`` error to the client, unless the filter was configured to ignore errors. + * In other words, the process is a request/response conversation, but using a gRPC stream to make + * it easier for the server to maintain state. * */ public static final class ExternalProcessorBlockingV2Stub @@ -317,7 +287,7 @@ protected ExternalProcessorBlockingV2Stub build( *
      * This begins the bidirectional stream that the data plane will use to
      * give the server control over what the filter does. The actual
-     * protocol is described by the ProcessingRequest and ProcessingResponse
+     * protocol is described by the ``ProcessingRequest`` and ``ProcessingResponse``
      * messages below.
      * 
*/ @@ -332,31 +302,25 @@ protected ExternalProcessorBlockingV2Stub build( /** * A stub to allow clients to do limited synchronous rpc calls to service ExternalProcessor. *
-   * A service that can access and modify HTTP requests and responses
-   * as part of a filter chain.
+   * A service that can access and modify HTTP requests and responses as part of a filter chain.
    * The overall external processing protocol works like this:
    * 1. The data plane sends to the service information about the HTTP request.
-   * 2. The service sends back a ProcessingResponse message that directs
-   *    the data plane to either stop processing, continue without it, or send
-   *    it the next chunk of the message body.
-   * 3. If so requested, the data plane sends the server the message body in
-   *    chunks, or the entire body at once. In either case, the server may send
-   *    back a ProcessingResponse for each message it receives, or wait for
-   *    a certain amount of body chunks received before streaming back the
-   *    ProcessingResponse messages.
-   * 4. If so requested, the data plane sends the server the HTTP trailers,
-   *    and the server sends back a ProcessingResponse.
-   * 5. At this point, request processing is done, and we pick up again
-   *    at step 1 when the data plane receives a response from the upstream
-   *    server.
-   * 6. At any point above, if the server closes the gRPC stream cleanly,
-   *    then the data plane proceeds without consulting the server.
-   * 7. At any point above, if the server closes the gRPC stream with an error,
-   *    then the data plane returns a 500 error to the client, unless the filter
-   *    was configured to ignore errors.
-   * In other words, the process is a request/response conversation, but
-   * using a gRPC stream to make it easier for the server to
-   * maintain state.
+   * 2. The service sends back a ``ProcessingResponse`` message that directs the data plane to either
+   *    stop processing, continue without it, or send it the next chunk of the message body.
+   * 3. If so requested, the data plane sends the server the message body in chunks, or the entire
+   *    body at once. In either case, the server may send back a ``ProcessingResponse`` for each
+   *    message it receives, or wait for a certain amount of body chunks to be received before
+   *    streaming back the ``ProcessingResponse`` messages.
+   * 4. If so requested, the data plane sends the server the HTTP trailers, and the server sends back
+   *    a ``ProcessingResponse``.
+   * 5. At this point, request processing is done, and we pick up again at step 1 when the data plane
+   *    receives a response from the upstream server.
+   * 6. At any point above, if the server closes the gRPC stream cleanly, then the data plane
+   *    proceeds without consulting the server.
+   * 7. At any point above, if the server closes the gRPC stream with an error, then the data plane
+   *    returns a ``500`` error to the client, unless the filter was configured to ignore errors.
+   * In other words, the process is a request/response conversation, but using a gRPC stream to make
+   * it easier for the server to maintain state.
    * 
*/ public static final class ExternalProcessorBlockingStub @@ -376,31 +340,25 @@ protected ExternalProcessorBlockingStub build( /** * A stub to allow clients to do ListenableFuture-style rpc calls to service ExternalProcessor. *
-   * A service that can access and modify HTTP requests and responses
-   * as part of a filter chain.
+   * A service that can access and modify HTTP requests and responses as part of a filter chain.
    * The overall external processing protocol works like this:
    * 1. The data plane sends to the service information about the HTTP request.
-   * 2. The service sends back a ProcessingResponse message that directs
-   *    the data plane to either stop processing, continue without it, or send
-   *    it the next chunk of the message body.
-   * 3. If so requested, the data plane sends the server the message body in
-   *    chunks, or the entire body at once. In either case, the server may send
-   *    back a ProcessingResponse for each message it receives, or wait for
-   *    a certain amount of body chunks received before streaming back the
-   *    ProcessingResponse messages.
-   * 4. If so requested, the data plane sends the server the HTTP trailers,
-   *    and the server sends back a ProcessingResponse.
-   * 5. At this point, request processing is done, and we pick up again
-   *    at step 1 when the data plane receives a response from the upstream
-   *    server.
-   * 6. At any point above, if the server closes the gRPC stream cleanly,
-   *    then the data plane proceeds without consulting the server.
-   * 7. At any point above, if the server closes the gRPC stream with an error,
-   *    then the data plane returns a 500 error to the client, unless the filter
-   *    was configured to ignore errors.
-   * In other words, the process is a request/response conversation, but
-   * using a gRPC stream to make it easier for the server to
-   * maintain state.
+   * 2. The service sends back a ``ProcessingResponse`` message that directs the data plane to either
+   *    stop processing, continue without it, or send it the next chunk of the message body.
+   * 3. If so requested, the data plane sends the server the message body in chunks, or the entire
+   *    body at once. In either case, the server may send back a ``ProcessingResponse`` for each
+   *    message it receives, or wait for a certain amount of body chunks to be received before
+   *    streaming back the ``ProcessingResponse`` messages.
+   * 4. If so requested, the data plane sends the server the HTTP trailers, and the server sends back
+   *    a ``ProcessingResponse``.
+   * 5. At this point, request processing is done, and we pick up again at step 1 when the data plane
+   *    receives a response from the upstream server.
+   * 6. At any point above, if the server closes the gRPC stream cleanly, then the data plane
+   *    proceeds without consulting the server.
+   * 7. At any point above, if the server closes the gRPC stream with an error, then the data plane
+   *    returns a ``500`` error to the client, unless the filter was configured to ignore errors.
+   * In other words, the process is a request/response conversation, but using a gRPC stream to make
+   * it easier for the server to maintain state.
    * 
*/ public static final class ExternalProcessorFutureStub diff --git a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java index af46866b0d9..de507889ed5 100644 --- a/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java +++ b/xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java @@ -261,6 +261,8 @@ public void setUp() throws Exception { .build().start()); } + + private ExternalProcessor.Builder createBaseProto(String targetName) { return ExternalProcessor.newBuilder() .setGrpcService(GrpcService.newBuilder() @@ -274,6 +276,7 @@ private ExternalProcessor.Builder createBaseProto(String targetName) { .build()); } + // --- Category 1: Configuration Override --- @Test @@ -368,6 +371,8 @@ public void givenOverrideConfig_whenFailureModeAllowOverridden_thenTakesEffect() assertThat(interceptor.getFilterConfig().getFailureModeAllow()).isTrue(); } + + @Test public void givenOverrideConfig_whenOtherFieldsOverridden_thenReplaced() throws Exception { ExternalProcessor parentProto = createBaseProto(extProcServerName) @@ -532,6 +537,7 @@ public void givenOverrideConfig_whenSomeFieldsOverridden_thenMergedCorrectly() t .containsExactly("attr-parent"); } + @Test public void givenOverrideConfig_whenDisableImmediateResponseOverridden_thenInheritedFromParent() throws Exception { @@ -811,6 +817,8 @@ public ClientCall interceptCall( channelManager.close(); } + + // --- Category 3: Protocol config propagation --- @Test @@ -3021,6 +3029,7 @@ public void onCompleted() { channelManager.close(); } + // --- Category 8: Response Header Mutation --- @Test @@ -3309,6 +3318,7 @@ public ServerCall.Listener interceptCall( channelManager.close(); } + // --- Category 9: Body Mutation: Inbound/Response (GRPC Mode) --- @Test @@ -3453,7 +3463,6 @@ public void onClose(Status status, Metadata trailers) { long startTime = System.currentTimeMillis(); while (sidecarBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(capturedRequest.get().getResponseBody().getBody().toStringUtf8()) .isEqualTo("Server Message"); @@ -3461,7 +3470,6 @@ public void onClose(Status status, Metadata trailers) { while ((appMessageLatch.getCount() > 0 || appCloseLatch.getCount() > 0) && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } proxyCall.cancel("Cleanup", null); @@ -3611,22 +3619,20 @@ public void onClose(Status status, Metadata trailers) { long startTime = System.currentTimeMillis(); while (sidecarBodyLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } while (appMessageLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(capturedMessage.get()).isEqualTo("Mutated Server"); while (appCloseLatch.getCount() > 0 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } proxyCall.cancel("Cleanup", null); channelManager.close(); } + // --- Category 10: Response Trailers --- @Test @@ -4693,6 +4699,8 @@ public void onClose(Status status, Metadata trailers) { channelManager.close(); } + + // --- Category 12: Half-Close handling --- @Test @@ -5470,6 +5478,8 @@ public void halfClose() { channelManager.close(); } + + // --- Category 13: Outbound Backpressure (isReady / onReady) --- @Test @@ -5933,7 +5943,6 @@ public void onReady() { // Wait for sidecar call to start and listener to be captured long startTime = System.currentTimeMillis(); while (sidecarListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { - Thread.sleep(10); } assertThat(sidecarListenerRef.get()).isNotNull(); @@ -6257,7 +6266,6 @@ public void onClose(Status status, Metadata trailers) { assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Wait for the drain signal to be received and processed by client call - Thread.sleep(100); // Call is now in DRAINING state. // Send a message. Since request_body_mode is NONE, it should go directly to data plane. @@ -6756,7 +6764,6 @@ public void onMessage(String message) { assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Wait for the drain signal to be received and processed by client call - Thread.sleep(100); // Send response headers first (they bypass ext_proc because send mode is default SKIP, so // they proceed immediately) @@ -6881,7 +6888,6 @@ public void onHeaders(Metadata headers) { assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Wait for the drain signal to be received and processed by client call - Thread.sleep(100); // Call is in DRAINING state. // Send response headers from server. Since response_header_mode is SKIP, they should go @@ -7002,7 +7008,6 @@ public void onClose(Status status, Metadata trailers) { assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Wait for the drain signal to be received and processed by client call - Thread.sleep(100); // Call is in DRAINING state. // Complete the server call. Since response_trailer_mode is SKIP, onClose should trigger @@ -7107,7 +7112,6 @@ public void onCompleted() { // Use a small loop because of SerializingExecutor delay even with directExecutor. long start = System.currentTimeMillis(); while (proxyCall.isReady() && System.currentTimeMillis() - start < 2000) { - Thread.sleep(10); } assertThat(proxyCall.isReady()).isFalse(); @@ -7162,10 +7166,10 @@ public void onNext(ProcessingRequest request) { sidecarOnNextLatch.countDown(); try { if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { - sidecarOnCompletedLatch.countDown(); synchronized (responseObserver) { responseObserver.onCompleted(); } + sidecarOnCompletedLatch.countDown(); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); @@ -7242,14 +7246,14 @@ public void onReady() { fakeClock.forwardTime(1, TimeUnit.SECONDS); } - // Wait for ext_proc server to send drain and test to observe it + // Wait for sidecar to send drain and test to observe it assertThat(sidecarOnNextLatch.await(5, TimeUnit.SECONDS)).isTrue(); for (int i = 0; i < 10; i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); } assertThat(proxyCall.isReady()).isFalse(); - // Now let ext_proc server complete + // Now let sidecar complete sidecarFinishLatch.countDown(); for (int i = 0; i < 10; i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); @@ -7265,12 +7269,8 @@ public void onReady() { fakeClock.forwardTime(1, TimeUnit.SECONDS); } - // After ext_proc server stream completes, it should trigger onReady and become ready + // After sidecar stream completes, it should trigger onReady and become ready assertThat(onReadyLatch.await(5, TimeUnit.SECONDS)).isTrue(); - for (int i = 0; i < 50 && !proxyCall.isReady(); i++) { - fakeClock.forwardTime(100, TimeUnit.MILLISECONDS); - Thread.sleep(10); - } assertThat(proxyCall.isReady()).isTrue(); proxyCall.cancel("Cleanup", null); @@ -7506,24 +7506,22 @@ public void onMessage(String message) { // Wait for drain to be processed long startTime = System.currentTimeMillis(); while (proxyCall.isReady() && System.currentTimeMillis() - startTime < 5000) { - Thread.sleep(10); } assertThat(proxyCall.isReady()).isFalse(); - // Request messages from server while stream is draining (and ext_proc server not ready) + // Request messages from server while stream is draining (and sidecar not ready) proxyCall.request(1); - // Now let ext_proc server complete + // Now let sidecar complete sidecarFinishLatch.countDown(); // Wait for it to become ready again startTime = System.currentTimeMillis(); while (!proxyCall.isReady() && System.currentTimeMillis() - startTime < 5000) { - Thread.sleep(10); } assertThat(proxyCall.isReady()).isTrue(); - // 1. Verify application message is forwarded to data plane WITHOUT ext_proc server contact + // 1. Verify application message is forwarded to data plane WITHOUT sidecar contact proxyCall.sendMessage("Direct Message"); proxyCall.halfClose(); @@ -7533,7 +7531,7 @@ public void onMessage(String message) { assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(dataPlaneReceivedMessage.get()).isEqualTo("Direct Message"); - // 2. Verify server response is delivered to application WITHOUT ext_proc server call + // 2. Verify server response is delivered to application WITHOUT sidecar call assertThat(appLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(appReceivedMessage.get()).isEqualTo("Direct Response"); @@ -7630,7 +7628,7 @@ public void onCompleted() { filterConfig, channelManager, scheduler, FAKE_CONTEXT); final List dataPlaneReceivedMessages = - new CopyOnWriteArrayList<>(); + new java.util.concurrent.CopyOnWriteArrayList<>(); final CountDownLatch dataPlaneLatch = new CountDownLatch(1); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( @@ -7644,7 +7642,8 @@ public void onNext(String value) { } @Override - public void onError(Throwable t) {} + public void onError(Throwable t) { + } @Override public void onCompleted() { @@ -7669,7 +7668,7 @@ public void onMessage(String message) { appLatch.countDown(); } }; - + CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor()); ClientCall proxyCall = interceptCall(interceptor, METHOD_CLIENT_STREAMING, callOptions, dataPlaneChannel); @@ -7692,7 +7691,7 @@ public void onMessage(String message) { // Assert that it was NOT received by extProc assertThat(extProcReceivedBodyCount.get()).isEqualTo(0); - // Now let ext_proc server complete + // Now let sidecar complete sidecarFinishLatch.countDown(); // Request response from data plane @@ -7815,7 +7814,7 @@ public void onCompleted() { filterConfig, channelManager, scheduler, FAKE_CONTEXT); final List dataPlaneReceivedMessages = - new CopyOnWriteArrayList<>(); + new java.util.concurrent.CopyOnWriteArrayList<>(); final CountDownLatch dataPlaneLatch = new CountDownLatch(1); dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") .addMethod(METHOD_CLIENT_STREAMING, ServerCalls.asyncClientStreamingCall( @@ -7888,7 +7887,7 @@ public void onMessage(String message) { // Verify the message during drain has NOT been delivered to the data plane server yet assertThat(dataPlaneReceivedMessages).containsExactly("Mutated Message 1"); - // Now let ext_proc server complete + // Now let sidecar complete sidecarFinishLatch.countDown(); // Wait for the control stream drain to be fully completed @@ -8025,7 +8024,7 @@ public void onCompleted() { ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - final List appReceivedMessages = new CopyOnWriteArrayList<>(); + final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); final AtomicReference appReceivedHeaders = new AtomicReference<>(); final AtomicReference appReceivedStatus = new AtomicReference<>(); final AtomicReference appReceivedTrailers = new AtomicReference<>(); @@ -8057,7 +8056,7 @@ public void onClose(Status status, Metadata trailers) { // Request messages from server proxyCall.request(10); - // Wait for drain to be processed and ext_proc server's client stream to finish + // Wait for drain to be processed and sidecar's client stream to finish assertThat(drainCompletedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isFalse(); @@ -8076,7 +8075,7 @@ public void onClose(Status status, Metadata trailers) { assertThat(appReceivedMessages).isEmpty(); assertThat(appReceivedStatus.get()).isNull(); - // Now let ext_proc server complete the drain + // Now let sidecar complete the drain sidecarFinishLatch.countDown(); // Wait for the call to close on application side @@ -8125,9 +8124,6 @@ public void drainingStartsAfterResponseHeaders_whenUpstreamResponds_thenBuffered final CountDownLatch sidecarFinishLatch = new CountDownLatch(1); final CountDownLatch drainCompletedLatch = new CountDownLatch(1); - final ByteString mutated1 = ByteString.copyFromUtf8("Mutated Message 1"); - final ByteString mutated2 = ByteString.copyFromUtf8("Mutated Message 2"); - // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @@ -8141,102 +8137,84 @@ public StreamObserver process( public void onNext(ProcessingRequest request) { if (request.hasRequestHeaders()) { synchronized (responseObserver) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); } reqHeadersLatch.countDown(); } else if (request.hasResponseHeaders()) { synchronized (responseObserver) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); } respHeadersLatch.countDown(); } else if (request.hasResponseBody()) { String msgStr = request.getResponseBody().getBody().toStringUtf8(); if ("Original Message 1".equals(msgStr)) { - new Thread( - () -> { - try { - // Wait until M2 is received by sidecar so both M1 and M2 - // are in flight - if (m2ReceivedLatch.await(5, TimeUnit.SECONDS)) { - synchronized (responseObserver) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setResponseBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse - .newBuilder() - .setBody(mutated1) - .build()) - .build()) - .build()) - .build()) - .setRequestDrain(true) - .build()); - } - respBody1Latch.countDown(); - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }) - .start(); + new Thread(() -> { + try { + // Wait until M2 is received by sidecar so both M1 and M2 are in flight + if (m2ReceivedLatch.await(5, TimeUnit.SECONDS)) { + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8( + "Mutated Message 1")) + .build()) + .build()) + .build()) + .build()) + .setRequestDrain(true) + .build()); + } + respBody1Latch.countDown(); + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }).start(); } else if ("Original Message 2".equals(msgStr)) { m2ReceivedLatch.countDown(); - new Thread( - () -> { - try { - // Wait until M3 is sent by upstream concurrently during drain - if (m3SentLatch.await(5, TimeUnit.SECONDS)) { - synchronized (responseObserver) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setResponseBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse - .newBuilder() - .setBody(mutated2) - .build()) - .build()) - .build()) - .build()) - .build()); - } - respBody2Latch.countDown(); - } - if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { - synchronized (responseObserver) { - responseObserver.onCompleted(); - } - } - } catch (InterruptedException e) { - Thread.currentThread().interrupt(); - } - }) - .start(); + new Thread(() -> { + try { + // Wait until M3 is sent by upstream concurrently during drain + if (m3SentLatch.await(5, TimeUnit.SECONDS)) { + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(ByteString.copyFromUtf8( + "Mutated Message 2")) + .build()) + .build()) + .build()) + .build()) + .build()); + } + respBody2Latch.countDown(); + } + if (sidecarFinishLatch.await(5, TimeUnit.SECONDS)) { + synchronized (responseObserver) { + responseObserver.onCompleted(); + } + } + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + }).start(); } } } - + @Override - public void onError(Throwable t) { - } - + public void onError(Throwable t) {} + @Override public void onCompleted() { drainCompletedLatch.countDown(); @@ -8285,7 +8263,7 @@ public void onCompleted() {} ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build()); - final List appReceivedMessages = new CopyOnWriteArrayList<>(); + final List appReceivedMessages = new java.util.concurrent.CopyOnWriteArrayList<>(); final AtomicReference appReceivedHeaders = new AtomicReference<>(); final AtomicReference appReceivedStatus = new AtomicReference<>(); final AtomicReference appReceivedTrailers = new AtomicReference<>(); @@ -8330,13 +8308,13 @@ public void onClose(Status status, Metadata trailers) { StreamObserver dataPlaneResponseObserver = dataPlaneResponseObserverRef.get(); assertThat(dataPlaneResponseObserver).isNotNull(); - // 1. Upstream sends M1 (which triggers response headers and M1 body to ext_proc server) + // 1. Upstream sends M1 (which triggers response headers and M1 body to sidecar) dataPlaneResponseObserver.onNext("Original Message 1"); - // Wait for ext_proc server to receive and respond to response headers + // Wait for sidecar to receive and respond to response headers assertThat(respHeadersLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // 2. Upstream sends M2 (which triggers M2 body to ext_proc server) + // 2. Upstream sends M2 (which triggers M2 body to sidecar) dataPlaneResponseObserver.onNext("Original Message 2"); // Wait for app to receive Mutated Message 1 (meaning M1's response with request_drain=true @@ -8355,10 +8333,10 @@ public void onClose(Status status, Metadata trailers) { assertThat(appReceivedMessages).containsExactly("Mutated Message 1"); assertThat(appReceivedStatus.get()).isNull(); - // 4. Signal ext_proc server to send Mutated Message 2 + // 4. Signal sidecar to send Mutated Message 2 m3SentLatch.countDown(); - // Wait for ext_proc server to finish sending M2 and app to receive it + // Wait for sidecar to finish sending M2 and app to receive it assertThat(respBody2Latch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(mutatedMsg2ReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue(); @@ -8366,7 +8344,7 @@ public void onClose(Status status, Metadata trailers) { // M3 is released) assertThat(appReceivedMessages).containsExactly("Mutated Message 1", "Mutated Message 2"); - // 5. Complete ext_proc server stream to finish the drain + // 5. Complete sidecar stream to finish the drain sidecarFinishLatch.countDown(); // Wait for the call to close on application side @@ -8519,24 +8497,23 @@ public void request(int numMessages) { // Wait for sidecar call to start long startTime = System.currentTimeMillis(); while (sidecarListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { - Thread.sleep(10); } assertThat(sidecarListenerRef.get()).isNotNull(); - // Sidecar server is busy + // Sidecar is busy sidecarReady.set(false); assertThat(proxyCall.isReady()).isFalse(); proxyCall.request(5); - // Verify data plane call NOT requested yet (due to observability mode and ext_proc server busy) + // Verify data plane call NOT requested yet (due to observability mode and sidecar busy) assertThat(dataPlaneRequestCount.get()).isEqualTo(0); - // Sidecar server becomes ready + // Sidecar becomes ready sidecarReady.set(true); sidecarListenerRef.get().onReady(); - // After ext_proc server becomes ready, pending requests should be drained to data plane. + // After sidecar becomes ready, pending requests should be drained to data plane. assertThat(dataPlaneRequestCount.get()).isEqualTo(5); assertThat(proxyCall.isReady()).isTrue(); @@ -8645,7 +8622,6 @@ public void request(int numMessages) { // Wait for drain to be processed long startTime = System.currentTimeMillis(); while (proxyCall.isReady() && System.currentTimeMillis() - startTime < 5000) { - Thread.sleep(10); } assertThat(proxyCall.isReady()).isFalse(); @@ -8780,21 +8756,20 @@ public void request(int numMessages) { interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel); proxyCall.start(new ClientCall.Listener() {}, new Metadata()); - // Wait for ext_proc server call to start + // Wait for sidecar call to start long startTime = System.currentTimeMillis(); while (sidecarListenerRef.get() == null && System.currentTimeMillis() - startTime < 5000) { - Thread.sleep(10); } assertThat(sidecarListenerRef.get()).isNotNull(); - // Sidecar server is busy initially + // Sidecar is busy initially sidecarReady.set(false); // Request from application proxyCall.request(10); assertThat(dataPlaneRequestCount.get()).isEqualTo(0); - // Sidecar server becomes ready + // Sidecar becomes ready sidecarReady.set(true); sidecarListenerRef.get().onReady(); @@ -8905,7 +8880,7 @@ public void onReady() { } }, new Metadata()); - // Wait for ext_proc server stream completion + // Wait for sidecar stream completion assertThat(readyLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isTrue(); @@ -9083,6 +9058,7 @@ public void onClose(Status status, Metadata trailers) { channelManager.close(); } + @Test @SuppressWarnings("FutureReturnValueIgnored") public void @@ -9480,8 +9456,7 @@ public void onClose(Status status, Metadata trailers) { // Trigger unary call. request(1) starts it. proxyCall.request(1); - // Wait for the async ext_proc server thread to enter activateCall() and block inside - // interceptCall + // Wait for the async sidecar thread to enter activateCall() and block inside interceptCall assertThat(headersReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue(); // Now, while the async thread is blocked (and passThroughMode is still false), @@ -9768,7 +9743,6 @@ public void onClose(Status status, Metadata trailers) { // Wait for sidecar to receive headers and filter to activate call for (int i = 0; i < 5000 && closedLatch.getCount() > 0; i++) { fakeClock.forwardTime(10, TimeUnit.MILLISECONDS); - Thread.sleep(1); } // Trigger request body processing to hit the unsupported compression check @@ -9819,73 +9793,61 @@ public void givenUnsupportedCompressionInResponseBody_whenReceived_thenStreamErr // External Processor Server ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; - extProcImpl = - new ExternalProcessorGrpc.ExternalProcessorImplBase() { + extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { + @Override + @SuppressWarnings("unchecked") + public StreamObserver process( + final StreamObserver responseObserver) { + ((ServerCallStreamObserver) responseObserver).request(100); + return new StreamObserver() { @Override - @SuppressWarnings("unchecked") - public StreamObserver process( - final StreamObserver responseObserver) { - ((ServerCallStreamObserver) responseObserver).request(100); - return new StreamObserver() { - @Override - public void onNext(ProcessingRequest request) { - if (request.hasRequestHeaders()) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setRequestHeaders( - HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder().build()) - .build()) - .build()); - } else if (request.hasRequestBody()) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setRequestBody( - BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder().build()) - .build()) - .build()); - } else if (request.hasResponseHeaders()) { - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setResponseHeaders( - HeadersResponse.newBuilder() - .setResponse(CommonResponse.newBuilder().build()) - .build()) - .build()); - } else if (request.hasResponseBody()) { - // Simulate sidecar sending compressed body mutation (unsupported) for - // response body - responseObserver.onNext( - ProcessingResponse.newBuilder() - .setResponseBody( - BodyResponse.newBuilder() - .setResponse( - CommonResponse.newBuilder() - .setBodyMutation( - BodyMutation.newBuilder() - .setStreamedResponse( - StreamedBodyResponse.newBuilder() - .setGrpcMessageCompressed(true) - .build()) - .build()) - .build()) + public void onNext(ProcessingRequest request) { + if (request.hasRequestHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder().build()) + .build()) + .build()); + } else if (request.hasRequestBody()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder().build()) + .build()) + .build()); + } else if (request.hasResponseHeaders()) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder() + .setResponse(CommonResponse.newBuilder().build()) + .build()) + .build()); + } else if (request.hasResponseBody()) { + // Simulate sidecar sending compressed body mutation (unsupported) for + // response body + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setGrpcMessageCompressed(true) .build()) - .build()); - } - } + .build()) + .build()) + .build()) + .build()); + } + } - @Override - public void onError(Throwable t) { - } + @Override + public void onError(Throwable t) { + } - @Override - public void onCompleted() { - responseObserver.onCompleted(); - } - }; + @Override + public void onCompleted() { + responseObserver.onCompleted(); } }; + } + }; grpcCleanup.register(InProcessServerBuilder.forName(uniqueExtProcServerName) .addService(extProcImpl) .directExecutor() @@ -10068,7 +10030,6 @@ public void onCompleted() { for (int i = 0; i < 1000 && finishLatch.getCount() > 0; i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(1); } assertThat(finishLatch.await(5, TimeUnit.SECONDS)).isTrue(); @@ -10089,7 +10050,6 @@ public void onCompleted() { || !extProcServer.isTerminated()); i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(1); } channelManager.close(); } @@ -10326,9 +10286,8 @@ public void onClose(Status status, Metadata trailers) { assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(closedStatus.get().getCode()).isEqualTo(Status.Code.UNAUTHENTICATED); assertThat(closedStatus.get().getDescription()).isEqualTo("Custom security rejection"); - - // Data plane call should NOT have been started as ext_proc server rejected immediately on - // headers + + // Data plane call should NOT have been started as sidecar rejected immediately on headers assertThat(dataPlaneStarted.get()).isFalse(); proxyCall.cancel("Cleanup", null); @@ -10538,7 +10497,6 @@ public void onCompleted() { for (int i = 0; i < 1000 && closedLatch.getCount() > 0; i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(1); } // Verify app listener notified with an error (not the sidecar's UNAUTHENTICATED) assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue(); @@ -10552,7 +10510,6 @@ public void onCompleted() { i < 100 && (!dataPlaneChannel.isTerminated() || !extProcServer.isTerminated()); i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(1); } channelManager.close(); } @@ -10662,7 +10619,6 @@ public void onCompleted() { for (int i = 0; i < 1000 && closedLatch.getCount() > 0; i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(1); } // Verify app listener notified with UNIMPLEMENTED because data plane connection succeeded // but the method was not registered, and it failed before the ext-proc stream failed @@ -10677,7 +10633,6 @@ public void onCompleted() { i < 100 && (!dataPlaneChannel.isTerminated() || !extProcServer.isTerminated()); i++) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(1); } channelManager.close(); } @@ -10934,14 +10889,13 @@ public void onCompleted() { // Wait for activation for (int i = 0; i < 50 && !proxyCall.isReady(); i++) { fakeClock.forwardTime(100, TimeUnit.MILLISECONDS); - Thread.sleep(10); } assertThat(proxyCall.isReady()).isTrue(); // Application cancels the RPC proxyCall.cancel("User cancelled", null); - // Verify ext_proc server stream also cancelled + // Verify sidecar stream also cancelled assertThat(cancelLatch.await(5, TimeUnit.SECONDS)).isTrue(); channelManager.close(); @@ -10971,9 +10925,10 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenIsReadyReturnsFalse( ExternalProcessorFilterConfig filterConfig = configOrError.config; final List extProcRequests = - new CopyOnWriteArrayList<>(); + new java.util.concurrent.CopyOnWriteArrayList<>(); // Sidecar server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); + final CountDownLatch responseSentLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override @@ -10993,6 +10948,7 @@ public void onNext(ProcessingRequest request) { .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } + responseSentLatch.countDown(); } }).start(); } @@ -11043,16 +10999,13 @@ public boolean isReady() { ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor( filterConfig, channelManager, scheduler, FAKE_CONTEXT); - dataPlaneServiceRegistry.addService( - ServerServiceDefinition.builder("test.TestService") - .addMethod( - METHOD_SAY_HELLO, - ServerCalls.asyncUnaryCall( - (request, responseObserver) -> { - responseObserver.onNext("Hello"); - responseObserver.onCompleted(); - })) - .build()); + dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService") + .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall( + (request, responseObserver) -> { + responseObserver.onNext("Hello"); + responseObserver.onCompleted(); + })) + .build()); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName) @@ -11079,18 +11032,14 @@ public boolean isReady() { // Wait for activation assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); - for (int i = 0; i < 50 && !proxyCall.isReady(); i++) { - fakeClock.forwardTime(100, TimeUnit.MILLISECONDS); - Thread.sleep(10); - } + assertThat(responseSentLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isTrue(); - // Sidecar server becomes busy -> proxyCall becomes busy + // Sidecar becomes busy -> proxyCall becomes busy sidecarReady.set(false); assertThat(proxyCall.isReady()).isFalse(); - // Sidecar server becomes ready, but Data Plane is busy -> proxyCall is STILL ready because - // Normal Mode + // Sidecar becomes ready, but Data Plane is busy -> proxyCall is STILL ready because Normal Mode sidecarReady.set(true); dataPlaneReady.set(false); assertThat(proxyCall.isReady()).isTrue(); @@ -11133,6 +11082,7 @@ public void givenObservabilityModeFalse_whenExtProcBusy_thenAppRequestsAreBuffer // Sidecar server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); + final CountDownLatch responseSentLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override @@ -11146,25 +11096,32 @@ public void onNext(ProcessingRequest request) { new Thread(() -> { if (request.hasRequestHeaders()) { sidecarActionLatch.countDown(); - responseObserver.onNext(ProcessingResponse.newBuilder() - .setRequestHeaders(HeadersResponse.newBuilder().build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setRequestHeaders(HeadersResponse.newBuilder().build()) + .build()); + } + responseSentLatch.countDown(); } else if (request.hasResponseHeaders()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseHeaders(HeadersResponse.newBuilder().build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseHeaders(HeadersResponse.newBuilder().build()) + .build()); + } } else if (request.hasResponseBody()) { - responseObserver.onNext(ProcessingResponse.newBuilder() - .setResponseBody(BodyResponse.newBuilder() - .setResponse(CommonResponse.newBuilder() - .setBodyMutation(BodyMutation.newBuilder() - .setStreamedResponse(StreamedBodyResponse.newBuilder() - .setBody(request.getResponseBody().getBody()) - .build()) - .build()) - .build()) - .build()) - .build()); + synchronized (responseObserver) { + responseObserver.onNext(ProcessingResponse.newBuilder() + .setResponseBody(BodyResponse.newBuilder() + .setResponse(CommonResponse.newBuilder() + .setBodyMutation(BodyMutation.newBuilder() + .setStreamedResponse(StreamedBodyResponse.newBuilder() + .setBody(request.getResponseBody().getBody()) + .build()) + .build()) + .build()) + .build()) + .build()); + } } }).start(); } @@ -11275,27 +11232,22 @@ public void request(int numMessages) { // Wait for activation assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); - for (int i = 0; i < 50 && !proxyCall.isReady(); i++) { - fakeClock.forwardTime(100, TimeUnit.MILLISECONDS); - Thread.sleep(10); - } + assertThat(responseSentLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isTrue(); - // Sidecar server busy -> request(5) should be buffered + // Sidecar busy -> request(5) should be buffered sidecarReady.set(false); proxyCall.request(5); assertThat(dataPlaneRequestCount.get()).isEqualTo(1); // (Only the initial bootstrap request went through) - // Sidecar server becomes ready -> buffered requests should start draining (pulling next - // message) + // Sidecar becomes ready -> buffered requests should be drained sidecarReady.set(true); sidecarListenerRef.get().onReady(); - + long startTime2 = System.currentTimeMillis(); while (dataPlaneRequestCount.get() < 2 && System.currentTimeMillis() - startTime2 < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(dataPlaneRequestCount.get()).isEqualTo(2); @@ -11307,7 +11259,6 @@ public void request(int numMessages) { startTime2 = System.currentTimeMillis(); while (dataPlaneRequestCount.get() < 3 && System.currentTimeMillis() - startTime2 < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(dataPlaneRequestCount.get()).isEqualTo(3); @@ -11317,7 +11268,6 @@ public void request(int numMessages) { startTime2 = System.currentTimeMillis(); while (dataPlaneRequestCount.get() < 4 && System.currentTimeMillis() - startTime2 < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(dataPlaneRequestCount.get()).isEqualTo(4); @@ -11327,7 +11277,6 @@ public void request(int numMessages) { startTime2 = System.currentTimeMillis(); while (dataPlaneRequestCount.get() < 5 && System.currentTimeMillis() - startTime2 < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(dataPlaneRequestCount.get()).isEqualTo(5); @@ -11337,7 +11286,6 @@ public void request(int numMessages) { startTime2 = System.currentTimeMillis(); while (dataPlaneRequestCount.get() < 6 && System.currentTimeMillis() - startTime2 < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(dataPlaneRequestCount.get()).isEqualTo(6); @@ -11374,6 +11322,7 @@ public void givenResponseBodyModeNone_whenExtProcBusy_thenAppRequestsAreNotBuffe // Sidecar server final CountDownLatch sidecarActionLatch = new CountDownLatch(1); + final CountDownLatch responseSentLatch = new CountDownLatch(1); ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl; extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() { @Override @@ -11392,6 +11341,7 @@ public void onNext(ProcessingRequest request) { .setRequestHeaders(HeadersResponse.newBuilder().build()) .build()); } + responseSentLatch.countDown(); } }).start(); } @@ -11488,10 +11438,7 @@ public void request(int numMessages) { // Wait for activation assertThat(sidecarActionLatch.await(5, TimeUnit.SECONDS)).isTrue(); - for (int i = 0; i < 50 && !proxyCall.isReady(); i++) { - fakeClock.forwardTime(100, TimeUnit.MILLISECONDS); - Thread.sleep(10); - } + assertThat(responseSentLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(proxyCall.isReady()).isTrue(); // Sidecar server busy @@ -11504,7 +11451,6 @@ public void request(int numMessages) { long startTime = System.currentTimeMillis(); while (dataPlaneRequestCount.get() < 6 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(dataPlaneRequestCount.get()).isEqualTo(6); // 1 bootstrap + 5 requested @@ -12044,7 +11990,6 @@ public void onMessage(String message) { upstreamResponseObserver.onNext(largeMessage30k); // Wait a brief period and verify that the 30k chunk has NOT been sent to the ext_proc server - Thread.sleep(200); assertThat(secondResponseBodyLatch.getCount()).isEqualTo(1); assertThat(appReceivedMessages).hasSize(2); @@ -14870,7 +14815,6 @@ public void onCompleted() {} while (filterClientRequests(receivedRequests).size() < 2 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(filterClientRequests(receivedRequests)).hasSize(2); assertThat(filterClientRequests(receivedRequests).get(1).hasRequestBody()).isTrue(); @@ -14883,7 +14827,6 @@ public void onCompleted() {} startTime = System.currentTimeMillis(); while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } String msg20Bytes = new String(new char[20]).replace('\0', 's'); @@ -14893,7 +14836,6 @@ public void onCompleted() {} startTime = System.currentTimeMillis(); while (receivedRequests.size() < 4 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } // Now send Msg 2 from app (5 bytes). This should piggyback the accumulated updates. @@ -14902,7 +14844,6 @@ public void onCompleted() {} startTime = System.currentTimeMillis(); while (receivedRequests.size() < 6 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(receivedRequests).hasSize(6); @@ -15077,7 +15018,6 @@ public void onCompleted() {} while (filterClientRequests(receivedRequests).size() < 2 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(filterClientRequests(receivedRequests)).hasSize(2); @@ -15089,7 +15029,6 @@ public void onCompleted() {} startTime = System.currentTimeMillis(); while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } String msg20Bytes = new String(new char[20]).replace('\0', 's'); @@ -15099,7 +15038,6 @@ public void onCompleted() {} startTime = System.currentTimeMillis(); while (receivedRequests.size() < 4 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } // Now send a second response body (5 bytes) from upstream. @@ -15108,7 +15046,6 @@ public void onCompleted() {} startTime = System.currentTimeMillis(); while (receivedRequests.size() < 6 && System.currentTimeMillis() - startTime < 5000) { fakeClock.forwardTime(1, TimeUnit.SECONDS); - Thread.sleep(10); } assertThat(receivedRequests).hasSize(6); @@ -15267,6 +15204,7 @@ public void onCompleted() { final CountDownLatch blockSendMessageLatch = new CountDownLatch(1); final CountDownLatch sendMessageEnteredLatch = new CountDownLatch(1); + final CountDownLatch sendMessageFinishedLatch = new CountDownLatch(1); ManagedChannel dataPlaneChannel = grpcCleanup.register( InProcessChannelBuilder.forName(dataPlaneServerName) .intercept(new ClientInterceptor() { @@ -15302,24 +15240,23 @@ public void sendMessage(ReqT message) { String body40k = new String(new char[40000]).replace('\0', 'a'); // Call sendMessage in a background thread to avoid blocking the main test execution thread - new Thread(() -> proxyCall.sendMessage(body40k)).start(); + new Thread(() -> { + proxyCall.sendMessage(body40k); + sendMessageFinishedLatch.countDown(); + }).start(); // Wait until interceptor's super.sendMessage() enters the custom interceptor and blocks assertThat(sendMessageEnteredLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // Wait a brief moment to make sure no standalone ClientWindowUpdate is sent while blocked - Thread.sleep(200); + // No standalone ClientWindowUpdate should be sent while blocked assertThat(receivedRequests).hasSize(2); // Only Headers and Request Body requests sent so far // Unblock the sendMessage call blockSendMessageLatch.countDown(); // Wait for the interceptor to complete super.sendMessage() and send the window update - long startTime = System.currentTimeMillis(); - while (receivedRequests.size() < 3 && System.currentTimeMillis() - startTime < 5000) { - Thread.sleep(10); - } + assertThat(sendMessageFinishedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(receivedRequests).hasSize(3); assertThat(receivedRequests.get(2).hasClientWindowUpdate()).isTrue(); assertThat(receivedRequests.get(2) @@ -15449,6 +15386,7 @@ public void onCompleted() {} final CountDownLatch blockOnMessageLatch = new CountDownLatch(1); final CountDownLatch onMessageEnteredLatch = new CountDownLatch(1); + final CountDownLatch onNextFinishedLatch = new CountDownLatch(1); ClientCall.Listener appListener = new ClientCall.Listener() { @Override public void onMessage(String message) { @@ -15480,14 +15418,16 @@ public void onMessage(String message) { // Trigger response body from upstream in a background thread to avoid blocking // the main test execution thread String response40k = new String(new char[40000]).replace('\0', 'a'); - new Thread(() -> upstreamResponseObserver.onNext(response40k)).start(); + new Thread(() -> { + upstreamResponseObserver.onNext(response40k); + onNextFinishedLatch.countDown(); + }).start(); // Wait until client app's onMessage enters and blocks assertThat(onMessageEnteredLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(sidecarLatch.await(5, TimeUnit.SECONDS)).isTrue(); - // Wait a brief moment to make sure no standalone ClientWindowUpdate is sent while blocked - Thread.sleep(200); + // No standalone ClientWindowUpdate should be sent while blocked assertThat(receivedRequests).hasSize(4); // (Headers + Response Headers + Dummy Body + Response 40k Body) @@ -15495,10 +15435,7 @@ public void onMessage(String message) { blockOnMessageLatch.countDown(); // Wait for the interceptor to complete onMessage processing and send the window update - long startTime = System.currentTimeMillis(); - while (receivedRequests.size() < 5 && System.currentTimeMillis() - startTime < 5000) { - Thread.sleep(10); - } + assertThat(onNextFinishedLatch.await(5, TimeUnit.SECONDS)).isTrue(); assertThat(receivedRequests).hasSize(5); assertThat(receivedRequests.get(4).hasClientWindowUpdate()).isTrue(); assertThat( @@ -17357,6 +17294,8 @@ public void onClose(Status status, Metadata trailers) { channelManager.close(); } + + // --- Category 24: Response Ordering Checks --- @Test @@ -20563,6 +20502,4 @@ private void shutdownAndAwaitTermination(ExecutorService executor) { Thread.currentThread().interrupt(); } } - } -