• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

grpc / grpc-java / #20343

01 Jul 2026 04:40PM UTC coverage: 89.118% (+0.02%) from 89.099%
#20343

push

github

web-flow
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.

37983 of 42621 relevant lines covered (89.12%)

0.89 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

96.29
/../xds/src/main/java/io/grpc/xds/ExternalProcessorClientInterceptor.java
1
/*
2
 * Copyright 2024 The gRPC Authors
3
 *
4
 * Licensed under the Apache License, Version 2.0 (the "License");
5
 * you may not use this file except in compliance with the License.
6
 * You may obtain a copy of the License at
7
 *
8
 *     http://www.apache.org/licenses/LICENSE-2.0
9
 *
10
 * Unless required by applicable law or agreed to in writing, software
11
 * distributed under the License is distributed on an "AS IS" BASIS,
12
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
 * See the License for the specific language governing permissions and
14
 * limitations under the License.
15
 */
16

17
package io.grpc.xds;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.applyHeaderMutations;
21
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.collectAttributes;
22
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.markDataPlaneCallClosed;
23
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.markExtProcStreamCompleted;
24
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.markExtProcStreamFailed;
25
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.outboundStreamToByteString;
26
import static io.grpc.xds.internal.extproc.ExternalProcessorUtil.toHeaderMap;
27

28
import com.google.common.annotations.VisibleForTesting;
29
import com.google.common.collect.ImmutableList;
30
import com.google.common.collect.ImmutableMap;
31
import com.google.protobuf.ByteString;
32
import com.google.protobuf.Struct;
33
import io.envoyproxy.envoy.extensions.filters.http.ext_proc.v3.ProcessingMode;
34
import io.envoyproxy.envoy.service.ext_proc.v3.BodyMutation;
35
import io.envoyproxy.envoy.service.ext_proc.v3.BodyResponse;
36
import io.envoyproxy.envoy.service.ext_proc.v3.CommonResponse;
37
import io.envoyproxy.envoy.service.ext_proc.v3.ExternalProcessorGrpc;
38
import io.envoyproxy.envoy.service.ext_proc.v3.HttpBody;
39
import io.envoyproxy.envoy.service.ext_proc.v3.HttpHeaders;
40
import io.envoyproxy.envoy.service.ext_proc.v3.HttpTrailers;
41
import io.envoyproxy.envoy.service.ext_proc.v3.ImmediateResponse;
42
import io.envoyproxy.envoy.service.ext_proc.v3.ProcessingRequest;
43
import io.envoyproxy.envoy.service.ext_proc.v3.ProcessingResponse;
44
import io.envoyproxy.envoy.service.ext_proc.v3.ProtocolConfiguration;
45
import io.envoyproxy.envoy.service.ext_proc.v3.StreamedBodyResponse;
46
import io.grpc.CallOptions;
47
import io.grpc.Channel;
48
import io.grpc.ClientCall;
49
import io.grpc.ClientInterceptor;
50
import io.grpc.Context;
51
import io.grpc.Deadline;
52
import io.grpc.DoubleHistogramMetricInstrument;
53
import io.grpc.ForwardingClientCall.SimpleForwardingClientCall;
54
import io.grpc.ForwardingClientCallListener.SimpleForwardingClientCallListener;
55
import io.grpc.ManagedChannel;
56
import io.grpc.Metadata;
57
import io.grpc.MethodDescriptor;
58
import io.grpc.MetricInstrumentRegistry;
59
import io.grpc.MetricRecorder;
60
import io.grpc.Status;
61
import io.grpc.StatusRuntimeException;
62
import io.grpc.internal.DelayedClientCall;
63
import io.grpc.internal.SerializingExecutor;
64
import io.grpc.stub.ClientCallStreamObserver;
65
import io.grpc.stub.ClientResponseObserver;
66
import io.grpc.xds.ExternalProcessorFilter.ExternalProcessorFilterConfig;
67
import io.grpc.xds.Filter.FilterContext;
68
import io.grpc.xds.internal.extproc.DataPlaneCallState;
69
import io.grpc.xds.internal.extproc.EventType;
70
import io.grpc.xds.internal.extproc.ExtProcStreamState;
71
import io.grpc.xds.internal.extproc.KnownLengthInputStream;
72
import io.grpc.xds.internal.grpcservice.CachedChannelManager;
73
import io.grpc.xds.internal.grpcservice.HeaderValue;
74
import io.grpc.xds.internal.headermutations.HeaderMutationDisallowedException;
75
import io.grpc.xds.internal.headermutations.HeaderMutationFilter;
76
import io.grpc.xds.internal.headermutations.HeaderMutationRulesConfig;
77
import io.grpc.xds.internal.headermutations.HeaderMutator;
78
import java.io.IOException;
79
import java.io.InputStream;
80
import java.util.List;
81
import java.util.Optional;
82
import java.util.Queue;
83
import java.util.concurrent.ConcurrentLinkedQueue;
84
import java.util.concurrent.Executor;
85
import java.util.concurrent.ScheduledExecutorService;
86
import java.util.concurrent.ScheduledFuture;
87
import java.util.concurrent.TimeUnit;
88
import java.util.concurrent.atomic.AtomicBoolean;
89
import java.util.concurrent.atomic.AtomicInteger;
90
import java.util.concurrent.atomic.AtomicReference;
91
import javax.annotation.Nullable;
92

93
/**
94
 * Client-side interceptor for external processing filter.
95
 */
96
final class ExternalProcessorClientInterceptor implements ClientInterceptor {
97

98
  @VisibleForTesting
99
  static DoubleHistogramMetricInstrument clientHeadersDuration;
100
  @VisibleForTesting
101
  static DoubleHistogramMetricInstrument clientHalfCloseDuration;
102
  @VisibleForTesting
103
  static DoubleHistogramMetricInstrument serverHeadersDuration;
104
  @VisibleForTesting
105
  static DoubleHistogramMetricInstrument serverTrailersDuration;
106

107
  // Copied from io.grpc.opentelemetry.internal.OpenTelemetryConstants.LATENCY_BUCKETS
108
  private static final List<Double> LATENCY_BUCKETS = ImmutableList.of(
1✔
109
      0d,     0.00001d, 0.00005d, 0.0001d, 0.0003d, 0.0006d, 0.0008d, 0.001d, 0.002d,
1✔
110
      0.003d, 0.004d,   0.005d,   0.006d,  0.008d,  0.01d,   0.013d,  0.016d, 0.02d,
1✔
111
      0.025d, 0.03d,    0.04d,    0.05d,   0.065d,  0.08d,   0.1d,    0.13d,  0.16d,
1✔
112
      0.2d,   0.25d,    0.3d,     0.4d,    0.5d,    0.65d,   0.8d,    1d,     2d,
1✔
113
      5d,     10d,      20d,      50d,     100d);
1✔
114

115
  static {
116
    initMetricInstruments();
1✔
117
  }
1✔
118

119
  static synchronized void initMetricInstruments() {
120
    if (io.grpc.internal.GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false)) {
1✔
121
      if (clientHeadersDuration == null) {
1✔
122
        MetricInstrumentRegistry registry = MetricInstrumentRegistry.getDefaultRegistry();
1✔
123

124
        clientHeadersDuration = registry.registerDoubleHistogram(
1✔
125
            "grpc.client_ext_proc.client_headers_duration",
126
            "Time between when the ext_proc filter sees the client's headers and when "
127
                + "it allows those headers to continue on to the next filter",
128
            "s",
129
            LATENCY_BUCKETS,
130
            ImmutableList.of("grpc.target"),
1✔
131
            ImmutableList.of("grpc.lb.backend_service"),
1✔
132
            true);
133

134
        clientHalfCloseDuration = registry.registerDoubleHistogram(
1✔
135
            "grpc.client_ext_proc.client_half_close_duration",
136
            "Time between when the ext_proc filter sees the client's half-close and when "
137
                + "it allows that half-close to continue on to the next filter",
138
            "s",
139
            LATENCY_BUCKETS,
140
            ImmutableList.of("grpc.target"),
1✔
141
            ImmutableList.of("grpc.lb.backend_service"),
1✔
142
            true);
143

144
        serverHeadersDuration = registry.registerDoubleHistogram(
1✔
145
            "grpc.client_ext_proc.server_headers_duration",
146
            "Time between when the ext_proc filter sees the server's headers and when "
147
                + "it allows those headers to continue on to the next filter",
148
            "s",
149
            LATENCY_BUCKETS,
150
            ImmutableList.of("grpc.target"),
1✔
151
            ImmutableList.of("grpc.lb.backend_service"),
1✔
152
            true);
153

154
        serverTrailersDuration = registry.registerDoubleHistogram(
1✔
155
            "grpc.client_ext_proc.server_trailers_duration",
156
            "Time between when the ext_proc filter sees the server's trailers and when "
157
                + "it allows those trailers to continue on to the next filter",
158
            "s",
159
            LATENCY_BUCKETS,
160
            ImmutableList.of("grpc.target"),
1✔
161
            ImmutableList.of("grpc.lb.backend_service"),
1✔
162
            true);
163
      }
164
    }
165
  }
