Skip to content

Commit a5417a7

Browse files
committed
xds: stabilize ext_proc server interceptor stream termination and protocol compliance
Refines the stream termination state machine of the ExternalProcessorServerInterceptor to achieve full gRFC protocol compliance and resolve concurrency timing races: - Corrects skipped trailers protocol flow: Eliminates the transmission of terminal empty ProcessingRequest messages to the ext_proc engine when responseTrailerMode is set to SKIP, immediately unblocking the call and closing the ext_proc stream per gRFC specifications. - Gated stream termination: Refactors checkEndOfStream() to strictly avoid premature stream closure by verifying that the interceptor is not actively waiting for a 1-1 response header or RESPONSE_TRAILERS event, accommodating the non-1-1 cardinality of body chunks. - Race condition mitigation: Introduces isRequestSideCompleted() as a guard to ensure that early server-side application completions (e.g., immediate auth errors) defer final call and stream teardown until all pending request-side client messages are fully processed. - Forwards empty mutated bodies: Reverts redundant empty body checks in handleResponseBodyResponse() to correctly deliver legitimate empty body mutation payloads. - Updates test harnesses: Ensures all 50 tests in ExternalProcessorServerInterceptorTest stably pass under high concurrency and match the refined handshake protocol sequence.
1 parent c766293 commit a5417a7

3 files changed

Lines changed: 606 additions & 297 deletions

File tree

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,11 @@ private static ConfigOrError<Optional<GrpcServiceConfig>> parseAndValidate(
348348
return ConfigOrError.fromError("Invalid response_body_mode: " + mode.getResponseBodyMode()
349349
+ ". Only GRPC and NONE are supported.");
350350
}
351+
if (mode.getResponseBodyMode() == ProcessingMode.BodySendMode.GRPC
352+
&& mode.getResponseTrailerMode() != ProcessingMode.HeaderSendMode.SEND) {
353+
return ConfigOrError.fromError("Invalid response_trailer_mode: " + mode.getResponseTrailerMode()
354+
+ ". Trailer mode must be SEND if response body mode is GRPC.");
355+
}
351356

352357
try {
353358
if (grpcService != null && grpcService.hasGoogleGrpc()) {

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

Lines changed: 73 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,6 @@
105105
final class ExternalProcessorServerInterceptor implements ServerInterceptor {
106106
private final CachedChannelManager cachedChannelManager;
107107
private final ExternalProcessorFilterConfig filterConfig;
108-
private final FilterContext context;
109108
private final MetricRecorder metricsRecorder;
110109

111110
ExternalProcessorServerInterceptor(
@@ -114,7 +113,6 @@ final class ExternalProcessorServerInterceptor implements ServerInterceptor {
114113
FilterContext context) {
115114
this.filterConfig = checkNotNull(filterConfig, "filterConfig");
116115
this.cachedChannelManager = checkNotNull(cachedChannelManager, "cachedChannelManager");
117-
this.context = checkNotNull(context, "context");
118116
this.metricsRecorder = checkNotNull(context.metricsRecorder(), "metricsRecorder");
119117
}
120118

@@ -332,7 +330,9 @@ private enum EventType {
332330
new AtomicReference<>(ExtProcStreamState.ACTIVE);
333331
final AtomicBoolean passThroughMode = new AtomicBoolean(false);
334332
final AtomicBoolean halfClosed = new AtomicBoolean(false);
335-
final AtomicBoolean responseSideClosed = new AtomicBoolean(false);
333+
final AtomicBoolean requestSideClosed = new AtomicBoolean(false);
334+
final AtomicBoolean dataPlaneCallClosed = new AtomicBoolean(false);
335+
336336
final AtomicBoolean isProcessingTrailers = new AtomicBoolean(false);
337337
final AtomicBoolean responseHeadersSent = new AtomicBoolean(false);
338338
final AtomicBoolean terminationTriggered = new AtomicBoolean(false);
@@ -440,6 +440,9 @@ private void activateCall() {
440440
wrappedListener.setDelegate(appListener);
441441
drainPendingRequests();
442442
wrappedListener.onReadyNotify();
443+
if (wrappedListener.halfCloseDeferred) {
444+
wrappedListener.handleDeferredHalfClose();
445+
}
443446
}
444447

445448
private void recordDuration(DoubleHistogramMetricInstrument instrument, long durationNanos) {
@@ -618,7 +621,7 @@ else if (response.hasResponseTrailers()) {
618621
proceedWithClose();
619622
}
620623

621-
checkEndOfStream(response);
624+
checkEndOfStream();
622625
} catch (Throwable t) {
623626
internalOnError(t);
624627
}
@@ -712,7 +715,9 @@ private void closeExtProcStream() {
712715
extProcClientCallRequestObserver.onCompleted();
713716
}
714717
}
718+
expectedResponses.clear();
715719
}
720+
proceedWithClose();
716721
}
717722

718723
private void cancelExtProcStream(Throwable t) {
@@ -726,6 +731,8 @@ private void cancelExtProcStream(Throwable t) {
726731
extProcClientCallRequestObserver = null;
727732
}
728733
}
734+
expectedResponses.clear();
735+
proceedWithClose();
729736
}
730737
}
731738

