@@ -8428,6 +8428,170 @@ public void onCompleted() {
84288428 channelManager .close ();
84298429 }
84308430
8431+ @ Test
8432+ public void givenBidiStreamInterleavedEvents_whenExtProcRespondsOutOfLockstep_thenSucceeds ()
8433+ throws Exception {
8434+ String uniqueExtProcServerName = InProcessServerBuilder .generateName ();
8435+ String uniqueDataPlaneServerName = InProcessServerBuilder .generateName ();
8436+ ExecutorService bidiTestExecutor = Executors .newCachedThreadPool ();
8437+
8438+ final CountDownLatch sidecarRequestBodyLatch = new CountDownLatch (1 );
8439+ final CountDownLatch sidecarResponseHeadersLatch = new CountDownLatch (1 );
8440+ final CountDownLatch allDoneLatch = new CountDownLatch (1 );
8441+
8442+ ExternalProcessorGrpc .ExternalProcessorImplBase extProcImpl =
8443+ new ExternalProcessorGrpc .ExternalProcessorImplBase () {
8444+ @ Override
8445+ public StreamObserver <ProcessingRequest > process (
8446+ final StreamObserver <ProcessingResponse > responseObserver ) {
8447+ ((ServerCallStreamObserver <ProcessingResponse >) responseObserver ).request (100 );
8448+ final AtomicReference <StreamObserver <ProcessingResponse >> observerRef =
8449+ new AtomicReference <>(responseObserver );
8450+ return new StreamObserver <ProcessingRequest >() {
8451+ private ProcessingRequest savedRequestBody ;
8452+
8453+ @ Override
8454+ public void onNext (ProcessingRequest request ) {
8455+ if (request .hasRequestBody ()) {
8456+ if (request .getRequestBody ().getEndOfStream () || request .getRequestBody ().getEndOfStreamWithoutMessage ()) {
8457+ // This is the half-close request!
8458+ observerRef .get ().onNext (ProcessingResponse .newBuilder ()
8459+ .setRequestBody (BodyResponse .newBuilder ()
8460+ .setResponse (CommonResponse .newBuilder ()
8461+ .setBodyMutation (BodyMutation .newBuilder ()
8462+ .setStreamedResponse (StreamedBodyResponse .newBuilder ()
8463+ .setEndOfStream (true )
8464+ .build ())
8465+ .build ())
8466+ .build ())
8467+ .build ())
8468+ .build ());
8469+ } else {
8470+ savedRequestBody = request ;
8471+ sidecarRequestBodyLatch .countDown ();
8472+ }
8473+ } else if (request .hasResponseHeaders ()) {
8474+ // When RESPONSE_HEADERS is received, we respond to it first!
8475+ // This is out-of-lockstep because REQUEST_BODY response is still outstanding.
8476+ observerRef .get ().onNext (ProcessingResponse .newBuilder ()
8477+ .setResponseHeaders (HeadersResponse .newBuilder ().build ())
8478+ .build ());
8479+ sidecarResponseHeadersLatch .countDown ();
8480+
8481+ // Now send response to REQUEST_BODY with streamed response containing the body
8482+ if (savedRequestBody != null ) {
8483+ observerRef .get ().onNext (ProcessingResponse .newBuilder ()
8484+ .setRequestBody (BodyResponse .newBuilder ()
8485+ .setResponse (CommonResponse .newBuilder ()
8486+ .setBodyMutation (BodyMutation .newBuilder ()
8487+ .setStreamedResponse (StreamedBodyResponse .newBuilder ()
8488+ .setBody (savedRequestBody .getRequestBody ().getBody ())
8489+ .build ())
8490+ .build ())
8491+ .build ())
8492+ .build ())
8493+ .build ());
8494+ }
8495+ }
8496+ }
8497+
8498+ @ Override
8499+ public void onError (Throwable t ) {}
8500+
8501+ @ Override
8502+ public void onCompleted () {
8503+ observerRef .get ().onCompleted ();
8504+ }
8505+ };
8506+ }
8507+ };
8508+
8509+ grpcCleanup .register (InProcessServerBuilder .forName (uniqueExtProcServerName )
8510+ .addService (extProcImpl )
8511+ .executor (bidiTestExecutor )
8512+ .build ().start ());
8513+
8514+ MutableHandlerRegistry uniqueBidiRegistry = new MutableHandlerRegistry ();
8515+ uniqueBidiRegistry .addService (ServerServiceDefinition .builder ("test.TestService" )
8516+ .addMethod (METHOD_BIDI_STREAMING , ServerCalls .asyncBidiStreamingCall (
8517+ new ServerCalls .BidiStreamingMethod <String , String >() {
8518+ @ Override
8519+ public StreamObserver <String > invoke (StreamObserver <String > responseObserver ) {
8520+ // When bidi stream starts on data plane, send headers immediately by sending a message
8521+ responseObserver .onNext ("Welcome" );
8522+ return new StreamObserver <String >() {
8523+ @ Override public void onNext (String value ) {}
8524+ @ Override public void onError (Throwable t ) {}
8525+ @ Override public void onCompleted () {
8526+ responseObserver .onCompleted ();
8527+ }
8528+ };
8529+ }
8530+ }))
8531+ .build ());
8532+
8533+ grpcCleanup .register (InProcessServerBuilder .forName (uniqueDataPlaneServerName )
8534+ .fallbackHandlerRegistry (uniqueBidiRegistry )
8535+ .executor (bidiTestExecutor )
8536+ .build ().start ());
8537+
8538+ ExternalProcessor proto = createBaseProto (uniqueExtProcServerName )
8539+ .setProcessingMode (ProcessingMode .newBuilder ()
8540+ .setRequestHeaderMode (ProcessingMode .HeaderSendMode .SKIP ) // SKIP so data plane call starts immediately
8541+ .setRequestBodyMode (ProcessingMode .BodySendMode .GRPC ) // GRPC body mode to trigger REQUEST_BODY
8542+ .setResponseHeaderMode (ProcessingMode .HeaderSendMode .SEND ) // SEND to trigger RESPONSE_HEADERS
8543+ .build ())
8544+ .build ();
8545+ ExternalProcessorFilterConfig filterConfig =
8546+ provider .parseFilterConfig (Any .pack (proto ), filterContext ).config ;
8547+
8548+ CachedChannelManager channelManager = new CachedChannelManager (config -> {
8549+ return grpcCleanup .register (InProcessChannelBuilder .forName (uniqueExtProcServerName )
8550+ .executor (bidiTestExecutor )
8551+ .build ());
8552+ });
8553+
8554+ ExternalProcessorInterceptor interceptor = new ExternalProcessorInterceptor (
8555+ filterConfig , channelManager , scheduler , FAKE_CONTEXT );
8556+
8557+ ManagedChannel dataPlaneChannel = grpcCleanup .register (
8558+ InProcessChannelBuilder .forName (uniqueDataPlaneServerName )
8559+ .executor (bidiTestExecutor )
8560+ .build ());
8561+
8562+ ClientCall <String , String > clientCall = interceptCall (interceptor ,
8563+ METHOD_BIDI_STREAMING ,
8564+ DEFAULT_CALL_OPTIONS .withExecutor (bidiTestExecutor ),
8565+ dataPlaneChannel );
8566+
8567+ StreamObserver <String > bidiRequestObserver = ClientCalls .asyncBidiStreamingCall (
8568+ clientCall ,
8569+ new StreamObserver <String >() {
8570+ @ Override public void onNext (String value ) {}
8571+ @ Override public void onError (Throwable t ) {}
8572+ @ Override public void onCompleted () {
8573+ allDoneLatch .countDown ();
8574+ }
8575+ });
8576+
8577+ // Send client message to trigger REQUEST_BODY to ext_proc
8578+ bidiRequestObserver .onNext ("ClientMsg" );
8579+
8580+ // Wait for ext_proc to process out-of-lockstep events
8581+ assertThat (sidecarRequestBodyLatch .await (10 , TimeUnit .SECONDS )).isTrue ();
8582+ assertThat (sidecarResponseHeadersLatch .await (10 , TimeUnit .SECONDS )).isTrue ();
8583+
8584+ // Complete the bidi stream
8585+ bidiRequestObserver .onCompleted ();
8586+ assertThat (allDoneLatch .await (10 , TimeUnit .SECONDS )).isTrue ();
8587+
8588+ // Clean up by cancelling the call explicitly
8589+ clientCall .cancel ("Test finished" , null );
8590+
8591+ channelManager .close ();
8592+ bidiTestExecutor .shutdown ();
8593+ }
8594+
84318595 // --- Category 19: Header Response Status Checks ---
84328596
84338597 @ Test
0 commit comments