Skip to content

Commit 53ebe2d

Browse files
xds: Fix half-close race condition in ext-proc client interceptor (#12888)
Resolve a protocol violation bug in ExternalProcessorClientInterceptor and fix a related data race in its fail-open unit tests: 1. Interceptor Bug Fix (Incorrect Frame Order): When the external processor stream fails (extProcStreamState is FAILED/completed) but the interceptor has not yet transitioned to pass-through mode, a call to sendMessage() is buffered, but a call to halfClose() immediately executed super.halfClose() because it only checked stream completion. This sent the halfClose frame to the backend server before the buffered message was drained, violating the gRPC protocol and causing the backend to drop the message. Fixed by modifying halfClose() to only execute immediately if passThroughMode is true. If passThroughMode is false and the stream is completed/failed, halfClose is buffered (pendingHalfClose = true) and deferred until the draining task drains the buffered messages and triggers it. 2. Test Fix (Deterministic Race Simulation): Redesigned givenFailureModeAllowTrue_whenExtProcStreamFails_thenCallFailsOpen in ExternalProcessorClientInterceptorTest to deterministically simulate the interleaving that caused the bug. Used a custom ServerInterceptor on the backend server to suspend the async onError thread inside activateLine() (midway through fail-open processing) while the client main thread calls sendMessage() and halfClose(). This forces the race condition and verifies that the message is no longer dropped.
1 parent 0cf5239 commit 53ebe2d

2 files changed

Lines changed: 48 additions & 13 deletions

File tree

xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -841,7 +841,7 @@ private void proceedWithHalfClose() {
841841
@Override
842842
public void halfClose() {
843843
clientHalfCloseStartNanos = System.nanoTime();
844-
if (passThroughMode.get() || extProcStreamState.get().isCompleted()) {
844+
if (passThroughMode.get()) {
845845
if (requestSideClosed.compareAndSet(false, true)) {
846846
proceedWithHalfClose();
847847
}
@@ -850,6 +850,10 @@ public void halfClose() {
850850

851851
pendingHalfClose.set(true);
852852

853+
if (extProcStreamState.get().isCompleted()) {
854+
return;
855+
}
856+
853857
if (extProcStreamState.get().isDraining()) {
854858
return;
855859
}
@@ -867,8 +871,6 @@ public void halfClose() {
867871
.setEndOfStreamWithoutMessage(true)
868872
.build())
869873
.build());
870-
871-
// Defer super.halfClose() until ext-proc response signals end_of_stream.
872874
}
873875

874876
@Override

xds/src/test/java/io/grpc/xds/ExternalProcessorClientInterceptorTest.java

Lines changed: 43 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -8460,22 +8460,43 @@ public void onCompleted() {
84608460
filterConfig, channelManager, scheduler, FAKE_CONTEXT);
84618461

84628462
final CountDownLatch dataPlaneLatch = new CountDownLatch(1);
8463-
dataPlaneServiceRegistry.addService(ServerServiceDefinition.builder("test.TestService")
8464-
.addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall(
8465-
(request, responseObserver) -> {
8466-
responseObserver.onNext("Hello " + request);
8467-
responseObserver.onCompleted();
8468-
dataPlaneLatch.countDown();
8469-
}))
8470-
.build());
8463+
final CountDownLatch headersReceivedLatch = new CountDownLatch(1);
8464+
final CountDownLatch resumeAsyncThreadLatch = new CountDownLatch(1);
8465+
8466+
ServerInterceptor dataPlaneInterceptor = new ServerInterceptor() {
8467+
@Override
8468+
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
8469+
ServerCall<ReqT, RespT> call, Metadata headers, ServerCallHandler<ReqT, RespT> next) {
8470+
headersReceivedLatch.countDown();
8471+
try {
8472+
resumeAsyncThreadLatch.await();
8473+
} catch (InterruptedException e) {
8474+
Thread.currentThread().interrupt();
8475+
}
8476+
return next.startCall(call, headers);
8477+
}
8478+
};
8479+
8480+
dataPlaneServiceRegistry.addService(ServerInterceptors.intercept(
8481+
ServerServiceDefinition.builder("test.TestService")
8482+
.addMethod(METHOD_SAY_HELLO, ServerCalls.asyncUnaryCall(
8483+
(request, responseObserver) -> {
8484+
responseObserver.onNext("Hello " + request);
8485+
responseObserver.onCompleted();
8486+
dataPlaneLatch.countDown();
8487+
}))
8488+
.build(),
8489+
dataPlaneInterceptor));
84718490

84728491
ManagedChannel dataPlaneChannel = grpcCleanup.register(
84738492
InProcessChannelBuilder.forName(dataPlaneServerName).directExecutor().build());
84748493

8494+
final AtomicReference<Status> statusRef = new AtomicReference<>();
84758495
final CountDownLatch closedLatch = new CountDownLatch(1);
84768496
ClientCall.Listener<String> appListener = new ClientCall.Listener<String>() {
84778497
@Override
84788498
public void onClose(Status status, Metadata trailers) {
8499+
statusRef.set(status);
84798500
closedLatch.countDown();
84808501
}
84818502
};
@@ -8485,15 +8506,27 @@ public void onClose(Status status, Metadata trailers) {
84858506
interceptCall(interceptor, METHOD_SAY_HELLO, callOptions, dataPlaneChannel);
84868507
proxyCall.start(appListener, new Metadata());
84878508

8488-
// Send message and half-close to trigger unary call reaching server
8509+
// Trigger unary call. request(1) starts it.
84898510
proxyCall.request(1);
8511+
8512+
// Wait for the async sidecar thread to enter activateCall() and block inside interceptCall
8513+
assertThat(headersReceivedLatch.await(5, TimeUnit.SECONDS)).isTrue();
8514+
8515+
// Now, while the async thread is blocked (and passThroughMode is still false),
8516+
// send a message and half-close.
84908517
proxyCall.sendMessage("test");
84918518
proxyCall.halfClose();
84928519

8520+
// Unblock the async thread
8521+
resumeAsyncThreadLatch.countDown();
8522+
84938523
// Verify data plane call reached (failed open)
84948524
assertThat(dataPlaneLatch.await(5, TimeUnit.SECONDS)).isTrue();
8525+
8526+
// Verify client call completes successfully
8527+
assertThat(closedLatch.await(5, TimeUnit.SECONDS)).isTrue();
8528+
assertThat(statusRef.get().isOk()).isTrue();
84958529

8496-
proxyCall.cancel("Cleanup", null);
84978530
channelManager.close();
84988531
}
84998532

0 commit comments

Comments
 (0)