1✔
166

167
  private final ExternalProcessorFilterConfig filterConfig;
168
  private final ScheduledExecutorService scheduler;
169
  private final MetricRecorder metricsRecorder;
170
  private final ManagedChannel extProcChannel;
171

172
  @VisibleForTesting
173
  ExternalProcessorClientInterceptor(ExternalProcessorFilterConfig filterConfig,
174
      CachedChannelManager cachedChannelManager,
175
      ScheduledExecutorService scheduler,
176
      FilterContext context) {
1✔
177
    this.filterConfig = filterConfig;
1✔
178
    checkNotNull(cachedChannelManager, "cachedChannelManager");
1✔
179
    this.scheduler = checkNotNull(scheduler, "scheduler");
1✔
180
    this.metricsRecorder = checkNotNull(context.metricsRecorder(), "metricsRecorder");
1✔
181
    this.extProcChannel = cachedChannelManager.getChannel(filterConfig.getGrpcServiceConfig());
1✔
182
  }
1✔
183

184
  @VisibleForTesting
185
  ExternalProcessorFilterConfig getFilterConfig() {
186
    return filterConfig;
1✔
187
  }
188

189
  @VisibleForTesting
190
  ManagedChannel getExtProcChannel() {
191
    return extProcChannel;
×
192
  }
193

194
  @Override
195
  @SuppressWarnings("unchecked")
196
  public <ReqT, RespT> ClientCall<ReqT, RespT> interceptCall(
197
      MethodDescriptor<ReqT, RespT> method,
198
      CallOptions callOptions,
199
      Channel next) {
200
    SerializingExecutor serializingExecutor = new SerializingExecutor(callOptions.getExecutor());
1✔
201
    
202
    ExternalProcessorGrpc.ExternalProcessorStub extProcStub = ExternalProcessorGrpc.newStub(
1✔
203
        extProcChannel)
204
        .withExecutor(serializingExecutor);
1✔
205
    
206
    if (filterConfig.getGrpcServiceConfig().timeout().isPresent()) {
1✔
207
      long timeoutNanos = filterConfig.getGrpcServiceConfig().timeout().get().toNanos();
1✔
208
      if (timeoutNanos > 0) {
1✔
209
        extProcStub = extProcStub.withDeadlineAfter(timeoutNanos, TimeUnit.NANOSECONDS);
1✔
210
      }
211
    }
212
    if (filterConfig.getGrpcServiceConfig().initialMetadata() != null
1✔
213
        && !filterConfig.getGrpcServiceConfig().initialMetadata().isEmpty()) {
1✔
214
      Metadata extraHeaders = new Metadata();
1✔
215
      for (HeaderValue headerValue : filterConfig.getGrpcServiceConfig().initialMetadata()) {
1✔
216
        String key = headerValue.key();
1✔
217
        if (key.endsWith(Metadata.BINARY_HEADER_SUFFIX)) {
1✔
218
          if (headerValue.rawValue().isPresent()) {
1✔
219
            Metadata.Key<byte[]> metadataKey =
1✔
220
                Metadata.Key.of(key, Metadata.BINARY_BYTE_MARSHALLER);
1✔
221
            extraHeaders.put(metadataKey, headerValue.rawValue().get().toByteArray());
1✔
222
          }
1✔
223
        } else {
224
          if (headerValue.value().isPresent()) {
1✔
225
            Metadata.Key<String> metadataKey =
1✔
226
                Metadata.Key.of(key, Metadata.ASCII_STRING_MARSHALLER);
1✔
227
            extraHeaders.put(metadataKey, headerValue.value().get());
1✔
228
          }
229
        }
230
      }
1✔
231
      extProcStub = extProcStub.withInterceptors(
1✔
232
          io.grpc.stub.MetadataUtils.newAttachHeadersInterceptor(extraHeaders));
1✔
233
    }
234

235

236
    // The filter chain is preceded by RawMessageClientInterceptor, so ReqT and RespT are
237
    // InputStream.
238
    MethodDescriptor<InputStream, InputStream> rawMethod =
1✔
239
        (MethodDescriptor<InputStream, InputStream>) (MethodDescriptor<?, ?>) method;
240
    ClientCall<InputStream, InputStream> rawCall =
1✔
241
        (ClientCall<InputStream, InputStream>) (ClientCall<?, ?>)
242
            next.newCall(method, callOptions);
1✔
243

244
    // Create a local subclass instance to buffer outbound actions
245
    DataPlaneDelayedCall<InputStream, InputStream> delayedCall =
1✔
246
        new DataPlaneDelayedCall<>(
247
            serializingExecutor, scheduler, callOptions.getDeadline());
1✔
248

249
    DataPlaneClientCall dataPlaneCall = new DataPlaneClientCall(
1✔
250
        delayedCall, rawCall, extProcStub, filterConfig, filterConfig.getMutationRulesConfig(),
1✔
251
        scheduler, rawMethod, next, metricsRecorder, next.authority(),
1✔
252
        callOptions.getOption(XdsNameResolver.CLUSTER_SELECTION_KEY));
1✔
253

254
    return (ClientCall<ReqT, RespT>) (ClientCall<?, ?>) dataPlaneCall;
1✔
255
  }
256

257
  // --- SHARED UTILITY METHODS ---
258

259
  /**
260
   * A local subclass to expose the protected constructor of DelayedClientCall.
261
   */
262
  private static class DataPlaneDelayedCall<ReqT, RespT> extends DelayedClientCall<ReqT, RespT> {
263
    DataPlaneDelayedCall(
264
        Executor executor, ScheduledExecutorService scheduler, @Nullable Deadline deadline) {
265
      super("ext_proc", executor, scheduler, deadline);
1✔
266
    }
1✔
267
  }
268

269
  /**
270
   * Handles the bidirectional stream with the External Processor.
271
   * Buffers the actual RPC start until the Ext Proc header response is received.
272
   */
273
  private static class DataPlaneClientCall 
