@@ -9157,6 +9157,146 @@ public void request(int numMessages) {
91579157 channelManager.close();
91589158 }
91599159
9160+ @Test
9161+ @SuppressWarnings("unchecked")
9162+ public void givenRequestDrainActive_whenAppRequestsMessages_thenRequestsDrained()
9163+ throws Exception {
9164+ ExternalProcessor proto = ExternalProcessor.newBuilder()
9165+ .setGrpcService(GrpcService.newBuilder()
9166+ .setGoogleGrpc(GrpcService.GoogleGrpc.newBuilder()
9167+ .setTargetUri("in-process:///" + extProcServerName)
9168+ .addChannelCredentialsPlugin(Any.newBuilder()
9169+ .setTypeUrl("type.googleapis.com/envoy.extensions.grpc_service."
9170+ + "channel_credentials.insecure.v3.InsecureCredentials")
9171+ .build())
9172+ .build())
9173+ .build())
9174+ .setProcessingMode(ProcessingMode.newBuilder()
9175+ .setRequestBodyMode(ProcessingMode.BodySendMode.GRPC)
9176+ .setRequestTrailerMode(ProcessingMode.HeaderSendMode.SEND)
9177+ .build())
9178+ .build();
9179+ ConfigOrError<ExternalProcessorFilterConfig> configOrError =
9180+ provider.parseFilterConfig(Any.pack(proto), filterContext);
9181+ assertThat(configOrError.errorDetail).isNull();
9182+ ExternalProcessorFilterConfig filterConfig = configOrError.config;
9183+
9184+ final CountDownLatch drainSentLatch = new CountDownLatch(1);
9185+ final CountDownLatch headersReceivedLatch = new CountDownLatch(1);
9186+ final CountDownLatch sendDrainLatch = new CountDownLatch(1);
9187+ final CountDownLatch filterSentDrainCompleteLatch = new CountDownLatch(1);
9188+ // External Processor Server
9189+ ExternalProcessorGrpc.ExternalProcessorImplBase extProcImpl;
9190+ extProcImpl = new ExternalProcessorGrpc.ExternalProcessorImplBase() {
9191+ @Override
9192+ @SuppressWarnings("unchecked")
9193+ public StreamObserver<ProcessingRequest> process(
9194+ final StreamObserver<ProcessingResponse> responseObserver) {
9195+ ((ServerCallStreamObserver<ProcessingResponse>) responseObserver).request(100);
9196+ return new StreamObserver<ProcessingRequest>() {
9197+ @Override
9198+ public void onNext(ProcessingRequest request) {
9199+ if (request.hasRequestHeaders()) {
9200+ headersReceivedLatch.countDown();
9201+ new Thread(() -> {
9202+ try {
9203+ sendDrainLatch.await();
9204+ synchronized (responseObserver) {
9205+ responseObserver.onNext(ProcessingResponse.newBuilder()
9206+ .setRequestDrainRequests(true)
9207+ .build());
9208+ }
9209+ drainSentLatch.countDown();
9210+ } catch (Exception e) {}
9211+ }).start();
9212+ } else if (request.hasRequestBody()) {
9213+ if (request.getRequestBody().getDrainComplete()) {
9214+ filterSentDrainCompleteLatch.countDown();
9215+ }
9216+ }
9217+ }
9218+
9219+ @Override
9220+ public void onError(Throwable t) {
9221+ }
9222+
9223+ @Override
9224+ public void onCompleted() {
9225+ }
9226+ };
9227+ }
9228+ };
9229+ grpcCleanup.register(InProcessServerBuilder.forName(extProcServerName)
9230+ .addService(extProcImpl)
9231+ .directExecutor()
9232+ .build().start());
9233+
9234+ CachedChannelManager channelManager = new CachedChannelManager(config -> {
9235+ return grpcCleanup.register(
9236+ InProcessChannelBuilder.forName(extProcServerName).directExecutor().build());
9237+ });
9238+
9239+ dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService")
9240+ .addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall(
9241+ (request, responseObserver) -> {
9242+ responseObserver.onNext("Hello " + request);
9243+ responseObserver.onCompleted();
9244+ }))
9245+ .build());
9246+
9247+ final AtomicInteger dataPlaneRequestCount = new AtomicInteger();
9248+ ExternalProcessorClientInterceptor interceptor = new ExternalProcessorClientInterceptor(
9249+ filterConfig, channelManager, scheduler, FAKE_CONTEXT);
9250+
9251+ ManagedChannel dataPlaneChannel = grpcCleanup.register(
9252+ InProcessChannelBuilder.forName(dataPlaneServerName)
9253+ .directExecutor()
9254+ .intercept(new ClientInterceptor() {
9255+ @Override
9256+ public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
9257+ MethodDescriptor<ReqT, RespT> method, CallOptions callOptions, Channel next) {
9258+ return new io.grpc.ForwardingClientCall.SimpleForwardingClientCall<ReqT, RespT>(
9259+ next.newCall(method, callOptions)) {
9260+ @Override
9261+ public void request(int numMessages) {
9262+ dataPlaneRequestCount.addAndGet(numMessages);
9263+ super.request(numMessages);
9264+ }
9265+ };
9266+ }
9267+ })
9268+ .build());
9269+
9270+ CallOptions callOptions = DEFAULT_CALL_OPTIONS.withExecutor(MoreExecutors.directExecutor());
9271+ ClientCall<String, String> proxyCall =
9272+ interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel);
9273+ proxyCall.start(new ClientCall.Listener<String>() {}, new Metadata());
9274+
9275+ // Wait for headers to reach mock server
9276+ assertThat(headersReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue();
9277+
9278+ // App requests messages early (before drain is received)
9279+ proxyCall.request(3);
9280+
9281+ // Verify requests are buffered and not sent to data plane yet (call is IDLE)
9282+ assertThat(dataPlaneRequestCount.get()).isEqualTo(0);
9283+
9284+ // Now trigger drain (which also activates the call)
9285+ sendDrainLatch.countDown();
9286+
9287+ // Wait for drain to be processed
9288+ assertThat(drainSentLatch.await(5, TimeUnit.SECONDS)).isTrue();
9289+
9290+ // Verify requests are now drained to data plane (request drain does not block response path)
9291+ assertThat(dataPlaneRequestCount.get()).isEqualTo(3);
9292+
9293+ // Wait for filter to send drain_complete to mock server
9294+ assertThat(filterSentDrainCompleteLatch.await(5, TimeUnit.SECONDS)).isTrue();
9295+
9296+ proxyCall.cancel("Cleanup", null);
9297+ channelManager.close();
9298+ }
9299+
91609300 @Test
91619301 @SuppressWarnings("unchecked")
91629302 public void givenBufferedRequests_whenExtProcStreamBecomesReady_thenDataPlaneDrained()
0 commit comments