@@ -740,6 +747,7 @@ private void internalOnError(Throwable t) {
740747
extProcClientCallRequestObserver = null;
741748
}
742749
}
750+
expectedResponses.clear();
743751
if (config.getFailureModeAllow()) {
744752
handleFailOpen();
745753
} else {
@@ -860,7 +868,7 @@ private void proceedWithSendHeaders(Metadata headers) {
860868

861869
@Override
862870
public void sendMessage(InputStream message) {
863-
if (responseSideClosed.get()) {
871+
if (dataPlaneCallClosed.get()) {
864872
return;
865873
}
866874

@@ -959,7 +967,8 @@ public void close(Status status, Metadata trailers) {
959967

960968
void proceedWithClose() {
961969
synchronized (rawCallLock) {
962-
if (savedStatus != null) {
970+
if (savedStatus != null
971+
&& (isExtProcStreamCompleted() || config.getObservabilityMode())) {
963972
if (markDataPlaneCallClosed()) {
964973
proceedWithClose(savedStatus, savedTrailers);
965974
}
@@ -970,12 +979,14 @@ void proceedWithClose() {
970979
}
971980

972981
private void proceedWithClose(Status status, Metadata trailers) {
973-
if (serverTrailersStartNanos > 0) {
974-
long durationNanos = System.nanoTime() - serverTrailersStartNanos;
975-
recordDuration(serverTrailersDuration, durationNanos);
976-
serverTrailersStartNanos = 0;
982+
if (dataPlaneCallClosed.compareAndSet(false, true)) {
983+
if (serverTrailersStartNanos > 0) {
984+
long durationNanos = System.nanoTime() - serverTrailersStartNanos;
985+
recordDuration(serverTrailersDuration, durationNanos);
986+
serverTrailersStartNanos = 0;
987+
}
988+
super.close(status, trailers);
977989
}
978-
super.close(status, trailers);
979990
}
980991

981992
private void triggerCloseHandshake() {
@@ -994,14 +1005,9 @@ private void triggerCloseHandshake() {
9941005
.build())
9951006
.build());
9961007
} else {
997-
sendToExtProc(ProcessingRequest.newBuilder()
998-
.setResponseBody(HttpBody.newBuilder()
999-
.setEndOfStreamWithoutMessage(true)
1000-
.build())
1001-
.build());
1002-
1003-
if (config.getObservabilityMode()) {
1004-
proceedWithClose();
1008+
if (isRequestSideCompleted()) {
1009+
unblockAfterStreamComplete();
1010+
closeExtProcStream();
10051011
}
10061012
}
10071013
}
@@ -1024,11 +1030,12 @@ private void sendResponseBodyToExtProc(@Nullable ByteString bodyByteString, bool
10241030
}
10251031

10261032
private void handleRequestBodyResponse(BodyResponse bodyResponse) {
1027-
if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1033+
if (bodyResponse.hasResponse()
1034+
&& bodyResponse.getResponse().hasBodyMutation()) {
10281035
BodyMutation mutation = bodyResponse.getResponse().getBodyMutation();
10291036
if (mutation.hasStreamedResponse()) {
10301037
StreamedBodyResponse streamed = mutation.getStreamedResponse();
1031-
if (!streamed.getBody().isEmpty()) {
1038+
if (!streamed.getEndOfStreamWithoutMessage()) {
10321039
wrappedListener.onExternalBody(streamed.getBody());
10331040
}
10341041
if (streamed.getEndOfStream() || streamed.getEndOfStreamWithoutMessage()) {
@@ -1039,18 +1046,14 @@ private void handleRequestBodyResponse(BodyResponse bodyResponse) {
10391046
}
10401047

10411048
private void handleResponseBodyResponse(BodyResponse bodyResponse) {
1049+
if (dataPlaneCallClosed.get()) {
1050+
return;
1051+
}
10421052
if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
10431053
BodyMutation mutation = bodyResponse.getResponse().getBodyMutation();
10441054
if (mutation.hasStreamedResponse()) {
10451055
StreamedBodyResponse streamed = mutation.getStreamedResponse();
1046-
if (!streamed.getBody().isEmpty()) {
1047-
super.sendMessage(new OutboundZeroCopyInputStream(streamed.getBody()));
1048-
}
1049-
}
1050-
}
1051-
if (responseSideClosed.compareAndSet(false, true)) {
1052-
if (terminationTriggered.get()) {
1053-
proceedWithClose();
1056+
super.sendMessage(new OutboundZeroCopyInputStream(streamed.getBody()));
10541057
}
10551058
}
10561059
}
@@ -1097,23 +1100,20 @@ private void handleFailOpen() {
10971100
closeExtProcStream();
10981101
}
10991102

1100-
private void checkEndOfStream(ProcessingResponse response) {
1101-
boolean terminal = false;
1102-
if (response.hasResponseTrailers()) {
1103-
terminal = true;
1104-
} else if (response.hasResponseHeaders() && savedStatus != null
1105-
&& currentProcessingMode.getResponseBodyMode() == ProcessingMode.BodySendMode.NONE) {
1106-
terminal = true;
1107-
} else if (response.hasResponseBody() && responseSideClosed.get()) {
1108-
terminal = true;
1109-
}
1110-
1111-
if (terminal) {
1103+
private void checkEndOfStream() {
1104+
if (terminationTriggered.get() && isRequestSideCompleted()
1105+
&& !expectedResponses.contains(EventType.RESPONSE_TRAILERS)) {
11121106
unblockAfterStreamComplete();
11131107
closeExtProcStream();
11141108
}
11151109
}
11161110

1111+
private boolean isRequestSideCompleted() {
1112+
return (currentProcessingMode.getRequestHeaderMode() != ProcessingMode.HeaderSendMode.SEND
1113+
&& currentProcessingMode.getRequestBodyMode() != ProcessingMode.BodySendMode.GRPC)
1114+
|| requestSideClosed.get();
1115+
}
1116+
11171117
void unblockAfterStreamComplete() {
11181118
proceedWithSendHeaders();
11191119
drainPendingDrainingMessages();
@@ -1125,6 +1125,7 @@ private static final class DataPlaneServerListener extends ServerCall.Listener<I
11251125
private final DataPlaneServerCall dataPlaneServerCall;
11261126
private final Queue<InputStream> savedMessages = new ConcurrentLinkedQueue<>();
11271127
private volatile boolean halfCloseReceived;
1128+
private volatile boolean halfCloseDeferred;
11281129
private volatile ServerCall.Listener<InputStream> delegate;
11291130

11301131
private DataPlaneServerListener(DataPlaneServerCall dataPlaneServerCall) {
@@ -1164,6 +1165,9 @@ void onReadyNotify() {
11641165
@Override
11651166
public void onMessage(InputStream message) {
11661167
dataPlaneServerCall.delegateExecutor.execute(() -> {
1168+
if (dataPlaneServerCall.requestSideClosed.get()) {
1169+
return;
1170+
}
11671171
ServerCall.Listener<InputStream> del = delegate;
11681172
if (dataPlaneServerCall.passThroughMode.get() && del != null) {
11691173
dataPlaneServerCall.callContext.run(() -> del.onMessage(message));
@@ -1177,10 +1181,10 @@ public void onMessage(InputStream message) {
11771181
!= ProcessingMode.BodySendMode.GRPC
11781182
|| dataPlaneServerCall.config.getObservabilityMode()) {
11791183

1180-
if (dataPlaneServerCall.dataPlaneCallState.get() == DataPlaneCallState.IDLE) {
1181-
// We must buffer because the application call hasn't started yet
1184+
if (del == null) {
1185+
// We must buffer because the application call hasn't started yet or delegate is not set
11821186
savedMessages.add(message);
1183-
} else if (del != null) {
1187+
} else {
11841188
dataPlaneServerCall.callContext.run(() -> del.onMessage(message));
11851189
}
11861190
return;
@@ -1201,6 +1205,9 @@ public void onMessage(InputStream message) {
12011205
@Override
12021206
public void onHalfClose() {
12031207
dataPlaneServerCall.delegateExecutor.execute(() -> {
1208+
if (dataPlaneServerCall.requestSideClosed.get()) {
1209+
return;
1210+
}
12041211
dataPlaneServerCall.clientHalfCloseStartNanos = System.nanoTime();
12051212
dataPlaneServerCall.halfClosed.set(true);
12061213
ServerCall.Listener<InputStream> del = delegate;
@@ -1210,7 +1217,7 @@ public void onHalfClose() {
12101217
}
12111218

12121219
if (dataPlaneServerCall.dataPlaneCallState.get() == DataPlaneCallState.IDLE) {
1213-
halfCloseReceived = true;
1220+
halfCloseDeferred = true;
12141221
return;
12151222
}
12161223

@@ -1223,7 +1230,22 @@ public void onHalfClose() {
12231230
});
12241231
}
12251232

1233+
void handleDeferredHalfClose() {
1234+
dataPlaneServerCall.delegateExecutor.execute(() -> {
1235+
if (dataPlaneServerCall.currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE
1236+
|| dataPlaneServerCall.isExtProcStreamCompleted()) {
1237+
proceedWithHalfClose();
1238+
} else {
1239+
sendRequestBodyToExtProc(null, true);
1240+
}
1241+
});
1242+
}
1243+
12261244
void proceedWithHalfClose() {
1245+
if (!dataPlaneServerCall.requestSideClosed.compareAndSet(false, true)) {
1246+
return;
1247+
}
1248+
halfCloseReceived = true;
12271249
if (dataPlaneServerCall.clientHalfCloseStartNanos > 0) {
12281250
long durationNanos = System.nanoTime() - dataPlaneServerCall.clientHalfCloseStartNanos;
12291251
dataPlaneServerCall.recordDuration(clientHalfCloseDuration, durationNanos);
@@ -1233,12 +1255,15 @@ void proceedWithHalfClose() {
12331255
if (del != null) {
12341256
dataPlaneServerCall.callContext.run(del::onHalfClose);
12351257
}
1258+
dataPlaneServerCall.checkEndOfStream();
12361259
}
12371260

12381261
void onExternalBody(ByteString body) {
12391262
ServerCall.Listener<InputStream> del = delegate;
12401263
if (del != null) {
12411264
dataPlaneServerCall.callContext.run(() -> del.onMessage(new InboundZeroCopyInputStream(body)));
1265+
} else {
1266+
savedMessages.add(new InboundZeroCopyInputStream(body));
12421267
}
12431268
}
12441269

@@ -1252,8 +1277,10 @@ private void sendRequestBodyToExtProc(@Nullable ByteString bodyByteString, boole
12521277
HttpBody.Builder bodyBuilder = HttpBody.newBuilder();
12531278
if (bodyByteString != null) {
12541279
bodyBuilder.setBody(bodyByteString);
1280+
bodyBuilder.setEndOfStream(endOfStream);
1281+
} else {
1282+
bodyBuilder.setEndOfStreamWithoutMessage(true);
12551283
}
1256-
bodyBuilder.setEndOfStream(endOfStream);
12571284

12581285
dataPlaneServerCall.sendToExtProc(ProcessingRequest.newBuilder()
12591286
.setRequestBody(bodyBuilder.build())

0 commit comments

Comments
 (0)