274
      extends SimpleForwardingClientCall<InputStream, InputStream> {
275

276
    private final ExternalProcessorGrpc.ExternalProcessorStub stub;
277
    private final ExternalProcessorFilterConfig config;
278
    private final ClientCall<InputStream, InputStream> rawCall;
279
    private final DataPlaneDelayedCall<InputStream, InputStream> delayedCall;
280
    private final ScheduledExecutorService scheduler;
281
    private final Object streamLock = new Object();
1✔
282
    @Nullable private volatile EventType expectedRequestResponse;
283
    @Nullable private volatile EventType expectedResponseResponse;
284
    @Nullable private volatile ClientCallStreamObserver<ProcessingRequest>
285
        extProcClientCallRequestObserver;
286
    private final Queue<InputStream> pendingDrainingMessages =
1✔
287
        new ConcurrentLinkedQueue<>();
288
    @Nullable private volatile DataPlaneListener wrappedListener;
289
    private final HeaderMutationFilter mutationFilter;
290
    private final HeaderMutator mutator = HeaderMutator.create();
1✔
291
    private final AtomicInteger pendingRequests = new AtomicInteger(0);
1✔
292
    private final ProcessingMode currentProcessingMode;
293
    private final MethodDescriptor<?, ?> method;
294
    private final Channel channel;
295
    private final MetricRecorder metricsRecorder;
296
    private final String target;
297
    private final String backendService;
298
    private volatile Context callContext = Context.ROOT;
1✔
299

300
    private long clientHeadersStartNanos;
301
    private long clientHalfCloseStartNanos;
302
    private long serverHeadersStartNanos;
303
    private long serverTrailersStartNanos;
304

305
    private boolean protocolConfigSent = false;
1✔
306
    private ImmutableMap<String, Struct> collectedAttributes;
307
    private boolean requestAttributesSent = false;
1✔
308
    @Nullable private volatile Metadata requestHeaders;
309
    final AtomicReference<DataPlaneCallState> dataPlaneCallState =
1✔
310
        new AtomicReference<>(DataPlaneCallState.IDLE);
311
    final AtomicReference<ExtProcStreamState> extProcStreamState =
1✔
312
        new AtomicReference<>(ExtProcStreamState.ACTIVE);
313
    final AtomicBoolean passThroughMode = new AtomicBoolean(false);
1✔
314
    final AtomicBoolean requestSideClosed = new AtomicBoolean(false);
1✔
315
    final AtomicBoolean isProcessingTrailers = new AtomicBoolean(false);
1✔
316
    final AtomicBoolean pendingHalfClose = new AtomicBoolean(false);
1✔
317
    final AtomicBoolean bodyMessageSentToExtProc = new AtomicBoolean(false);
1✔
318

319
    protected DataPlaneClientCall(
320
        DataPlaneDelayedCall<InputStream, InputStream> delayedCall,
321
        ClientCall<InputStream, InputStream> rawCall,
322
        ExternalProcessorGrpc.ExternalProcessorStub stub,
323
        ExternalProcessorFilterConfig config,
324
        Optional<HeaderMutationRulesConfig> mutationRulesConfig,
325
        ScheduledExecutorService scheduler,
326
        MethodDescriptor<?, ?> method,
327
        Channel channel,
328
        MetricRecorder metricsRecorder,
329
        String target,
330
        String backendService) {
331
      super(delayedCall);
1✔
332
      this.delayedCall = delayedCall;
1✔
333
      this.rawCall = rawCall;
1✔
334
      this.stub = stub;
1✔
335
      this.config = config;
1✔
336
      this.currentProcessingMode = config.getExternalProcessor().getProcessingMode();
1✔
337
      this.mutationFilter = new HeaderMutationFilter(mutationRulesConfig);
1✔
338
      this.scheduler = scheduler;
1✔
339
      this.method = method;
1✔
340
      this.channel = channel;
1✔
341
      this.metricsRecorder = checkNotNull(metricsRecorder, "metricsRecorder");
1✔
342
      this.target = checkNotNull(target, "target");
1✔
343
      this.backendService = checkNotNull(backendService, "backendService");
1✔
344
    }
1✔
345

346

347

348
    private void activateCall() {
349
      if ((extProcStreamState.get() == ExtProcStreamState.FAILED
1✔
350
              && !config.getFailureModeAllow()
1✔
351
              && !config.getObservabilityMode())
1✔
352
          || !dataPlaneCallState.compareAndSet(
1✔
353
              DataPlaneCallState.IDLE, DataPlaneCallState.ACTIVE)) {
354
        return;
1✔
355
      }
356
      if (clientHeadersStartNanos > 0) {
1✔
357
        long durationNanos = System.nanoTime() - clientHeadersStartNanos;
1✔
358
        recordDuration(clientHeadersDuration, durationNanos);
1✔
359
        clientHeadersStartNanos = 0;
1✔
360
      }
361
      Runnable toRun = delayedCall.setCall(rawCall);
1✔
362
      if (toRun != null) {
1✔
363
        callContext.run(toRun);
1✔
364
      }
365
      drainPendingRequests();
1✔
366
      onReadyNotify();
1✔
367
    }
1✔
368

369
    private void recordDuration(DoubleHistogramMetricInstrument instrument, long durationNanos) {
370
      if (instrument != null) {
1✔
371
        double durationSecs = (double) durationNanos / 1_000_000_000.0;
1✔
372
        metricsRecorder.recordDoubleHistogram(
1✔
373
            instrument,
374
            durationSecs,
375
            ImmutableList.of(target),
1✔
376
            ImmutableList.of(backendService));
1✔
377
      }
378
    }
1✔
379

380
    /**
381
     * Validates whether the body response uses unsupported gRPC message compression.
382
     * If compression is unsupported, this method will cancel the call, transition the
383
     * stream to a failed state, send an error to the external processor, and return false.
384
     *
385
     * @param bodyResponse the response to validate
386
     * @return true if validation passes (compression is supported or not used),
387
     *     false if validation fails
388
     */
389
    private boolean validateCompressionSupport(BodyResponse bodyResponse) {
390
      if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1✔
391
        BodyMutation mutation = 
1✔
392
            bodyResponse.getResponse().getBodyMutation();
1✔
393
        if (mutation.hasStreamedResponse()
1✔
394
            && mutation.getStreamedResponse().getGrpcMessageCompressed()) {
1✔
395
          StatusRuntimeException ex = Status.UNAVAILABLE
1✔
396
              .withDescription("gRPC message compression not supported in ext_proc")
1✔
397
              .asRuntimeException();
1✔
398
          synchronized (streamLock) {
1✔
399
            if (!extProcStreamState.get().isCompleted()
1✔
400
                && extProcClientCallRequestObserver != null) {
401
              extProcClientCallRequestObserver.onError(ex);
1✔
402
            }
403
          }
1✔
404
          activateCall();
1✔
405
          markExtProcStreamFailed(extProcStreamState);
1✔
406
          delayedCall.cancel("gRPC message compression not supported in ext_proc", ex);
1✔
407
          closeExtProcStream();
1✔
408
          return false;
1✔
409
        }
410
      }
411
      return true;
1✔
412
    }
413

414

415

416
    @Override
417
    public void start(Listener<InputStream> responseListener, Metadata headers) {
418
      this.callContext = Context.current();
1✔
419
      clientHeadersStartNanos = System.nanoTime();
1✔
420
      this.requestHeaders = headers;
1✔
421
      this.wrappedListener = new DataPlaneListener(responseListener, rawCall, this);
1✔
422

423
      // DelayedClientCall.start will buffer the listener and headers until setCall is called.
424
      super.start(wrappedListener, headers);
1✔
425

426
      stub.process(new ClientResponseObserver<ProcessingRequest, ProcessingResponse>() {
1✔
427
        @Override
428
        public void beforeStart(ClientCallStreamObserver<ProcessingRequest> requestStream) {
429
          synchronized (streamLock) {
1✔
430
            extProcClientCallRequestObserver = requestStream;
1✔
431
          }
1✔
432
          requestStream.setOnReadyHandler(DataPlaneClientCall.this::onExtProcStreamReady);
1✔
433
        }
1✔
434

435
        @Override
436
        public void onNext(ProcessingResponse response) {
437
          try {
438
            if (config.getObservabilityMode()) {
1✔
439
              return;
1✔
440
            }
441

442
            if (response.hasImmediateResponse()) {
1✔
443
              if (config.getDisableImmediateResponse()) {
1✔
444
                internalOnError(Status.UNAVAILABLE
1✔
445
                    .withDescription(
1✔
446
                        "Immediate response is disabled but received from external processor")
447
                    .asRuntimeException());
1✔
448
                return;
1✔
449
              }
450
              handleImmediateResponse(response.getImmediateResponse(), wrappedListener);
1✔
451
              return;
1✔
452
            }
453

454
            if (response.hasRequestHeaders()) {
1✔
455
              EventType expected = expectedRequestResponse;
1✔
456
              if (expected == null || expected != EventType.REQUEST_HEADERS) {
1✔
457
                internalOnError(Status.UNAVAILABLE
×
458
                    .withDescription("Protocol error: received response out of order. Expected: " 
×
459
                        + expected + ", Received: REQUEST_HEADERS")
460
                    .asRuntimeException());
×
461
                return;
×
462
              }
463
              expectedRequestResponse = null;
1✔
464
            } else if (response.hasResponseHeaders()) {
1✔
465
              EventType expected = expectedResponseResponse;
1✔
466
              if (expected == null || expected != EventType.RESPONSE_HEADERS) {
1✔
467
                internalOnError(Status.UNAVAILABLE
1✔
468
                    .withDescription("Protocol error: received response out of order. Expected: " 
1✔
469
                        + expected + ", Received: RESPONSE_HEADERS")
470
                    .asRuntimeException());
1✔
471
                return;
1✔
472
              }
473
              expectedResponseResponse = null;
1✔
474
            } else if (response.hasResponseTrailers()) {
1✔
475
              EventType expected = expectedResponseResponse;
1✔
476
              if (expected == null || expected != EventType.RESPONSE_TRAILERS) {
1✔
477
                internalOnError(Status.UNAVAILABLE
1✔
478
                    .withDescription("Protocol error: received response out of order. Expected: " 
1✔
479
                        + expected + ", Received: RESPONSE_TRAILERS")
480
                    .asRuntimeException());
1✔
481
                return;
1✔
482
              }
483
              expectedResponseResponse = null;
1✔
484
            } else if (response.hasRequestBody()) {
1✔
485
              EventType expected = expectedRequestResponse;
1✔
486
              if (expected == EventType.REQUEST_HEADERS) {
1✔
487
                internalOnError(Status.UNAVAILABLE
1✔
488
                    .withDescription(
1✔
489
                        "Protocol error: received request_body before request_headers response.")
490
                    .asRuntimeException());
1✔
491
                return;
1✔
492
              }
493
            } else if (response.hasResponseBody()) {
1✔
494
              EventType expected = expectedResponseResponse;
1✔
495
              if (expected == EventType.RESPONSE_HEADERS) {
1✔
496
                internalOnError(Status.UNAVAILABLE
1✔
497
                    .withDescription(
1✔
498
                        "Protocol error: received response_body before headers response.")
499
                    .asRuntimeException());
1✔
500
                return;
1✔
501
              }
502
            }
503

504
            if (response.getRequestDrain()) {
1✔
505
              extProcStreamState.set(ExtProcStreamState.DRAINING);
1✔
506
              halfCloseExtProcStream();
1✔
507
              activateCall();
1✔
508
            }
509

510
            // 1. Client Headers
511
            if (response.hasRequestHeaders()) {
1✔
512
              if (response.getRequestHeaders().hasResponse()) {
1✔
513
                if (response.getRequestHeaders().getResponse().getStatus()
1✔
514
                    == CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) {
515
                  internalOnError(Status.UNAVAILABLE
1✔
516
                      .withDescription("CONTINUE_AND_REPLACE is not supported")
1✔
517
                      .asRuntimeException());
1✔
518
                  return;
1✔
519
                }
520
                applyHeaderMutations(
1✔
521
                    requestHeaders,
1✔
522
                    response.getRequestHeaders().getResponse().getHeaderMutation(),
1✔
523
                    mutationFilter,
1✔
524
                    mutator);
1✔
525
              }
526
              activateCall();
1✔
527
            }
528
            // 2. Client Message (Request Body)
529
            else if (response.hasRequestBody()) {
1✔
530
              if (validateCompressionSupport(response.getRequestBody())) {
1✔
531
                handleRequestBodyResponse(response.getRequestBody());
1✔
532
              }
533
            }
534
            // 4. Server Headers
535
            else if (response.hasResponseHeaders()) {
1✔
536
              if (response.getResponseHeaders().hasResponse()) {
1✔
537
                if (response.getResponseHeaders().getResponse().getStatus()
1✔
538
                    == CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) {
539
                  internalOnError(Status.UNAVAILABLE
1✔
540
                      .withDescription("CONTINUE_AND_REPLACE is not supported")
1✔
541
                      .asRuntimeException());
1✔
542
                  return;
1✔
543
                }
544
                Metadata target = wrappedListener.isTrailersOnly()
1✔
545
                    ? wrappedListener.getSavedTrailers() : wrappedListener.getSavedHeaders();
1✔
546
                applyHeaderMutations(
1✔
547
                    target,
548
                    response.getResponseHeaders().getResponse().getHeaderMutation(),
1✔
549
                    mutationFilter,
1✔
550
                    mutator);
1✔
551
              }
552
              if (wrappedListener.isTrailersOnly()) {
1✔
553
                wrappedListener.proceedWithClose();
1✔
554
              } else {
555
                wrappedListener.proceedWithHeaders();
1✔
556
              }
557
            }
558
            // 5. Server Message (Response Body)
559
            else if (response.hasResponseBody()) {
1✔
560
              if (validateCompressionSupport(response.getResponseBody())) {
1✔
561
                handleResponseBodyResponse(response.getResponseBody(), wrappedListener);
1✔
562
              }
563
            }
564
            // 6. Response Trailers
565
            else if (response.hasResponseTrailers()) {
1✔
566
              if (response.getResponseTrailers().hasHeaderMutation()) {
1✔
567
                applyHeaderMutations(
1✔
568
                    wrappedListener.getSavedTrailers(),
1✔
569
                    response.getResponseTrailers().getHeaderMutation(),
1✔
570
                    mutationFilter,
1✔
571
                    mutator);
1✔
572
              }
573
              wrappedListener.proceedWithClose();
1✔
574
            }
575

576
            checkEndOfStream(response);
1✔
577
          } catch (Throwable t) {
1✔
578
            internalOnError(t);
1✔
579
          }
1✔
580
        }
1✔
581

582
        @Override
583
        public void onError(Throwable t) {
584
          if (markExtProcStreamFailed(extProcStreamState)) {
1✔
585
            synchronized (streamLock) {
1✔
586
              extProcClientCallRequestObserver = null;
1✔
587
            }
1✔
588
            if (config.getObservabilityMode()
1✔
589
                || (config.getFailureModeAllow() && !bodyMessageSentToExtProc.get())) {
1✔
590
              handleFailOpen(wrappedListener);
1✔
591
            } else {
592
              String message = "External processor stream failed";
1✔
593
              delayedCall.cancel(message, t);
1✔
594
              wrappedListener.proceedWithClose();
1✔
595
            }
596
          }
597
        }
1✔
598

599
        @Override
600
        public void onCompleted() {
601
          if (markExtProcStreamCompleted(extProcStreamState)) {
1✔
602
            handleFailOpen(wrappedListener);
1✔
603
          }
604
        }
1✔
605
      });
606

607
      this.collectedAttributes = collectAttributes(
1✔
608
          config.getRequestAttributes(), method, channel.authority(), headers);
1✔
609

610
      boolean sendRequestHeaders =
1✔
611
          currentProcessingMode.getRequestHeaderMode() == ProcessingMode.HeaderSendMode.SEND
1✔
612
          || currentProcessingMode.getRequestHeaderMode()
1✔
613
              == ProcessingMode.HeaderSendMode.DEFAULT;
614

615
      if (sendRequestHeaders) {
1✔
616
        sendToExtProc(ProcessingRequest.newBuilder()
1✔
617
            .setRequestHeaders(HttpHeaders.newBuilder()
1✔
618
                .setHeaders(toHeaderMap(headers, config.getForwardRulesConfig()))
1✔
619
                .setEndOfStream(false)
1✔
620
                .build())
1✔
621
            .build());
1✔
622
      }
623

624
      if (config.getObservabilityMode() || !sendRequestHeaders) {
1✔
625
        activateCall();
1✔
626
      }
627
    }
1✔
628

629
    private void sendToExtProc(ProcessingRequest request) {
630
      synchronized (streamLock) {
1✔
631
        if (extProcStreamState.get().isCompleted()) {
1✔
632
          return;
×
633
        }
634
        
635
        if (request.hasRequestHeaders()) {
1✔
636
          expectedRequestResponse = EventType.REQUEST_HEADERS;
1✔
637
        } else if (request.hasResponseHeaders()) {
1✔
638
          expectedResponseResponse = EventType.RESPONSE_HEADERS;
1✔
639
        } else if (request.hasResponseTrailers()) {
1✔
640
          expectedResponseResponse = EventType.RESPONSE_TRAILERS;
1✔
641
        }
642

643
        ProcessingRequest requestToSend = request;
1✔
644
        if (!protocolConfigSent) {
1✔
645
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1✔
646
              .setProtocolConfig(ProtocolConfiguration.newBuilder()
1✔
647
                  .setRequestBodyMode(currentProcessingMode.getRequestBodyMode())
1✔
648
                  .setResponseBodyMode(currentProcessingMode.getResponseBodyMode())
1✔
649
                  .build())
1✔
650
              .build();
1✔
651
          protocolConfigSent = true;
1✔
652
        }
653

654
        boolean isClientServerMessage =
1✔
655
            requestToSend.hasRequestHeaders() || requestToSend.hasRequestBody();
1✔
656
        if (isClientServerMessage
1✔
657
            && !requestAttributesSent
658
            && collectedAttributes != null
659
            && !collectedAttributes.isEmpty()) {
1✔
660
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1✔
661
              .putAllAttributes(collectedAttributes)
1✔
662
              .build();
1✔
663
          requestAttributesSent = true;
1✔
664
        }
665

666
        if (config.getObservabilityMode()) {
1✔
667
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1✔
668
              .setObservabilityMode(true)
1✔
669
              .build();
1✔
670
        }
671

672
        extProcClientCallRequestObserver.onNext(requestToSend);
1✔
673
      }
1✔
674
    }
1✔
675

676
    private void onExtProcStreamReady() {
677
      drainPendingRequests();
1✔
678
      onReadyNotify();
1✔
679
    }
1✔
680

681
    private void drainPendingRequests() {
682
      int toRequest = pendingRequests.getAndSet(0);
1✔
683
      if (toRequest > 0) {
1✔
684
        super.request(toRequest);
1✔
685
      }
686
    }
1✔
687

688
    private void closeExtProcStream() {
689
      synchronized (streamLock) {
1✔
690
        if (markExtProcStreamCompleted(extProcStreamState)) {
1✔
691
          if (extProcClientCallRequestObserver != null) {
1✔
692
            extProcClientCallRequestObserver.onCompleted();
1✔
693
          }
694
        }
695
      }
1✔
696
    }
1✔
697

698
    private void internalOnError(Throwable t) {
699
      if (markExtProcStreamFailed(extProcStreamState)) {
1✔
700
        synchronized (streamLock) {
1✔
701
          if (extProcClientCallRequestObserver != null) {
1✔
702
            try {
703
              extProcClientCallRequestObserver.onError(t);
1✔
704
            } catch (Throwable ignored) {
×
705
              // Ignore exceptions during cancel/onError propagation
706
            }
1✔
707
            extProcClientCallRequestObserver = null;
1✔
708
          }
709
        }
1✔
710
        if (config.getObservabilityMode()
1✔
711
            || (config.getFailureModeAllow() && !bodyMessageSentToExtProc.get())) {
1✔
712
          handleFailOpen(wrappedListener);
1✔
713
        } else {
714
          String message = "External processor stream failed";
1✔
715
          delayedCall.cancel(message, t);
1✔
716
          wrappedListener.proceedWithClose();
1✔
717
        }
718
      }
719
    }
1✔
720

721
    private void halfCloseExtProcStream() {
722
      synchronized (streamLock) {
1✔
723
        if (!extProcStreamState.get().isCompleted() && extProcClientCallRequestObserver != null) {
1✔
724
          extProcClientCallRequestObserver.onCompleted();
1✔
725
        }
726
      }
1✔
727
    }
1✔
728

729
    private void onReadyNotify() {
730
      wrappedListener.onReadyNotify();
1✔
731
    }
1✔
732

733
    private boolean isSidecarReady() {
734
      ExtProcStreamState state = extProcStreamState.get();
1✔
735
      if (state.isCompleted()) {
1✔
736
        return true;
×
737
      }
738
      if (state.isDraining()) {
1✔
739
        return false;
1✔
740
      }
741
      synchronized (streamLock) {
1✔
742
        ClientCallStreamObserver<ProcessingRequest> observer = extProcClientCallRequestObserver;
1✔
743
        return observer != null && observer.isReady();
1✔
744
      }
745
    }
746

747
    @Override
748
    public boolean isReady() {
749
      if (passThroughMode.get()) {
1✔
750
        return super.isReady();
1✔
751
      }
752
      if (extProcStreamState.get().isCompleted()) {
1✔
753
        return super.isReady();
×
754
      }
755
      if (dataPlaneCallState.get() == DataPlaneCallState.IDLE && !config.getObservabilityMode()) {
1✔
756
        return false;
1✔
757
      }
758
      boolean sidecarReady = isSidecarReady();
1✔
759
      if (config.getObservabilityMode()) {
1✔
760
        return super.isReady() && sidecarReady;
1✔
761
      }
762
      return sidecarReady;
1✔
763
    }
764

765
    @Override
766
    public void request(int numMessages) {
767
      if (passThroughMode.get() || extProcStreamState.get().isCompleted()) {
1✔
768
        super.request(numMessages);
1✔
769
        return;
1✔
770
      }
771
      if (!isSidecarReady()) {
1✔
772
        pendingRequests.addAndGet(numMessages);
1✔
773
        return;
1✔
774
      }
775
      super.request(numMessages);
1✔
776
    }
1✔
777

778
    @Override
779
    public void sendMessage(InputStream message) {
780
      if (requestSideClosed.get()) {
1✔
781
        // External processor already closed the request stream. Discard further messages.
782
        return;
1✔
783
      }
784

785
      if (passThroughMode.get()) {
1✔
786
        super.sendMessage(message);
1✔
787
        return;
1✔
788
      }
789

790
      synchronized (streamLock) {
1✔
791
        if (passThroughMode.get()) {
1✔
792
          super.sendMessage(message);
×
793
          return;
×
794
        }
795

796
        ExtProcStreamState state = extProcStreamState.get();
1✔
797
        if (state.isDraining() || state.isCompleted()) {
1✔
798
          try {
799
            ByteString copiedBody = ByteString.readFrom(message);
1✔
800
            pendingDrainingMessages.add(new KnownLengthInputStream(copiedBody));
1✔
801
          } catch (IOException e) {
×
802
            rawCall.cancel("Failed to copy outbound message for buffering", e);
×
803
          }
1✔
804
          return;
1✔
805
        }
806
      }
1✔
807

808
      if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) {
1✔
809
        super.sendMessage(message);
1✔
810
        return;
1✔
811
      }
812

813
      // Mode is GRPC
814
      try {
815
        ByteString bodyByteString = outboundStreamToByteString(message);
1✔
816
        sendToExtProc(ProcessingRequest.newBuilder()
1✔
817
            .setRequestBody(HttpBody.newBuilder()
1✔
818
                .setBody(bodyByteString)
1✔
819
                .setEndOfStream(false)
1✔
820
                .build())
1✔
821
            .build());
1✔
822
        bodyMessageSentToExtProc.set(true);
1✔
823

824
        if (config.getObservabilityMode()) {
1✔
825
          super.sendMessage(new KnownLengthInputStream(bodyByteString));
×
826
        }
827
      } catch (IOException e) {
×
828
        rawCall.cancel("Failed to serialize message for External Processor", e);
×
829
      }
1✔
830
    }
1✔
831

832
    private void proceedWithHalfClose() {
833
      if (clientHalfCloseStartNanos > 0) {
1✔
834
        long durationNanos = System.nanoTime() - clientHalfCloseStartNanos;
1✔
835
        recordDuration(clientHalfCloseDuration, durationNanos);
1✔
836
        clientHalfCloseStartNanos = 0;
1✔
837
      }
838
      super.halfClose();
1✔
839
    }
1✔
840

841
    @Override
842
    public void halfClose() {
843
      clientHalfCloseStartNanos = System.nanoTime();
1✔
844
      if (passThroughMode.get()) {
1✔
845
        if (requestSideClosed.compareAndSet(false, true)) {
1✔
846
          proceedWithHalfClose();
1✔
847
        }
848
        return;
1✔
849
      }
850

851
      pendingHalfClose.set(true);
1✔
852

853
      if (extProcStreamState.get().isCompleted()) {
1✔
854
        return;
1✔
855
      }
856

857
      if (extProcStreamState.get().isDraining()) {
1✔
858
        return;
1✔
859
      }
860

861
      if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) {
1✔
862
        if (requestSideClosed.compareAndSet(false, true)) {
1✔
863
          proceedWithHalfClose();
1✔
864
        }
865
        return;
1✔
866
      }
867

868
      // Mode is GRPC
869
      sendToExtProc(ProcessingRequest.newBuilder()
1✔
870
          .setRequestBody(HttpBody.newBuilder()
1✔
871
              .setEndOfStreamWithoutMessage(true)
1✔
872
              .build())
1✔
873
          .build());
1✔
874
    }
1✔
875

876
    @Override
877
    public void cancel(@Nullable String message, @Nullable Throwable cause) {
878
      synchronized (streamLock) {
1✔
879
        if (!extProcStreamState.get().isCompleted() && extProcClientCallRequestObserver != null) {
1✔
880
          extProcClientCallRequestObserver.onError(
1✔
881
              Status.CANCELLED
882
                  .withDescription(message)
1✔
883
                  .withCause(cause)
1✔
884
                  .asRuntimeException());
1✔
885
        }
886
      }
1✔
887
      super.cancel(message, cause);
1✔
888
    }
1✔
889

890
    private void handleRequestBodyResponse(BodyResponse bodyResponse) {
891
      if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1✔
892
        BodyMutation mutation = bodyResponse.getResponse().getBodyMutation();
1✔
893
        if (mutation.hasStreamedResponse()) {
1✔
894
          StreamedBodyResponse streamed = mutation.getStreamedResponse();
1✔
895
          if (!streamed.getEndOfStreamWithoutMessage()) {
1✔
896
            super.sendMessage(new KnownLengthInputStream(streamed.getBody()));
1✔
897
          }
898
          if (streamed.getEndOfStream() || streamed.getEndOfStreamWithoutMessage()) {
1✔
899
            if (requestSideClosed.compareAndSet(false, true)) {
1✔
900
              proceedWithHalfClose();
1✔
901
            }
902
          }
903
        }
904
      }
905
    }
1✔
906

907
    private void handleResponseBodyResponse(
908
        BodyResponse bodyResponse, DataPlaneListener listener) {
909
      if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1✔
910
        BodyMutation mutation = bodyResponse.getResponse().getBodyMutation();
1✔
911
        if (mutation.hasStreamedResponse()) {
1✔
912
          StreamedBodyResponse streamed = mutation.getStreamedResponse();
1✔
913
          listener.onExternalBody(streamed.getBody());
1✔
914
        }
915
      }
916
    }
1✔
917

918
    private void handleImmediateResponse(ImmediateResponse immediate, DataPlaneListener listener)
919
        throws HeaderMutationDisallowedException {
920
      Status status = Status.fromCodeValue(immediate.getGrpcStatus().getStatus());
1✔
921
      if (!immediate.getDetails().isEmpty()) {
1✔
922
        status = status.withDescription(immediate.getDetails());
1✔
923
      }
924

925
      Metadata trailers = new Metadata();
1✔
926
      if (immediate.hasHeaders()) {
1✔
927
        applyHeaderMutations(trailers, immediate.getHeaders(), mutationFilter, mutator);
1✔
928
      }
929

930
      listener.setImmediateResponse(status, trailers);
1✔
931

932
      if (isProcessingTrailers.get()) {
1✔
933
        // If sent in response to a server trailers event, sets the status and optionally
934
        // headers to be included in the trailers.
935
        listener.unblockAfterStreamComplete();
1✔
936
      } else {
937
        // If sent in response to any other event, it will cause the data plane RPC to
938
        // immediately fail with the specified status as if it were an out-of-band
939
        // cancellation.
940
        rawCall.cancel(status.getDescription(), null);
1✔
941
        listener.unblockAfterStreamComplete();
1✔
942
      }
943
      closeExtProcStream();
1✔
944
    }
1✔
945

946
    private void drainPendingDrainingMessages() {
947
      synchronized (streamLock) {
1✔
948
        InputStream msg;
949
        while ((msg = pendingDrainingMessages.poll()) != null) {
1✔
950
          super.sendMessage(msg);
1✔
951
        }
952
        passThroughMode.set(true);
1✔
953
        if (pendingHalfClose.get()) {
1✔
954
          if (requestSideClosed.compareAndSet(false, true)) {
1✔
955
            proceedWithHalfClose();
1✔
956
          }
957
        }
958
      }
1✔
959
    }
1✔
960

961
    private void handleFailOpen(DataPlaneListener listener) {
962
      activateCall();
1✔
963
      drainPendingRequests();
1✔
964
      listener.unblockAfterStreamComplete();
1✔
965
      closeExtProcStream();
1✔
966
    }
1✔
967

968
    private void checkEndOfStream(ProcessingResponse response) {
969
      boolean terminal = false;
1✔
970
      if (response.hasResponseTrailers()) {
1✔
971
        terminal = true;
1✔
972
      } else if (response.hasResponseHeaders() && wrappedListener.isTrailersOnly()) {
1✔
973
        terminal = true;
1✔
974
      }
975

976
      if (terminal) {
1✔
977
        wrappedListener.unblockAfterStreamComplete();
1✔
978
        closeExtProcStream();
1✔
979
      }
980
    }
1✔
981

982
    long getServerHeadersStartNanos() {
983
      return serverHeadersStartNanos;
1✔
984
    }
985

986
    void setServerHeadersStartNanos(long serverHeadersStartNanos) {
987
      this.serverHeadersStartNanos = serverHeadersStartNanos;
1✔
988
    }
1✔
989

990
    long getServerTrailersStartNanos() {
991
      return serverTrailersStartNanos;
1✔
992
    }
993

994
    void setServerTrailersStartNanos(long serverTrailersStartNanos) {
995
      this.serverTrailersStartNanos = serverTrailersStartNanos;
1✔
996
    }
1✔
997

998
    AtomicReference<ExtProcStreamState> getExtProcStreamState() {
999
      return extProcStreamState;
1✔
1000
    }
1001

1002
    ProcessingMode getCurrentProcessingMode() {
1003
      return currentProcessingMode;
1✔
1004
    }
1005

1006
    AtomicBoolean getPassThroughMode() {
1007
      return passThroughMode;
1✔
1008
    }
1009

1010
    ExternalProcessorFilterConfig getConfig() {
1011
      return config;
1✔
1012
    }
1013

1014
    Context getCallContext() {
1015
      return callContext;
1✔
1016
    }
1017

1018
    ScheduledExecutorService getScheduler() {
1019
      return scheduler;
1✔
1020
    }
1021

1022
    AtomicBoolean getIsProcessingTrailers() {
1023
      return isProcessingTrailers;
1✔
1024
    }
1025
  }
1026

1027
  private static class DataPlaneListener extends SimpleForwardingClientCallListener<InputStream> {
1028
    private final ClientCall<?, ?> rawCall;
1029
    private final DataPlaneClientCall dataPlaneClientCall;
1030
    private final Queue<InputStream> savedMessages = new ConcurrentLinkedQueue<>();
1✔
1031
    private boolean inboundPassThrough = false;
1✔
1032
    @Nullable private volatile Metadata savedHeaders;
1033
    @Nullable private volatile Metadata savedTrailers;
1034
    @Nullable private volatile Status savedStatus;
1035
    private final AtomicBoolean terminationTriggered = new AtomicBoolean(false);
1✔
1036
    private final AtomicBoolean responseHeadersSent = new AtomicBoolean(false);
1✔
1037
    private final AtomicBoolean trailersOnly = new AtomicBoolean(false);
1✔
1038

1039
    protected DataPlaneListener(
1040
        ClientCall.Listener<InputStream> delegate,
1041
        ClientCall<?, ?> rawCall,
1042
        DataPlaneClientCall dataPlaneClientCall) {
1043
      super(delegate);
1✔
1044
      this.rawCall = rawCall;
1✔
1045
      this.dataPlaneClientCall = dataPlaneClientCall;
1✔
1046
    }
1✔
1047

1048
    boolean isTrailersOnly() {
1049
      return trailersOnly.get();
1✔
1050
    }
1051

1052
    Metadata getSavedHeaders() {
1053
      return savedHeaders;
1✔
1054
    }
1055

1056
    Metadata getSavedTrailers() {
1057
      return savedTrailers;
1✔
1058
    }
1059

1060
    void setImmediateResponse(Status status, Metadata trailers) {
1061
      this.savedStatus = status;
1✔
1062
      this.savedTrailers = trailers;
1✔
1063
    }
1✔
1064

1065
    @Override
1066
    public void onReady() {
1067
      dataPlaneClientCall.drainPendingRequests();
1✔
1068
      onReadyNotify();
1✔
1069
    }
1✔
1070

1071
    @Override
1072
    public void onHeaders(Metadata headers) {
1073
      dataPlaneClientCall.setServerHeadersStartNanos(System.nanoTime());
1✔
1074
      responseHeadersSent.set(true);
1✔
1075
      if (dataPlaneClientCall.getExtProcStreamState().get().isDraining()) {
1✔
1076
        this.savedHeaders = headers;
1✔
1077
        return;
1✔
1078
      }
1079
      boolean sendResponseHeaders =
1✔
1080
          dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1✔
1081
              == ProcessingMode.HeaderSendMode.SEND
1082
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1✔
1083
              == ProcessingMode.HeaderSendMode.DEFAULT;
1084

1085
      if (dataPlaneClientCall.getPassThroughMode().get() 
1✔
1086
          || dataPlaneClientCall.getExtProcStreamState().get().isCompleted() 
1✔
1087
          || !sendResponseHeaders) {
1088
        proceedWithHeaders(headers);
1✔
1089
        return;
1✔
1090
      }
1091

1092
      this.savedHeaders = headers;
1✔
1093
      dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1✔
1094
          .setResponseHeaders(HttpHeaders.newBuilder()
1✔
1095
              .setHeaders(
1✔
1096
                  toHeaderMap(headers, dataPlaneClientCall.getConfig().getForwardRulesConfig()))
1✔
1097
              .build())
1✔
1098
          .build());
1✔
1099

1100
      if (dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1101
        proceedWithHeaders();
×
1102
      }
1103
    }
1✔
1104

1105
    @Override
1106
    public void onMessage(InputStream message) {
1107
      synchronized (savedMessages) {
1✔
1108
        if (inboundPassThrough) {
1✔
1109
          dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message));
1✔
1110
          return;
1✔
1111
        }
1112

1113
        if (savedHeaders != null
1✔
1114
            || dataPlaneClientCall.getExtProcStreamState().get().isDraining()) {
1✔
1115
          try {
1116
            ByteString copiedBody = ByteString.readFrom(message);
1✔
1117
            savedMessages.add(new KnownLengthInputStream(copiedBody));
1✔
1118
          } catch (IOException e) {
×
1119
            rawCall.cancel("Failed to copy inbound message for buffering", e);
×
1120
          }
1✔
1121
          return;
1✔
1122
        }
1123
      }
1✔
1124

1125
      if (dataPlaneClientCall.getPassThroughMode().get()) {
1✔
1126
        dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message));
×
1127
        return;
×
1128
      }
1129

1130
      if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted()
1✔
1131
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode()
1✔
1132
              != ProcessingMode.BodySendMode.GRPC) {
1133
        dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message));
1✔
1134
        return;
1✔
1135
      }
1136

1137
      try {
1138
        ByteString bodyByteString = ByteString.readFrom(message);
1✔
1139
        sendResponseBodyToExtProc(bodyByteString, false);
1✔
1140
        dataPlaneClientCall.bodyMessageSentToExtProc.set(true);
1✔
1141

1142
        if (dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1143
          // If needed, downstream reading can be made more optimal by creating a wrapped
1144
          // Inputstream wraps the underlying bytestring and that implements HasByteBuffer,
1145
          // Detachable, KnownLength
1146
          dataPlaneClientCall.getCallContext().run(
×
1147
              () -> delegate().onMessage(bodyByteString.newInput()));
×
1148
        }
1149
      } catch (IOException e) {
×
1150
        rawCall.cancel("Failed to read server response", e);
×
1151
      }
1✔
1152
    }
1✔
1153

1154
    @Override
1155
    public void onClose(Status status, Metadata trailers) {
1156
      dataPlaneClientCall.setServerTrailersStartNanos(System.nanoTime());
1✔
1157
      ExtProcStreamState extProcStreamState =
1✔
1158
          dataPlaneClientCall.getExtProcStreamState().get();
1✔
1159
      if (extProcStreamState.isFailed()
1✔
1160
          && !dataPlaneClientCall.getConfig().getObservabilityMode()
1✔
1161
          && (!dataPlaneClientCall.getConfig().getFailureModeAllow()
1✔
1162
              || dataPlaneClientCall.bodyMessageSentToExtProc.get())) {
1✔
1163
        if (markDataPlaneCallClosed(dataPlaneClientCall.dataPlaneCallState)) {
1✔
1164
          proceedWithClose(Status.INTERNAL.withDescription("External processor stream failed")
1✔
1165
              .withCause(status.getCause()), new Metadata());
1✔
1166
        }
1167
        return;
1✔
1168
      }
1169
      if (dataPlaneClientCall.getPassThroughMode().get()) {
1✔
1170
        if (markDataPlaneCallClosed(dataPlaneClientCall.dataPlaneCallState)) {
1✔
1171
          proceedWithClose(status, trailers);
1✔
1172
        }
1173
        return;
1✔
1174
      }
1175

1176
      if (this.savedStatus == null) {
1✔
1177
        this.savedStatus = status;
1✔
1178
        this.savedTrailers = trailers;
1✔
1179
      }
1180

1181
      // If we are still waiting for the external processor to validate response headers,
1182
      // buffer the close status/trailers and defer the close until headers are processed.
1183
      if (savedHeaders != null) {
1✔
1184
        return;
1✔
1185
      }
1186

1187
      if (dataPlaneClientCall.getExtProcStreamState().get().isDraining()) {
1✔
1188
        return;
1✔
1189
      }
1190

1191
      if (!responseHeadersSent.get()) {
1✔
1192
        trailersOnly.set(true);
1✔
1193
      }
1194

1195
      triggerCloseHandshake();
1✔
1196

1197
      if (dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1198
        proceedWithClose();
1✔
1199
        @SuppressWarnings("unused")
1200
        ScheduledFuture<?> unused = dataPlaneClientCall.getScheduler().schedule(
1✔
1201
            dataPlaneClientCall::closeExtProcStream,
1✔
1202
            dataPlaneClientCall.getConfig().getDeferredCloseTimeoutNanos(),
1✔
1203
            TimeUnit.NANOSECONDS);
1204
      }
1205
    }
1✔
1206

1207
    void onReadyNotify() {
1208
      dataPlaneClientCall.getCallContext().run(() -> delegate().onReady());
1✔
1209
    }
1✔
1210

1211
    void proceedWithHeaders() {
1212
      if (savedHeaders != null) {
1✔
1213
        proceedWithHeaders(savedHeaders);
1✔
1214
        synchronized (savedMessages) {
1✔
1215
          savedHeaders = null;
1✔
1216
          if (!dataPlaneClientCall.getExtProcStreamState().get().isDraining()) {
1✔
1217
            InputStream msg;
1218
            while ((msg = savedMessages.poll()) != null) {
1✔
1219
              onMessage(msg);
1✔
1220
            }
1221
          }
1222
        }
1✔
1223
        onReadyNotify();
1✔
1224
        if (savedStatus != null) {
1✔
1225
          triggerCloseHandshake();
1✔
1226
        }
1227
      }
1228
    }
1✔
1229

1230
    private void proceedWithHeaders(Metadata headers) {
1231
      if (dataPlaneClientCall.getServerHeadersStartNanos() > 0) {
1✔
1232
        long durationNanos = System.nanoTime() - dataPlaneClientCall.getServerHeadersStartNanos();
1✔
1233
        dataPlaneClientCall.recordDuration(serverHeadersDuration, durationNanos);
1✔
1234
        dataPlaneClientCall.setServerHeadersStartNanos(0);
1✔
1235
      }
1236
      dataPlaneClientCall.getCallContext().run(() -> delegate().onHeaders(headers));
1✔
1237
    }
1✔
1238

1239
    void proceedWithClose() {
1240
      if (savedStatus != null) {
1✔
1241
        if (markDataPlaneCallClosed(dataPlaneClientCall.dataPlaneCallState)) {
1✔
1242
          proceedWithClose(savedStatus, savedTrailers);
1✔
1243
        }
1244
        savedStatus = null;
1✔
1245
        savedTrailers = null;
1✔
1246
      }
1247
    }
1✔
1248

1249
    private void proceedWithClose(Status status, Metadata trailers) {
1250
      if (dataPlaneClientCall.getServerTrailersStartNanos() > 0) {
1✔
1251
        long durationNanos = System.nanoTime() - dataPlaneClientCall.getServerTrailersStartNanos();
1✔
1252
        dataPlaneClientCall.recordDuration(serverTrailersDuration, durationNanos);
1✔
1253
        dataPlaneClientCall.setServerTrailersStartNanos(0);
1✔
1254
      }
1255
      dataPlaneClientCall.getCallContext().run(() -> delegate().onClose(status, trailers));
1✔
1256
    }
1✔
1257

1258
    void onExternalBody(ByteString body) {
1259
      // If needed, downstream reading can be made more optimal by creating a wrapped
1260
      // Inputstream wraps the underlying bytestring and that implements HasByteBuffer,
1261
      // Detachable, KnownLength
1262
      dataPlaneClientCall.getCallContext().run(
1✔
1263
          () -> delegate().onMessage(body.newInput()));
1✔
1264
    }
1✔
1265

1266
    void unblockAfterStreamComplete() {
1267
      proceedWithHeaders();
1✔
1268
      proceedWithSavedMessages();
1✔
1269
      dataPlaneClientCall.drainPendingDrainingMessages();
1✔
1270
      proceedWithClose();
1✔
1271
    }
1✔
1272

1273
    private void proceedWithSavedMessages() {
1274
      synchronized (savedMessages) {
1✔
1275
        InputStream msg;
1276
        while ((msg = savedMessages.poll()) != null) {
1✔
1277
          final InputStream finalMsg = msg;
1✔
1278
          dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(finalMsg));
1✔
1279
        }
1✔
1280
        inboundPassThrough = true;
1✔
1281
      }
1✔
1282
    }
1✔
1283

1284
    private void triggerCloseHandshake() {
1285
      if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted()
1✔
1286
          || !terminationTriggered.compareAndSet(false, true)) {
1✔
1287
        return;
1✔
1288
      }
1289

1290
      boolean sendResponseHeaders =
1✔
1291
          dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1✔
1292
              == ProcessingMode.HeaderSendMode.SEND
1293
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1✔
1294
              == ProcessingMode.HeaderSendMode.DEFAULT;
1295

1296
      boolean sendResponseTrailers =
1✔
1297
          dataPlaneClientCall.getCurrentProcessingMode().getResponseTrailerMode()
1✔
1298
              == ProcessingMode.HeaderSendMode.SEND;
1299

1300
      if (trailersOnly.get()) {
1✔
1301
        if (sendResponseHeaders) {
1✔
1302
          dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1✔
1303
              .setResponseHeaders(HttpHeaders.newBuilder()
1✔
1304
                  .setHeaders(
1✔
1305
                      toHeaderMap(
1✔
1306
                          savedTrailers,
1307
                          dataPlaneClientCall.getConfig().getForwardRulesConfig()))
1✔
1308
                  .setEndOfStream(true)
1✔
1309
                  .build())
1✔
1310
              .build());
1✔
1311
        } else {
1312
          proceedWithClose();
1✔
1313
          if (!dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1314
            dataPlaneClientCall.closeExtProcStream();
1✔
1315
          }
1316
        }
1317
      } else if (sendResponseTrailers) {
1✔
1318
        dataPlaneClientCall.getIsProcessingTrailers().set(true);
1✔
1319
        dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1✔
1320
            .setResponseTrailers(HttpTrailers.newBuilder()
1✔
1321
                .setTrailers(
1✔
1322
                    toHeaderMap(
1✔
1323
                        savedTrailers,
1324
                        dataPlaneClientCall.getConfig().getForwardRulesConfig()))
1✔
1325
                .build())
1✔
1326
            .build());
1✔
1327
      } else {
1328
        proceedWithClose();
1✔
1329
        if (!dataPlaneClientCall.getConfig().getObservabilityMode()) {
1✔
1330
          dataPlaneClientCall.closeExtProcStream();
1✔
1331
        }
1332
      }
1333
    }
1✔
1334

1335
    private void sendResponseBodyToExtProc(
1336
        @Nullable ByteString bodyByteString, boolean endOfStream) {
1337
      if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted()
1✔
1338
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode()
1✔
1339
              != ProcessingMode.BodySendMode.GRPC) {
1340
        return;
×
1341
      }
1342

1343
      HttpBody.Builder bodyBuilder =
1344
          HttpBody.newBuilder();
1✔
1345
      if (bodyByteString != null) {
1✔
1346
        bodyBuilder.setBody(bodyByteString);
1✔
1347
      }
1348
      bodyBuilder.setEndOfStream(endOfStream);
1✔
1349

1350
      dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1✔
1351
          .setResponseBody(bodyBuilder.build())
1✔
1352
          .build());
1✔
1353
    }
1✔
1354
  }
1355
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc