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

grpc / grpc-java / #20525

13 Oct 2026 04:05PM UTC coverage: 89.449% (+0.03%) from 89.422%
#20525

push

github

web-flow
core: Delayed deserialization for unary/server-streaming calls (#13113)

Restore delayed deserialization and lazy decompression for unary and
server-streaming calls (originally introduced in #13004 and reverted in
#13107).

Additionally, convert ServerCall.request(1) to stream.request(2) for
unary and server-streaming calls (clientSendsOneMessage()) to prevent
transport deadlocks if multiple messages arrive on the stream.

39354 of 43996 relevant lines covered (89.45%)

0.89 hits per line

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

97.13
/../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.GrpcUtil;
64
import io.grpc.internal.SerializingExecutor;
65
import io.grpc.stub.ClientCallStreamObserver;
66
import io.grpc.stub.ClientResponseObserver;
67
import io.grpc.stub.MetadataUtils;
68
import io.grpc.xds.ExternalProcessorFilter.ExternalProcessorFilterConfig;
69
import io.grpc.xds.Filter.FilterContext;
70
import io.grpc.xds.internal.extproc.DataPlaneCallState;
71
import io.grpc.xds.internal.extproc.EventType;
72
import io.grpc.xds.internal.extproc.ExtProcStreamState;
73
import io.grpc.xds.internal.extproc.KnownLengthInputStream;
74
import io.grpc.xds.internal.grpcservice.CachedChannelManager;
75
import io.grpc.xds.internal.grpcservice.HeaderValue;
76
import io.grpc.xds.internal.headermutations.HeaderMutationDisallowedException;
77
import io.grpc.xds.internal.headermutations.HeaderMutationFilter;
78
import io.grpc.xds.internal.headermutations.HeaderMutationRulesConfig;
79
import io.grpc.xds.internal.headermutations.HeaderMutator;
80
import java.io.IOException;
81
import java.io.InputStream;
82
import java.util.ArrayList;
83
import java.util.List;
84
import java.util.Optional;
85
import java.util.Queue;
86
import java.util.concurrent.ConcurrentLinkedQueue;
87
import java.util.concurrent.Executor;
88
import java.util.concurrent.ScheduledExecutorService;
89
import java.util.concurrent.ScheduledFuture;
90
import java.util.concurrent.TimeUnit;
91
import java.util.concurrent.atomic.AtomicBoolean;
92
import java.util.concurrent.atomic.AtomicInteger;
93
import java.util.concurrent.atomic.AtomicReference;
94
import javax.annotation.Nullable;
95
import javax.annotation.concurrent.GuardedBy;
96

97
/**
98
 * Client-side interceptor for external processing filter.
99
 */
100
final class ExternalProcessorClientInterceptor implements ClientInterceptor {
101

102
  @VisibleForTesting
103
  static DoubleHistogramMetricInstrument clientHeadersDuration;
104
  @VisibleForTesting
105
  static DoubleHistogramMetricInstrument clientHalfCloseDuration;
106
  @VisibleForTesting
107
  static DoubleHistogramMetricInstrument serverHeadersDuration;
108
  @VisibleForTesting
109
  static DoubleHistogramMetricInstrument serverTrailersDuration;
110

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

119
  static {
120
    initMetricInstruments();
1 ✔
121
  }
1 ✔
122

123
  static synchronized void initMetricInstruments() {
124
    if (GrpcUtil.getFlag("GRPC_EXPERIMENTAL_XDS_EXT_PROC_ON_CLIENT", false)) {
1 ✔
125
      if (clientHeadersDuration == null) {
1 ✔
126
        MetricInstrumentRegistry registry = MetricInstrumentRegistry.getDefaultRegistry();
1 ✔
127

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

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

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

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

171
  private final ExternalProcessorFilterConfig filterConfig;
172
  private final ScheduledExecutorService scheduler;
173
  private final MetricRecorder metricsRecorder;
174
  private final ManagedChannel extProcChannel;
175

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

188
  @VisibleForTesting
189
  ExternalProcessorFilterConfig getFilterConfig() {
190
    return filterConfig;
1 ✔
191
  }
192

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

234
    // The filter chain is preceded by RawMessageClientInterceptor when payload access is
235
    // required, in which case ReqT and RespT are InputStream.
236
    ClientCall<ReqT, RespT> rawCall =
1 ✔
237
        new SimpleForwardingClientCall<ReqT, RespT>(next.newCall(method, callOptions)) {
1 ✔
238
          private final AtomicBoolean cancelled = new AtomicBoolean(false);
1 ✔
239

240
          @Override
241
          public void cancel(@Nullable String message, @Nullable Throwable cause) {
242
            if (cancelled.compareAndSet(false, true)) {
1 ✔
243
              super.cancel(message, cause);
1 ✔
244
            }
245
          }
1 ✔
246
        };
247

248
    // Create a local subclass instance to buffer outbound actions
249
    DataPlaneDelayedCall<ReqT, RespT> delayedCall =
1 ✔
250
        new DataPlaneDelayedCall<>(
251
            serializingExecutor, scheduler, callOptions.getDeadline());
1 ✔
252

253
    DataPlaneClientCall<ReqT, RespT> dataPlaneCall = new DataPlaneClientCall<>(
1 ✔
254
        delayedCall, rawCall, extProcStub, filterConfig, filterConfig.getMutationRulesConfig(),
1 ✔
255
        scheduler, method, next, metricsRecorder, next.authority());
1 ✔
256

257
    return dataPlaneCall;
1 ✔
258
  }
259

260
  // --- SHARED UTILITY METHODS ---
261

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

272
  /**
273
   * Handles the bidirectional stream with the External Processor.
274
   * Buffers the actual RPC start until the Ext Proc header response is received.
275
   */
276
  private static class DataPlaneClientCall<ReqT, RespT>
277
      extends SimpleForwardingClientCall<ReqT, RespT> {
278

279
    private final ExternalProcessorGrpc.ExternalProcessorStub stub;
280
    private final ExternalProcessorFilterConfig config;
281
    private final ClientCall<ReqT, RespT> rawCall;
282
    private final DataPlaneDelayedCall<ReqT, RespT> delayedCall;
283
    private final ScheduledExecutorService scheduler;
284
    final Object streamLock = new Object();
1 ✔
285
    @Nullable private volatile EventType expectedRequestResponse;
286
    @Nullable private volatile EventType expectedResponseResponse;
287
    @Nullable private volatile ClientCallStreamObserver<ProcessingRequest>
288
        extProcClientCallRequestObserver;
289
    @GuardedBy("streamLock")
1 ✔
290
    private final Queue<ReqT> pendingDrainingMessages =
291
        new ConcurrentLinkedQueue<>();
292
    @Nullable private volatile DataPlaneListener<RespT> wrappedListener;
293
    private final HeaderMutationFilter mutationFilter;
294
    private final HeaderMutator mutator = HeaderMutator.create();
1 ✔
295
    private final AtomicInteger pendingRequests = new AtomicInteger(0);
1 ✔
296
    private final ProcessingMode currentProcessingMode;
297

298
    // Default initial window size
299
    private static final long DEFAULT_INITIAL_WINDOW_SIZE = 65536;
300

301
    // Outbound (sending) windows
302
    @GuardedBy("streamLock")
1 ✔
303
    private long downstreamToSidestreamWindow = DEFAULT_INITIAL_WINDOW_SIZE;
304
    @GuardedBy("streamLock")
1 ✔
305
    private long upstreamToSidestreamWindow = DEFAULT_INITIAL_WINDOW_SIZE;
306

307
    // Inbound (receiving) windows
308
    @GuardedBy("streamLock")
1 ✔
309
    private long sidestreamToUpstreamWindow = DEFAULT_INITIAL_WINDOW_SIZE;
310
    @GuardedBy("streamLock")
1 ✔
311
    private long sidestreamToDownstreamWindow = DEFAULT_INITIAL_WINDOW_SIZE;
312

313
    // Threshold to trigger standalone client window updates
314
    private static final long WINDOW_UPDATE_THRESHOLD = DEFAULT_INITIAL_WINDOW_SIZE / 2;
315

316
    // Path 1: Pending/buffered request body messages from downstream
317
    @GuardedBy("streamLock")
1 ✔
318
    private final Queue<ByteString> pendingRequestBodyMessages = new ConcurrentLinkedQueue<>();
319
    // Deferred half-close flag for upstream direction
320
    private final AtomicBoolean pendingUpstreamHalfClose = new AtomicBoolean(false);
1 ✔
321

322
    // Path 2: Buffered request body messages from ext_proc server to forward upstream
323
    @GuardedBy("streamLock")
1 ✔
324
    private final Queue<ByteString> pendingUpstreamBodyMessages =
325
        new ConcurrentLinkedQueue<>();
326
    // Path 4: Outstanding requests from downstream for pulling responses
327
    @GuardedBy("streamLock")
1 ✔
328
    private int downstreamRequestsPending = 0;
329
    // Buffered mutated response bodies from ext_proc server
330
    @GuardedBy("streamLock")
1 ✔
331
    private final Queue<ByteString> pendingMutatedResponseBodies =
332
        new ConcurrentLinkedQueue<>();
333

334
    // Accumulated client window updates to send to ext_proc
335
    @GuardedBy("streamLock")
1 ✔
336
    private long accumulatedWindowUpdateSidestreamToUpstream = 0;
337
    @GuardedBy("streamLock")
1 ✔
338
    private long accumulatedWindowUpdateSidestreamToDownstream = 0;
339

340
    // Flag to track if FlowControlInit was sent in the initial message
341
    @GuardedBy("streamLock")
1 ✔
342
    private boolean flowControlInitSent = false;
343

344
    private final MethodDescriptor<?, ?> method;
345
    private final Channel channel;
346
    private final MetricRecorder metricsRecorder;
347
    private final String target;
348
    private volatile Context callContext = Context.ROOT;
1 ✔
349

350
    private volatile long clientHeadersStartNanos;
351
    private volatile long clientHalfCloseStartNanos;
352
    private volatile long serverHeadersStartNanos;
353
    private volatile long serverTrailersStartNanos;
354

355
    private boolean protocolConfigSent = false;
1 ✔
356
    private ImmutableMap<String, Struct> collectedAttributes;
357
    private boolean requestAttributesSent = false;
1 ✔
358
    @Nullable private volatile Metadata requestHeaders;
359
    final AtomicReference<DataPlaneCallState> dataPlaneCallState =
1 ✔
360
        new AtomicReference<>(DataPlaneCallState.IDLE);
361
    final AtomicReference<ExtProcStreamState> extProcStreamState =
1 ✔
362
        new AtomicReference<>(ExtProcStreamState.ACTIVE);
363
    final AtomicBoolean passThroughMode = new AtomicBoolean(false);
1 ✔
364
    final AtomicBoolean requestSideClosed = new AtomicBoolean(false);
1 ✔
365
    final AtomicBoolean appHalfClosed = new AtomicBoolean(false);
1 ✔
366
    final AtomicBoolean isProcessingTrailers = new AtomicBoolean(false);
1 ✔
367
    final AtomicBoolean pendingHalfClose = new AtomicBoolean(false);
1 ✔
368
    final AtomicBoolean bodyMessageSentToExtProc = new AtomicBoolean(false);
1 ✔
369

370
    protected DataPlaneClientCall(
371
        DataPlaneDelayedCall<ReqT, RespT> delayedCall,
372
        ClientCall<ReqT, RespT> rawCall,
373
        ExternalProcessorGrpc.ExternalProcessorStub stub,
374
        ExternalProcessorFilterConfig config,
375
        Optional<HeaderMutationRulesConfig> mutationRulesConfig,
376
        ScheduledExecutorService scheduler,
377
        MethodDescriptor<?, ?> method,
378
        Channel channel,
379
        MetricRecorder metricsRecorder,
380
        String target) {
381
      super(delayedCall);
1 ✔
382
      this.delayedCall = delayedCall;
1 ✔
383
      this.rawCall = rawCall;
1 ✔
384
      this.stub = stub;
1 ✔
385
      this.config = config;
1 ✔
386
      this.currentProcessingMode = config.getExternalProcessor().getProcessingMode();
1 ✔
387
      this.mutationFilter = new HeaderMutationFilter(mutationRulesConfig);
1 ✔
388
      this.scheduler = scheduler;
1 ✔
389
      this.method = method;
1 ✔
390
      this.channel = channel;
1 ✔
391
      this.metricsRecorder = checkNotNull(metricsRecorder, "metricsRecorder");
1 ✔
392
      this.target = checkNotNull(target, "target");
1 ✔
393
    }
1 ✔
394

395
    private boolean activateCall() {
396
      if ((extProcStreamState.get() == ExtProcStreamState.FAILED
1 ✔
397
              && !config.getFailureModeAllow()
1 ✔
398
              && !config.getObservabilityMode())
1 ✔
399
          || !dataPlaneCallState.compareAndSet(
1 ✔
400
              DataPlaneCallState.IDLE, DataPlaneCallState.ACTIVE)) {
401
        return false;
1 ✔
402
      }
403
      if (clientHeadersStartNanos > 0) {
1 ✔
404
        long durationNanos = System.nanoTime() - clientHeadersStartNanos;
1 ✔
405
        recordDuration(clientHeadersDuration, durationNanos);
1 ✔
406
        clientHeadersStartNanos = 0;
1 ✔
407
      }
408
      Runnable toRun = delayedCall.setCall(rawCall);
1 ✔
409
      if (toRun != null) {
1 ✔
410
        callContext.run(toRun);
1 ✔
411
      }
412
      drainPendingRequests();
1 ✔
413
      onReadyNotify();
1 ✔
414
      return true;
1 ✔
415
    }
416

417
    private void recordDuration(DoubleHistogramMetricInstrument instrument, long durationNanos) {
418
      if (instrument != null) {
1 ✔
419
        double durationSecs = (double) durationNanos / 1_000_000_000.0;
1 ✔
420
        metricsRecorder.recordDoubleHistogram(
1 ✔
421
            instrument,
422
            durationSecs,
423
            ImmutableList.of(target),
1 ✔
424
            ImmutableList.of());
1 ✔
425
      }
426
    }
1 ✔
427

428
    /**
429
     * Validates whether the body response uses unsupported gRPC message compression.
430
     * If compression is unsupported, this method will cancel the call, transition the
431
     * stream to a failed state, send an error to the external processor, and return false.
432
     *
433
     * @param bodyResponse the response to validate
434
     * @return true if validation passes (compression is supported or not used),
435
     *     false if validation fails
436
     */
437
    private boolean validateCompressionSupport(BodyResponse bodyResponse) {
438
      if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1 ✔
439
        BodyMutation mutation = 
1 ✔
440
            bodyResponse.getResponse().getBodyMutation();
1 ✔
441
        if (mutation.hasStreamedResponse()
1 ✔
442
            && mutation.getStreamedResponse().getGrpcMessageCompressed()) {
1 ✔
443
          StatusRuntimeException ex = Status.UNAVAILABLE
1 ✔
444
              .withDescription("gRPC message compression not supported in ext_proc")
1 ✔
445
              .asRuntimeException();
1 ✔
446
          synchronized (streamLock) {
1 ✔
447
            if (markExtProcStreamFailed(extProcStreamState)) {
1 ✔
448
              if (extProcClientCallRequestObserver != null) {
1 ✔
449
                extProcClientCallRequestObserver.onError(ex);
1 ✔
450
                extProcClientCallRequestObserver = null;
1 ✔
451
              }
452
            }
453
          }
1 ✔
454
          activateCall();
1 ✔
455
          cancelDownstream("gRPC message compression not supported in ext_proc", ex);
1 ✔
456
          closeExtProcStream();
1 ✔
457
          return false;
1 ✔
458
        }
459
      }
460
      return true;
1 ✔
461
    }
462

463
    @Override
464
    public void start(Listener<RespT> responseListener, Metadata headers) {
465
      this.callContext = Context.current();
1 ✔
466
      clientHeadersStartNanos = System.nanoTime();
1 ✔
467
      this.requestHeaders = headers;
1 ✔
468
      this.wrappedListener = new DataPlaneListener<>(responseListener, this);
1 ✔
469

470
      // DelayedClientCall.start will buffer the listener and headers until setCall is called.
471
      super.start(wrappedListener, headers);
1 ✔
472

473
      stub.process(new ClientResponseObserver<ProcessingRequest, ProcessingResponse>() {
1 ✔
474
        @Override
475
        public void beforeStart(ClientCallStreamObserver<ProcessingRequest> requestStream) {
476
          synchronized (streamLock) {
1 ✔
477
            extProcClientCallRequestObserver = requestStream;
1 ✔
478
          }
1 ✔
479
          requestStream.setOnReadyHandler(DataPlaneClientCall.this::onExtProcStreamReady);
1 ✔
480
        }
1 ✔
481

482
        @Override
483
        public void onNext(ProcessingResponse response) {
484
          try {
485
            if (config.getObservabilityMode()) {
1 ✔
486
              return;
1 ✔
487
            }
488

489
            if (response.hasServerWindowUpdate()) {
1 ✔
490
              ProcessingResponse.ServerWindowUpdate update = response.getServerWindowUpdate();
1 ✔
491
              boolean wasReady;
492
              synchronized (streamLock) {
1 ✔
493
                wasReady = isReady();
1 ✔
494
                downstreamToSidestreamWindow += update.getWindowIncrementDownstreamToSidestream();
1 ✔
495
                upstreamToSidestreamWindow += update.getWindowIncrementUpstreamToSidestream();
1 ✔
496
                drainPendingRequestBodyMessages();
1 ✔
497
                drainPendingRequests();
1 ✔
498
                if (wrappedListener != null) {
1 ✔
499
                  wrappedListener.drainSavedMessages();
1 ✔
500
                }
501
              }
1 ✔
502
              // If isReady() becomes true (depends on updated downstreamToSidestreamWindow),
503
              // notify the client application via onReadyNotify() (runs unlocked).
504
              if (!wasReady && isReady()) {
1 ✔
505
                onReadyNotify();
1 ✔
506
              }
507
            }
508

509
            if (response.hasImmediateResponse()) {
1 ✔
510
              if (config.getDisableImmediateResponse()) {
1 ✔
511
                internalOnError(Status.UNAVAILABLE
1 ✔
512
                    .withDescription(
1 ✔
513
                        "Immediate response is disabled but received from external processor")
514
                    .asRuntimeException());
1 ✔
515
                return;
1 ✔
516
              }
517
              handleImmediateResponse(response.getImmediateResponse(), wrappedListener);
1 ✔
518
              return;
1 ✔
519
            }
520

521
            if (response.hasRequestHeaders()) {
1 ✔
522
              EventType expected = expectedRequestResponse;
1 ✔
523
              if (expected == null || expected != EventType.REQUEST_HEADERS) {
1 ✔
524
                internalOnError(Status.UNAVAILABLE
×
525
                    .withDescription("Protocol error: received response out of order. Expected: " 
×
526
                        + expected + ", Received: REQUEST_HEADERS")
527
                    .asRuntimeException());
×
528
                return;
×
529
              }
530
              expectedRequestResponse = null;
1 ✔
531
            } else if (response.hasResponseHeaders()) {
1 ✔
532
              EventType expected = expectedResponseResponse;
1 ✔
533
              if (expected == null || expected != EventType.RESPONSE_HEADERS) {
1 ✔
534
                internalOnError(Status.UNAVAILABLE
1 ✔
535
                    .withDescription("Protocol error: received response out of order. Expected: " 
1 ✔
536
                        + expected + ", Received: RESPONSE_HEADERS")
537
                    .asRuntimeException());
1 ✔
538
                return;
1 ✔
539
              }
540
              expectedResponseResponse = null;
1 ✔
541
            } else if (response.hasResponseTrailers()) {
1 ✔
542
              EventType expected = expectedResponseResponse;
1 ✔
543
              if (expected == null || expected != EventType.RESPONSE_TRAILERS) {
1 ✔
544
                internalOnError(Status.UNAVAILABLE
1 ✔
545
                    .withDescription("Protocol error: received response out of order. Expected: " 
1 ✔
546
                        + expected + ", Received: RESPONSE_TRAILERS")
547
                    .asRuntimeException());
1 ✔
548
                return;
1 ✔
549
              }
550
              expectedResponseResponse = null;
1 ✔
551
            } else if (response.hasRequestBody()) {
1 ✔
552
              if (currentProcessingMode.getRequestBodyMode()
1 ✔
553
                  != ProcessingMode.BodySendMode.GRPC) {
554
                internalOnError(Status.UNAVAILABLE
1 ✔
555
                    .withDescription(
1 ✔
556
                        "Protocol error: received unexpected request_body response when"
557
                            + " request_body_mode is not GRPC.")
558
                    .asRuntimeException());
1 ✔
559
                return;
1 ✔
560
              }
561
              EventType expected = expectedRequestResponse;
1 ✔
562
              if (expected == EventType.REQUEST_HEADERS) {
1 ✔
563
                internalOnError(Status.UNAVAILABLE
1 ✔
564
                    .withDescription(
1 ✔
565
                        "Protocol error: received request_body before request_headers response.")
566
                    .asRuntimeException());
1 ✔
567
                return;
1 ✔
568
              }
569
            } else if (response.hasResponseBody()) {
1 ✔
570
              if (currentProcessingMode.getResponseBodyMode()
1 ✔
571
                  != ProcessingMode.BodySendMode.GRPC) {
572
                internalOnError(Status.UNAVAILABLE
1 ✔
573
                    .withDescription(
1 ✔
574
                        "Protocol error: received unexpected response_body response when"
575
                            + " response_body_mode is not GRPC.")
576
                    .asRuntimeException());
1 ✔
577
                return;
1 ✔
578
              }
579
              EventType expected = expectedResponseResponse;
1 ✔
580
              if (expected == EventType.RESPONSE_HEADERS) {
1 ✔
581
                internalOnError(Status.UNAVAILABLE
1 ✔
582
                    .withDescription(
1 ✔
583
                        "Protocol error: received response_body before headers response.")
584
                    .asRuntimeException());
1 ✔
585
                return;
1 ✔
586
              }
587
            }
588

589
            if (response.getRequestDrain()) {
1 ✔
590
              extProcStreamState.set(ExtProcStreamState.DRAINING);
1 ✔
591
              halfCloseExtProcStream();
1 ✔
592
              activateCall();
1 ✔
593
            }
594

595
            // 1. Client Headers
596
            if (response.hasRequestHeaders()) {
1 ✔
597
              if (response.getRequestHeaders().hasResponse()) {
1 ✔
598
                if (response.getRequestHeaders().getResponse().getStatus()
1 ✔
599
                    == CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) {
600
                  internalOnError(Status.UNAVAILABLE
1 ✔
601
                      .withDescription("CONTINUE_AND_REPLACE is not supported")
1 ✔
602
                      .asRuntimeException());
1 ✔
603
                  return;
1 ✔
604
                }
605
                applyHeaderMutations(
1 ✔
606
                    requestHeaders,
1 ✔
607
                    response.getRequestHeaders().getResponse().getHeaderMutation(),
1 ✔
608
                    mutationFilter,
1 ✔
609
                    mutator);
1 ✔
610
              }
611
              activateCall();
1 ✔
612
            }
613
            // 2. Client Message (Request Body)
614
            else if (response.hasRequestBody()) {
1 ✔
615
              if (validateCompressionSupport(response.getRequestBody())) {
1 ✔
616
                handleRequestBodyResponse(response.getRequestBody());
1 ✔
617
              }
618
            }
619
            // 4. Server Headers
620
            else if (response.hasResponseHeaders()) {
1 ✔
621
              if (response.getResponseHeaders().hasResponse()) {
1 ✔
622
                if (response.getResponseHeaders().getResponse().getStatus()
1 ✔
623
                    == CommonResponse.ResponseStatus.CONTINUE_AND_REPLACE) {
624
                  internalOnError(Status.UNAVAILABLE
1 ✔
625
                      .withDescription("CONTINUE_AND_REPLACE is not supported")
1 ✔
626
                      .asRuntimeException());
1 ✔
627
                  return;
1 ✔
628
                }
629
                Metadata target = wrappedListener.isTrailersOnly()
1 ✔
630
                    ? wrappedListener.getSavedTrailers() : wrappedListener.getSavedHeaders();
1 ✔
631
                applyHeaderMutations(
1 ✔
632
                    target,
633
                    response.getResponseHeaders().getResponse().getHeaderMutation(),
1 ✔
634
                    mutationFilter,
1 ✔
635
                    mutator);
1 ✔
636
              }
637
              if (wrappedListener.isTrailersOnly()) {
1 ✔
638
                wrappedListener.proceedWithClose();
1 ✔
639
              } else {
640
                wrappedListener.proceedWithHeaders();
1 ✔
641
              }
642
            }
643
            // 5. Server Message (Response Body)
644
            else if (response.hasResponseBody()) {
1 ✔
645
              if (validateCompressionSupport(response.getResponseBody())) {
1 ✔
646
                handleResponseBodyResponse(response.getResponseBody(), wrappedListener);
1 ✔
647
              }
648
            }
649
            // 6. Response Trailers
650
            else if (response.hasResponseTrailers()) {
1 ✔
651
              if (response.getResponseTrailers().hasHeaderMutation()) {
1 ✔
652
                applyHeaderMutations(
1 ✔
653
                    wrappedListener.getSavedTrailers(),
1 ✔
654
                    response.getResponseTrailers().getHeaderMutation(),
1 ✔
655
                    mutationFilter,
1 ✔
656
                    mutator);
1 ✔
657
              }
658
              wrappedListener.proceedWithClose();
1 ✔
659
            }
660

661
            checkEndOfStream(response);
1 ✔
662
          } catch (Throwable t) {
1 ✔
663
            internalOnError(t);
1 ✔
664
          }
1 ✔
665
        }
1 ✔
666

667
        @Override
668
        public void onError(Throwable t) {
669
          if (markExtProcStreamFailed(extProcStreamState)) {
1 ✔
670
            synchronized (streamLock) {
1 ✔
671
              extProcClientCallRequestObserver = null;
1 ✔
672
            }
1 ✔
673
            if (config.getObservabilityMode()
1 ✔
674
                || (config.getFailureModeAllow() && !bodyMessageSentToExtProc.get())) {
1 ✔
675
              handleFailOpen(wrappedListener);
1 ✔
676
            } else {
677
              String message = "External processor stream failed";
1 ✔
678
              cancelDownstream(message, t);
1 ✔
679
              wrappedListener.proceedWithClose();
1 ✔
680
            }
681
          }
682
        }
1 ✔
683

684
        @Override
685
        public void onCompleted() {
686
          if (markExtProcStreamCompleted(extProcStreamState)) {
1 ✔
687
            synchronized (streamLock) {
1 ✔
688
              extProcClientCallRequestObserver = null;
1 ✔
689
            }
1 ✔
690
            handleFailOpen(wrappedListener);
1 ✔
691
          }
692
        }
1 ✔
693
      });
694

695
      this.collectedAttributes = collectAttributes(
1 ✔
696
          config.getRequestAttributes(), method, channel.authority(), headers);
1 ✔
697

698
      boolean sendRequestHeaders =
1 ✔
699
          currentProcessingMode.getRequestHeaderMode() == ProcessingMode.HeaderSendMode.SEND
1 ✔
700
          || currentProcessingMode.getRequestHeaderMode()
1 ✔
701
              == ProcessingMode.HeaderSendMode.DEFAULT;
702

703
      if (sendRequestHeaders) {
1 ✔
704
        sendToExtProc(ProcessingRequest.newBuilder()
1 ✔
705
            .setRequestHeaders(HttpHeaders.newBuilder()
1 ✔
706
                .setHeaders(toHeaderMap(headers, config.getForwardRulesConfig()))
1 ✔
707
                .setEndOfStream(false)
1 ✔
708
                .build())
1 ✔
709
            .build());
1 ✔
710
      }
711

712
      if (config.getObservabilityMode() || !sendRequestHeaders) {
1 ✔
713
        activateCall();
1 ✔
714
      }
715
    }
1 ✔
716

717
    private void sendToExtProc(ProcessingRequest request) {
718
      synchronized (streamLock) {
1 ✔
719
        if (extProcStreamState.get().isCompleted() || extProcClientCallRequestObserver == null) {
1 ✔
720
          return;
1 ✔
721
        }
722
        
723
        if (request.hasRequestHeaders()) {
1 ✔
724
          expectedRequestResponse = EventType.REQUEST_HEADERS;
1 ✔
725
        } else if (request.hasResponseHeaders()) {
1 ✔
726
          expectedResponseResponse = EventType.RESPONSE_HEADERS;
1 ✔
727
        } else if (request.hasResponseTrailers()) {
1 ✔
728
          expectedResponseResponse = EventType.RESPONSE_TRAILERS;
1 ✔
729
        }
730

731
        ProcessingRequest requestToSend = request;
1 ✔
732
        if (!protocolConfigSent) {
1 ✔
733
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1 ✔
734
              .setProtocolConfig(ProtocolConfiguration.newBuilder()
1 ✔
735
                  .setRequestBodyMode(currentProcessingMode.getRequestBodyMode())
1 ✔
736
                  .setResponseBodyMode(currentProcessingMode.getResponseBodyMode())
1 ✔
737
                  .build())
1 ✔
738
              .build();
1 ✔
739
          protocolConfigSent = true;
1 ✔
740
        }
741

742
        boolean isClientServerMessage =
1 ✔
743
            requestToSend.hasRequestHeaders() || requestToSend.hasRequestBody();
1 ✔
744
        if (isClientServerMessage
1 ✔
745
            && !requestAttributesSent
746
            && collectedAttributes != null
747
            && !collectedAttributes.isEmpty()) {
1 ✔
748
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1 ✔
749
              .putAllAttributes(collectedAttributes)
1 ✔
750
              .build();
1 ✔
751
          requestAttributesSent = true;
1 ✔
752
        }
753

754
        if (config.getObservabilityMode()) {
1 ✔
755
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1 ✔
756
              .setObservabilityMode(true)
1 ✔
757
              .build();
1 ✔
758
        } else if (!flowControlInitSent) {
1 ✔
759
          requestToSend = ProcessingRequest.newBuilder(requestToSend)
1 ✔
760
              .setFlowControlInit(ProcessingRequest.FlowControlInit.newBuilder()
1 ✔
761
                  .setInitialWindowDownstreamToSidestream(DEFAULT_INITIAL_WINDOW_SIZE)
1 ✔
762
                  .setInitialWindowSidestreamToUpstream(DEFAULT_INITIAL_WINDOW_SIZE)
1 ✔
763
                  .setInitialWindowUpstreamToSidestream(DEFAULT_INITIAL_WINDOW_SIZE)
1 ✔
764
                  .setInitialWindowSidestreamToDownstream(DEFAULT_INITIAL_WINDOW_SIZE)
1 ✔
765
                  .build())
1 ✔
766
              .build();
1 ✔
767
          flowControlInitSent = true;
1 ✔
768
        }
769

770
        extProcClientCallRequestObserver.onNext(requestToSend);
1 ✔
771
      }
1 ✔
772
    }
1 ✔
773

774
    // Note: This method not only modifies the builder, but has the side effect of modifying
775
    // the window update bookkeeping.
776
    @GuardedBy("streamLock")
777
    void mergeAccumulatedWindowUpdates(ProcessingRequest.Builder requestBuilder) {
778
      long incrementUpstream = accumulatedWindowUpdateSidestreamToUpstream;
1 ✔
779
      long incrementDownstream = accumulatedWindowUpdateSidestreamToDownstream;
1 ✔
780

781
      if (incrementUpstream > 0 || incrementDownstream > 0) {
1 ✔
782
        requestBuilder.setClientWindowUpdate(
1 ✔
783
            ProcessingRequest.ClientWindowUpdate.newBuilder()
1 ✔
784
                .setWindowIncrementSidestreamToUpstream(incrementUpstream)
1 ✔
785
                .setWindowIncrementSidestreamToDownstream(incrementDownstream)
1 ✔
786
                .build());
1 ✔
787
        accumulatedWindowUpdateSidestreamToUpstream -= incrementUpstream;
1 ✔
788
        accumulatedWindowUpdateSidestreamToDownstream -= incrementDownstream;
1 ✔
789
        sidestreamToUpstreamWindow += incrementUpstream;
1 ✔
790
        sidestreamToDownstreamWindow += incrementDownstream;
1 ✔
791
      }
792
    }
1 ✔
793

794
    private void trySendAccumulatedWindowUpdates() {
795
      synchronized (streamLock) {
1 ✔
796
        if (extProcStreamState.get().isCompleted()) {
1 ✔
797
          return;
1 ✔
798
        }
799
        long incrementUpstream = accumulatedWindowUpdateSidestreamToUpstream;
1 ✔
800
        long incrementDownstream = accumulatedWindowUpdateSidestreamToDownstream;
1 ✔
801

802
        boolean shouldSend = (incrementUpstream > 0 || incrementDownstream > 0) && (
1 ✔
803
            (incrementUpstream >= WINDOW_UPDATE_THRESHOLD)
804
            || (incrementDownstream >= WINDOW_UPDATE_THRESHOLD)
805
            || (sidestreamToUpstreamWindow <= 0 && accumulatedWindowUpdateSidestreamToUpstream > 0)
806
            || (sidestreamToDownstreamWindow <= 0
807
                && accumulatedWindowUpdateSidestreamToDownstream > 0)
808
        );
809

810
        if (shouldSend) {
1 ✔
811
          accumulatedWindowUpdateSidestreamToUpstream -= incrementUpstream;
1 ✔
812
          accumulatedWindowUpdateSidestreamToDownstream -= incrementDownstream;
1 ✔
813
          sidestreamToUpstreamWindow += incrementUpstream;
1 ✔
814
          sidestreamToDownstreamWindow += incrementDownstream;
1 ✔
815

816
          sendToExtProc(ProcessingRequest.newBuilder()
1 ✔
817
              .setClientWindowUpdate(ProcessingRequest.ClientWindowUpdate.newBuilder()
1 ✔
818
                  .setWindowIncrementSidestreamToUpstream(incrementUpstream)
1 ✔
819
                  .setWindowIncrementSidestreamToDownstream(incrementDownstream)
1 ✔
820
                  .build())
1 ✔
821
              .build());
1 ✔
822
        }
823
      }
1 ✔
824
    }
1 ✔
825

826
    private void onExtProcStreamReady() {
827
      drainPendingRequests();
1 ✔
828
      onReadyNotify();
1 ✔
829
    }
1 ✔
830

831
    void drainPendingRequests() {
832
      synchronized (streamLock) {
1 ✔
833
        if (config.getObservabilityMode()
1 ✔
834
            || currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC
1 ✔
835
            || extProcStreamState.get().isCompleted()) {
1 ✔
836
          int toRequest = pendingRequests.getAndSet(0);
1 ✔
837
          if (toRequest > 0) {
1 ✔
838
            super.request(toRequest);
1 ✔
839
          }
840
          return;
1 ✔
841
        }
842

843
        // Normal mode flow control: pull 1 message at a time
844
        if (isSidecarReady() && upstreamToSidestreamWindow > 0 && pendingRequests.get() > 0) {
1 ✔
845
          super.request(1);
1 ✔
846
          pendingRequests.decrementAndGet();
1 ✔
847
        }
848
      }
1 ✔
849
    }
1 ✔
850

851
    private void closeExtProcStream() {
852
      synchronized (streamLock) {
1 ✔
853
        if (markExtProcStreamCompleted(extProcStreamState)) {
1 ✔
854
          if (extProcClientCallRequestObserver != null) {
1 ✔
855
            extProcClientCallRequestObserver.onCompleted();
1 ✔
856
            extProcClientCallRequestObserver = null;
1 ✔
857
          }
858
        }
859
      }
1 ✔
860
    }
1 ✔
861

862
    private void internalOnError(Throwable t) {
863
      if (markExtProcStreamFailed(extProcStreamState)) {
1 ✔
864
        synchronized (streamLock) {
1 ✔
865
          if (extProcClientCallRequestObserver != null) {
1 ✔
866
            extProcClientCallRequestObserver.onError(t);
1 ✔
867
            extProcClientCallRequestObserver = null;
1 ✔
868
          }
869
        }
1 ✔
870
        if (config.getObservabilityMode()
1 ✔
871
            || (config.getFailureModeAllow() && !bodyMessageSentToExtProc.get())) {
1 ✔
872
          handleFailOpen(wrappedListener);
1 ✔
873
        } else {
874
          String message = "External processor stream failed";
1 ✔
875
          cancelDownstream(message, t);
1 ✔
876
          wrappedListener.proceedWithClose();
1 ✔
877
        }
878
      }
879
    }
1 ✔
880

881
    private void halfCloseExtProcStream() {
882
      synchronized (streamLock) {
1 ✔
883
        if (!extProcStreamState.get().isCompleted() && extProcClientCallRequestObserver != null) {
1 ✔
884
          extProcClientCallRequestObserver.onCompleted();
1 ✔
885
        }
886
      }
1 ✔
887
    }
1 ✔
888

889
    private void onReadyNotify() {
890
      wrappedListener.onReadyNotify();
1 ✔
891
    }
1 ✔
892

893
    void onReady() {
894
      boolean isPassThrough;
895
      boolean isCompleted;
896
      boolean isDraining;
897

898
      synchronized (streamLock) {
1 ✔
899
        isPassThrough = passThroughMode.get();
1 ✔
900
        ExtProcStreamState state = extProcStreamState.get();
1 ✔
901
        isCompleted = state.isCompleted();
1 ✔
902
        isDraining = state.isDraining();
1 ✔
903
      }
1 ✔
904

905
      if (isPassThrough) {
1 ✔
906
        onReadyNotify();
×
907
        return;
×
908
      }
909

910
      if (isCompleted) {
1 ✔
911
        drainPendingDrainingMessages();
1 ✔
912
        return;
1 ✔
913
      }
914

915
      // Normal or Draining operation
916
      drainPendingUpstreamBodyMessages();
1 ✔
917
      if (!isDraining) {
1 ✔
918
        trySendAccumulatedWindowUpdates();
1 ✔
919
      }
920
      drainPendingRequests();
1 ✔
921
      onReadyNotify();
1 ✔
922
    }
1 ✔
923

924
    @GuardedBy("streamLock")
925
    boolean isSidecarReady() {
926
      ExtProcStreamState state = extProcStreamState.get();
1 ✔
927
      if (state.isCompleted()) {
1 ✔
928
        return true;
×
929
      }
930
      if (state.isDraining()) {
1 ✔
931
        return false;
1 ✔
932
      }
933
      ClientCallStreamObserver<ProcessingRequest> observer = extProcClientCallRequestObserver;
1 ✔
934
      return observer != null && observer.isReady();
1 ✔
935
    }
936

937
    @Override
938
    public boolean isReady() {
939
      if (passThroughMode.get()) {
1 ✔
940
        return super.isReady();
1 ✔
941
      }
942
      if (extProcStreamState.get().isCompleted()) {
1 ✔
943
        return super.isReady();
1 ✔
944
      }
945
      if (dataPlaneCallState.get() == DataPlaneCallState.IDLE && !config.getObservabilityMode()) {
1 ✔
946
        return false;
1 ✔
947
      }
948
      synchronized (streamLock) {
1 ✔
949
        boolean sidecarReady = isSidecarReady();
1 ✔
950
        if (config.getObservabilityMode()) {
1 ✔
951
          return super.isReady() && sidecarReady;
1 ✔
952
        }
953
        return downstreamToSidestreamWindow > 0 && sidecarReady
1 ✔
954
            && pendingRequestBodyMessages.isEmpty();
1 ✔
955
      }
956
    }
957

958
    @Override
959
    public void request(int numMessages) {
960
      if (passThroughMode.get() || extProcStreamState.get().isCompleted()) {
1 ✔
961
        super.request(numMessages);
1 ✔
962
        return;
1 ✔
963
      }
964
      if (!config.getObservabilityMode()
1 ✔
965
          && currentProcessingMode.getResponseBodyMode() != ProcessingMode.BodySendMode.GRPC) {
1 ✔
966
        super.request(numMessages);
1 ✔
967
        return;
1 ✔
968
      }
969
      synchronized (streamLock) {
1 ✔
970
        // We send response bodies to ext_proc server (either in normal GRPC mode or
971
        // observability mode).
972
        // Gated by ext_proc server readiness.
973
        // i.e. normal GRPC response body mode
974
        boolean normalFlowControl = !config.getObservabilityMode();
1 ✔
975

976
        if (normalFlowControl) {
1 ✔
977
          pendingRequests.addAndGet(numMessages);
1 ✔
978
          downstreamRequestsPending += numMessages;
1 ✔
979
          drainPendingMutatedResponseBodies();
1 ✔
980
          if (isSidecarReady()) {
1 ✔
981
            drainPendingRequests();
1 ✔
982
          }
983
        } else {
984
          // Observability mode: gate on readiness but pull all at once
985
          if (isSidecarReady()) {
1 ✔
986
            super.request(numMessages);
1 ✔
987
          } else {
988
            pendingRequests.addAndGet(numMessages);
1 ✔
989
          }
990
        }
991
      }
1 ✔
992
    }
1 ✔
993

994
    @SuppressWarnings("unchecked")
995
    @Override
996
    public void sendMessage(ReqT message) {
997
      if (requestSideClosed.get()) {
1 ✔
998
        // External processor already closed the request stream. Discard further messages.
999
        return;
1 ✔
1000
      }
1001

1002
      if (passThroughMode.get()) {
1 ✔
1003
        super.sendMessage(message);
1 ✔
1004
        return;
1 ✔
1005
      }
1006

1007
      synchronized (streamLock) {
1 ✔
1008
        if (passThroughMode.get()) {
1 ✔
1009
          super.sendMessage(message);
×
1010
          return;
×
1011
        }
1012

1013
        ExtProcStreamState state = extProcStreamState.get();
1 ✔
1014
        if (state.isDraining() || state.isCompleted()) {
1 ✔
1015
          if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) {
1 ✔
1016
            super.sendMessage(message);
1 ✔
1017
            return;
1 ✔
1018
          }
1019
          try {
1020
            ByteString copiedBody = outboundStreamToByteString((InputStream) message);
1 ✔
1021
            pendingDrainingMessages.add((ReqT) new KnownLengthInputStream(copiedBody));
1 ✔
1022
          } catch (IOException e) {
×
1023
            cancelDownstream("Failed to copy outbound message for buffering", e);
×
1024
          }
1 ✔
1025
          return;
1 ✔
1026
        }
1027

1028
        if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) {
1 ✔
1029
          super.sendMessage(message);
1 ✔
1030
          return;
1 ✔
1031
        }
1032

1033
        // Mode is GRPC
1034
        try {
1035
          ByteString bodyByteString = outboundStreamToByteString((InputStream) message);
1 ✔
1036
          if (config.getObservabilityMode()) {
1 ✔
1037
            sendToExtProc(ProcessingRequest.newBuilder()
1 ✔
1038
                .setRequestBody(HttpBody.newBuilder()
1 ✔
1039
                    .setBody(bodyByteString)
1 ✔
1040
                    .setEndOfStream(false)
1 ✔
1041
                    .build())
1 ✔
1042
                .build());
1 ✔
1043
            bodyMessageSentToExtProc.set(true);
1 ✔
1044
            super.sendMessage((ReqT) new KnownLengthInputStream(bodyByteString));
1 ✔
1045
          } else {
1046
            if (downstreamToSidestreamWindow <= 0 || !pendingRequestBodyMessages.isEmpty()) {
1 ✔
1047
              pendingRequestBodyMessages.add(bodyByteString);
1 ✔
1048
            } else {
1049
              sendRequestBodyToExtProc(bodyByteString);
1 ✔
1050
            }
1051
          }
1052
        } catch (IOException e) {
×
1053
          cancelDownstream("Failed to serialize message for External Processor", e);
×
1054
        }
1 ✔
1055
      }
1 ✔
1056
    }
1 ✔
1057

1058
    @GuardedBy("streamLock")
1059
    private void sendRequestBodyToExtProc(ByteString body) {
1060
      downstreamToSidestreamWindow -= body.size();
1 ✔
1061
      ProcessingRequest.Builder builder = ProcessingRequest.newBuilder()
1 ✔
1062
          .setRequestBody(HttpBody.newBuilder()
1 ✔
1063
              .setBody(body)
1 ✔
1064
              .setEndOfStream(false)
1 ✔
1065
              .build());
1 ✔
1066
      mergeAccumulatedWindowUpdates(builder);
1 ✔
1067
      sendToExtProc(builder.build());
1 ✔
1068
      bodyMessageSentToExtProc.set(true);
1 ✔
1069
    }
1 ✔
1070

1071
    @GuardedBy("streamLock")
1072
    private void drainPendingRequestBodyMessages() {
1073
      while (downstreamToSidestreamWindow > 0 && !pendingRequestBodyMessages.isEmpty()) {
1 ✔
1074
        ByteString body = pendingRequestBodyMessages.poll();
1 ✔
1075
        sendRequestBodyToExtProc(body);
1 ✔
1076
      }
1 ✔
1077
      if (pendingRequestBodyMessages.isEmpty() && pendingHalfClose.compareAndSet(true, false)) {
1 ✔
1078
        halfClose();
1 ✔
1079
      }
1080
    }
1 ✔
1081

1082
    private void proceedWithHalfClose() {
1083
      if (clientHalfCloseStartNanos > 0) {
1 ✔
1084
        long durationNanos = System.nanoTime() - clientHalfCloseStartNanos;
1 ✔
1085
        recordDuration(clientHalfCloseDuration, durationNanos);
1 ✔
1086
        clientHalfCloseStartNanos = 0;
1 ✔
1087
      }
1088
      super.halfClose();
1 ✔
1089
    }
1 ✔
1090

1091
    @Override
1092
    public void halfClose() {
1093
      if (appHalfClosed.compareAndSet(false, true)) {
1 ✔
1094
        clientHalfCloseStartNanos = System.nanoTime();
1 ✔
1095
      }
1096
      if (passThroughMode.get()) {
1 ✔
1097
        if (requestSideClosed.compareAndSet(false, true)) {
1 ✔
1098
          proceedWithHalfClose();
1 ✔
1099
        }
1100
        return;
1 ✔
1101
      }
1102

1103
      if (extProcStreamState.get().isCompleted()) {
1 ✔
1104
        if (passThroughMode.get()) {
1 ✔
1105
          if (requestSideClosed.compareAndSet(false, true)) {
×
1106
            proceedWithHalfClose();
×
1107
          }
1108
        } else {
1109
          pendingHalfClose.set(true);
1 ✔
1110
        }
1111
        return;
1 ✔
1112
      }
1113

1114
      if (extProcStreamState.get().isDraining()) {
1 ✔
1115
        boolean canProceed = false;
1 ✔
1116
        synchronized (streamLock) {
1 ✔
1117
          if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE
1 ✔
1118
              || (!bodyMessageSentToExtProc.get() && pendingDrainingMessages.isEmpty())) {
1 ✔
1119
            canProceed = true;
1 ✔
1120
          }
1121
        }
1 ✔
1122
        if (canProceed) {
1 ✔
1123
          if (requestSideClosed.compareAndSet(false, true)) {
1 ✔
1124
            proceedWithHalfClose();
1 ✔
1125
          }
1126
        } else {
1127
          pendingHalfClose.set(true);
1 ✔
1128
        }
1129
        return;
1 ✔
1130
      }
1131

1132
      if (currentProcessingMode.getRequestBodyMode() == ProcessingMode.BodySendMode.NONE) {
1 ✔
1133
        if (requestSideClosed.compareAndSet(false, true)) {
1 ✔
1134
          proceedWithHalfClose();
1 ✔
1135
        }
1136
        return;
1 ✔
1137
      }
1138

1139
      // Mode is GRPC
1140
      synchronized (streamLock) {
1 ✔
1141
        if (!pendingRequestBodyMessages.isEmpty()) {
1 ✔
1142
          pendingHalfClose.set(true);
1 ✔
1143
          return;
1 ✔
1144
        }
1145

1146
        ProcessingRequest.Builder builder = ProcessingRequest.newBuilder()
1 ✔
1147
            .setRequestBody(HttpBody.newBuilder()
1 ✔
1148
                .setEndOfStream(true)
1 ✔
1149
                .setEndOfStreamWithoutMessage(true)
1 ✔
1150
                .build());
1 ✔
1151
        mergeAccumulatedWindowUpdates(builder);
1 ✔
1152
        sendToExtProc(builder.build());
1 ✔
1153
      }
1 ✔
1154
      if (config.getObservabilityMode()) {
1 ✔
1155
        if (requestSideClosed.compareAndSet(false, true)) {
1 ✔
1156
          proceedWithHalfClose();
1 ✔
1157
        }
1158
      }
1159
    }
1 ✔
1160

1161
    void cancelDownstream(@Nullable String message, @Nullable Throwable cause) {
1162
      delayedCall.cancel(message, cause);
1 ✔
1163
    }
1 ✔
1164

1165
    @Override
1166
    public void cancel(@Nullable String message, @Nullable Throwable cause) {
1167
      synchronized (streamLock) {
1 ✔
1168
        if (markExtProcStreamFailed(extProcStreamState)) {
1 ✔
1169
          if (extProcClientCallRequestObserver != null) {
1 ✔
1170
            extProcClientCallRequestObserver.onError(
1 ✔
1171
                Status.CANCELLED
1172
                    .withDescription(message)
1 ✔
1173
                    .withCause(cause)
1 ✔
1174
                    .asRuntimeException());
1 ✔
1175
            extProcClientCallRequestObserver = null;
1 ✔
1176
          }
1177
        }
1178
      }
1 ✔
1179
      cancelDownstream(message, cause);
1 ✔
1180
    }
1 ✔
1181

1182
    @SuppressWarnings("unchecked")
1183
    private void handleRequestBodyResponse(BodyResponse bodyResponse) {
1184
      if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1 ✔
1185
        BodyMutation mutation = bodyResponse.getResponse().getBodyMutation();
1 ✔
1186
        if (mutation.hasStreamedResponse()) {
1 ✔
1187
          StreamedBodyResponse streamed = mutation.getStreamedResponse();
1 ✔
1188
          boolean isEndOfStream = streamed.getEndOfStream();
1 ✔
1189
          boolean isEndOfStreamWithoutMessage =
1 ✔
1190
              isEndOfStream && streamed.getEndOfStreamWithoutMessage();
1 ✔
1191
          if (!isEndOfStreamWithoutMessage) {
1 ✔
1192
            ByteString body = streamed.getBody();
1 ✔
1193
            boolean sendImmediately = false;
1 ✔
1194
            synchronized (streamLock) {
1 ✔
1195
              sidestreamToUpstreamWindow -= body.size();
1 ✔
1196
              if (pendingUpstreamBodyMessages.isEmpty() && super.isReady()) {
1 ✔
1197
                sendImmediately = true;
1 ✔
1198
                accumulatedWindowUpdateSidestreamToUpstream += body.size();
1 ✔
1199
              } else {
1200
                pendingUpstreamBodyMessages.add(body);
1 ✔
1201
              }
1202
            }
1 ✔
1203
            if (sendImmediately) {
1 ✔
1204
              super.sendMessage((ReqT) new KnownLengthInputStream(body));
1 ✔
1205
              trySendAccumulatedWindowUpdates();
1 ✔
1206
            }
1207
          }
1208
          if (isEndOfStream) {
1 ✔
1209
            synchronized (streamLock) {
1 ✔
1210
              if (pendingUpstreamBodyMessages.isEmpty()) {
1 ✔
1211
                if (requestSideClosed.compareAndSet(false, true)) {
1 ✔
1212
                  proceedWithHalfClose();
1 ✔
1213
                }
1214
              } else {
1215
                pendingUpstreamHalfClose.set(true);
1 ✔
1216
              }
1217
            }
1 ✔
1218
          }
1219
        }
1220
      }
1221
    }
1 ✔
1222

1223
    private void handleResponseBodyResponse(
1224
        BodyResponse bodyResponse, DataPlaneListener<RespT> listener) {
1225
      if (bodyResponse.hasResponse() && bodyResponse.getResponse().hasBodyMutation()) {
1 ✔
1226
        BodyMutation mutation = bodyResponse.getResponse().getBodyMutation();
1 ✔
1227
        if (mutation.hasStreamedResponse()) {
1 ✔
1228
          StreamedBodyResponse streamed = mutation.getStreamedResponse();
1 ✔
1229
          ByteString body = streamed.getBody();
1 ✔
1230
          final int bodySize = body.size();
1 ✔
1231
          synchronized (streamLock) {
1 ✔
1232
            sidestreamToDownstreamWindow -= bodySize;
1 ✔
1233
          }
1 ✔
1234
          deliverResponseBody(body, listener);
1 ✔
1235
        }
1236
      }
1237
    }
1 ✔
1238

1239
    private void deliverResponseBody(ByteString body, DataPlaneListener<RespT> listener) {
1240
      boolean shouldDeliver = false;
1 ✔
1241
      synchronized (streamLock) {
1 ✔
1242
        if (downstreamRequestsPending > 0) {
1 ✔
1243
          downstreamRequestsPending--;
1 ✔
1244
          shouldDeliver = true;
1 ✔
1245
        } else {
1246
          pendingMutatedResponseBodies.add(body);
1 ✔
1247
        }
1248
      }
1 ✔
1249
      if (shouldDeliver) {
1 ✔
1250
        final int bodySize = body.size();
1 ✔
1251
        callContext.run(() -> {
1 ✔
1252
          try {
1253
            listener.onExternalBody(body);
1 ✔
1254
          } finally {
1255
            synchronized (streamLock) {
1 ✔
1256
              accumulatedWindowUpdateSidestreamToDownstream += bodySize;
1 ✔
1257
            }
1 ✔
1258
            trySendAccumulatedWindowUpdates();
1 ✔
1259
          }
1260
        });
1 ✔
1261
      }
1262
    }
1 ✔
1263

1264
    private void drainPendingMutatedResponseBodies() {
1265
      List<ByteString> toDeliver = new ArrayList<>();
1 ✔
1266
      synchronized (streamLock) {
1 ✔
1267
        while (downstreamRequestsPending > 0 && !pendingMutatedResponseBodies.isEmpty()) {
1 ✔
1268
          ByteString body = pendingMutatedResponseBodies.poll();
1 ✔
1269
          downstreamRequestsPending--;
1 ✔
1270
          pendingRequests.decrementAndGet();
1 ✔
1271
          toDeliver.add(body);
1 ✔
1272
        }
1 ✔
1273
      }
1 ✔
1274
      for (ByteString body : toDeliver) {
1 ✔
1275
        final int bodySize = body.size();
1 ✔
1276
        callContext.run(() -> {
1 ✔
1277
          try {
1278
            wrappedListener.onExternalBody(body);
1 ✔
1279
          } finally {
1280
            synchronized (streamLock) {
1 ✔
1281
              accumulatedWindowUpdateSidestreamToDownstream += bodySize;
1 ✔
1282
            }
1 ✔
1283
            trySendAccumulatedWindowUpdates();
1 ✔
1284
          }
1285
        });
1 ✔
1286
      }
1 ✔
1287
    }
1 ✔
1288

1289
    // Used to immediately flush any mutated response chunks that we already received and buffered
1290
    // before the stream failed, ensuring the application receives them in the correct order
1291
    void drainPendingMutatedResponseBodiesDirect(DataPlaneListener<RespT> listener) {
1292
      List<ByteString> toDeliver = new ArrayList<>();
1 ✔
1293
      synchronized (streamLock) {
1 ✔
1294
        ByteString body;
1295
        while ((body = pendingMutatedResponseBodies.poll()) != null) {
1 ✔
1296
          toDeliver.add(body);
×
1297
        }
1298
      }
1 ✔
1299
      for (ByteString body : toDeliver) {
1 ✔
1300
        listener.onExternalBody(body);
×
1301
      }
×
1302
    }
1 ✔
1303

1304
    @SuppressWarnings("unchecked")
1305
    void drainPendingUpstreamBodyMessages() {
1306
      while (true) {
1307
        ByteString body = null;
1 ✔
1308
        boolean triggerHalfClose = false;
1 ✔
1309
        synchronized (streamLock) {
1 ✔
1310
          if (!pendingUpstreamBodyMessages.isEmpty() && super.isReady()) {
1 ✔
1311
            body = pendingUpstreamBodyMessages.poll();
1 ✔
1312
            accumulatedWindowUpdateSidestreamToUpstream += body.size();
1 ✔
1313
            if (pendingUpstreamBodyMessages.isEmpty()
1 ✔
1314
                && pendingUpstreamHalfClose.compareAndSet(true, false)) {
1 ✔
1315
              triggerHalfClose = true;
1 ✔
1316
            }
1317
          }
1318
        }
1 ✔
1319
        if (body == null) {
1 ✔
1320
          break;
1 ✔
1321
        }
1322
        super.sendMessage((ReqT) new KnownLengthInputStream(body));
1 ✔
1323
        trySendAccumulatedWindowUpdates();
1 ✔
1324
        if (triggerHalfClose) {
1 ✔
1325
          if (requestSideClosed.compareAndSet(false, true)) {
1 ✔
1326
            proceedWithHalfClose();
1 ✔
1327
          }
1328
        }
1329
      }
1 ✔
1330
    }
1 ✔
1331

1332
    private void handleImmediateResponse(
1333
        ImmediateResponse immediate, DataPlaneListener<RespT> listener)
1334
        throws HeaderMutationDisallowedException {
1335
      Status status = Status.fromCodeValue(immediate.getGrpcStatus().getStatus());
1 ✔
1336
      if (!immediate.getDetails().isEmpty()) {
1 ✔
1337
        status = status.withDescription(immediate.getDetails());
1 ✔
1338
      }
1339

1340
      Metadata trailers = new Metadata();
1 ✔
1341
      if (immediate.hasHeaders()) {
1 ✔
1342
        applyHeaderMutations(trailers, immediate.getHeaders(), mutationFilter, mutator);
1 ✔
1343
      }
1344

1345
      listener.setImmediateResponse(status, trailers);
1 ✔
1346

1347
      if (isProcessingTrailers.get()) {
1 ✔
1348
        // If sent in response to a server trailers event, sets the status and optionally
1349
        // headers to be included in the trailers.
1350
        listener.unblockAfterStreamComplete();
1 ✔
1351
      } else {
1352
        // If sent in response to any other event, it will cause the data plane RPC to
1353
        // immediately fail with the specified status as if it were an out-of-band
1354
        // cancellation.
1355
        cancelDownstream(status.getDescription(), null);
1 ✔
1356
        listener.unblockAfterStreamComplete();
1 ✔
1357
      }
1358
      closeExtProcStream();
1 ✔
1359
    }
1 ✔
1360

1361
    @SuppressWarnings("unchecked")
1362
    private void drainPendingDrainingMessages() {
1363
      while (true) {
1364
        Object msg = null; // Can be ByteString or InputStream
1 ✔
1365
        boolean isMutated = false;
1 ✔
1366
        boolean triggerHalfClose = false;
1 ✔
1367

1368
        synchronized (streamLock) {
1 ✔
1369
          if (!pendingUpstreamBodyMessages.isEmpty() && super.isReady()) {
1 ✔
1370
            msg = pendingUpstreamBodyMessages.poll();
1 ✔
1371
            isMutated = true;
1 ✔
1372
          } else if (pendingUpstreamBodyMessages.isEmpty()
1 ✔
1373
              && !pendingRequestBodyMessages.isEmpty() && super.isReady()) {
1 ✔
1374
            msg = pendingRequestBodyMessages.poll();
1 ✔
1375
            isMutated = true;
1 ✔
1376
          } else if (pendingUpstreamBodyMessages.isEmpty()
1 ✔
1377
              && pendingRequestBodyMessages.isEmpty()
1 ✔
1378
              && !pendingDrainingMessages.isEmpty() && super.isReady()) {
1 ✔
1379
            msg = pendingDrainingMessages.poll();
1 ✔
1380
            isMutated = false;
1 ✔
1381
          }
1382

1383
          if (msg == null) {
1 ✔
1384
            if (pendingUpstreamBodyMessages.isEmpty()
1 ✔
1385
                && pendingRequestBodyMessages.isEmpty()
1 ✔
1386
                && pendingDrainingMessages.isEmpty()) {
1 ✔
1387
              passThroughMode.set(true);
1 ✔
1388
              if (appHalfClosed.get()) {
1 ✔
1389
                triggerHalfClose = true;
1 ✔
1390
              }
1391
            }
1392
          }
1393
        }
1 ✔
1394

1395
        if (msg == null) {
1 ✔
1396
          if (triggerHalfClose) {
1 ✔
1397
            if (requestSideClosed.compareAndSet(false, true)) {
1 ✔
1398
              proceedWithHalfClose();
1 ✔
1399
            }
1400
          }
1401
          break;
1402
        }
1403

1404
        if (isMutated) {
1 ✔
1405
          super.sendMessage((ReqT) new KnownLengthInputStream((ByteString) msg));
1 ✔
1406
        } else {
1407
          super.sendMessage((ReqT) msg);
1 ✔
1408
        }
1409
      }
1 ✔
1410
    }
1 ✔
1411

1412
    private void handleFailOpen(DataPlaneListener<RespT> listener) {
1413
      if (!activateCall()) {
1 ✔
1414
        drainPendingRequests();
1 ✔
1415
      }
1416
      listener.unblockAfterStreamComplete();
1 ✔
1417
      closeExtProcStream();
1 ✔
1418
    }
1 ✔
1419

1420
    private void checkEndOfStream(ProcessingResponse response) {
1421
      boolean terminal = false;
1 ✔
1422
      if (response.hasResponseTrailers()) {
1 ✔
1423
        terminal = true;
1 ✔
1424
      } else if (response.hasResponseHeaders() && wrappedListener.isTrailersOnly()) {
1 ✔
1425
        terminal = true;
1 ✔
1426
      }
1427

1428
      if (terminal) {
1 ✔
1429
        wrappedListener.unblockAfterStreamComplete();
1 ✔
1430
        closeExtProcStream();
1 ✔
1431
      }
1432
    }
1 ✔
1433

1434
    long getServerHeadersStartNanos() {
1435
      return serverHeadersStartNanos;
1 ✔
1436
    }
1437

1438
    void setServerHeadersStartNanos(long serverHeadersStartNanos) {
1439
      this.serverHeadersStartNanos = serverHeadersStartNanos;
1 ✔
1440
    }
1 ✔
1441

1442
    long getServerTrailersStartNanos() {
1443
      return serverTrailersStartNanos;
1 ✔
1444
    }
1445

1446
    void setServerTrailersStartNanos(long serverTrailersStartNanos) {
1447
      this.serverTrailersStartNanos = serverTrailersStartNanos;
1 ✔
1448
    }
1 ✔
1449

1450
    AtomicReference<ExtProcStreamState> getExtProcStreamState() {
1451
      return extProcStreamState;
1 ✔
1452
    }
1453

1454
    ProcessingMode getCurrentProcessingMode() {
1455
      return currentProcessingMode;
1 ✔
1456
    }
1457

1458
    AtomicBoolean getPassThroughMode() {
1459
      return passThroughMode;
1 ✔
1460
    }
1461

1462
    ExternalProcessorFilterConfig getConfig() {
1463
      return config;
1 ✔
1464
    }
1465

1466
    Context getCallContext() {
1467
      return callContext;
1 ✔
1468
    }
1469

1470
    ScheduledExecutorService getScheduler() {
1471
      return scheduler;
1 ✔
1472
    }
1473

1474
    AtomicBoolean getIsProcessingTrailers() {
1475
      return isProcessingTrailers;
1 ✔
1476
    }
1477
  }
1478

1479
  private static class DataPlaneListener<RespT> extends SimpleForwardingClientCallListener<RespT> {
1480
    private final DataPlaneClientCall<?, RespT> dataPlaneClientCall;
1481
    // Path 3: Upstream response bodies queued because upstream to sidestream window not available,
1482
    // response headers not cleared by ext_proc or ext_proc stream draining
1483
    private final Queue<RespT> savedMessages = new ConcurrentLinkedQueue<>();
1 ✔
1484
    private boolean inboundPassThrough = false;
1 ✔
1485
    @Nullable private volatile Metadata savedHeaders;
1486
    @Nullable private volatile Metadata savedTrailers;
1487
    @Nullable private volatile Status savedStatus;
1488
    private final AtomicBoolean terminationTriggered = new AtomicBoolean(false);
1 ✔
1489
    private final AtomicBoolean responseHeadersSent = new AtomicBoolean(false);
1 ✔
1490
    private final AtomicBoolean trailersOnly = new AtomicBoolean(false);
1 ✔
1491

1492
    protected DataPlaneListener(
1493
        ClientCall.Listener<RespT> delegate,
1494
        DataPlaneClientCall<?, RespT> dataPlaneClientCall) {
1495
      super(delegate);
1 ✔
1496
      this.dataPlaneClientCall = dataPlaneClientCall;
1 ✔
1497
    }
1 ✔
1498

1499
    boolean isTrailersOnly() {
1500
      return trailersOnly.get();
1 ✔
1501
    }
1502

1503
    Metadata getSavedHeaders() {
1504
      return savedHeaders;
1 ✔
1505
    }
1506

1507
    Metadata getSavedTrailers() {
1508
      return savedTrailers;
1 ✔
1509
    }
1510

1511
    void setImmediateResponse(Status status, Metadata trailers) {
1512
      this.savedStatus = status;
1 ✔
1513
      this.savedTrailers = trailers;
1 ✔
1514
    }
1 ✔
1515

1516
    @Override
1517
    public void onReady() {
1518
      dataPlaneClientCall.onReady();
1 ✔
1519
    }
1 ✔
1520

1521
    @Override
1522
    public void onHeaders(Metadata headers) {
1523
      dataPlaneClientCall.setServerHeadersStartNanos(System.nanoTime());
1 ✔
1524
      responseHeadersSent.set(true);
1 ✔
1525
      boolean sendResponseHeaders =
1 ✔
1526
          dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1 ✔
1527
              == ProcessingMode.HeaderSendMode.SEND
1528
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1 ✔
1529
              == ProcessingMode.HeaderSendMode.DEFAULT;
1530

1531
      if (dataPlaneClientCall.getExtProcStreamState().get().isDraining() && sendResponseHeaders) {
1 ✔
1532
        this.savedHeaders = headers;
1 ✔
1533
        return;
1 ✔
1534
      }
1535

1536
      if (dataPlaneClientCall.getPassThroughMode().get()
1 ✔
1537
          || dataPlaneClientCall.getExtProcStreamState().get().isCompleted() 
1 ✔
1538
          || !sendResponseHeaders) {
1539
        proceedWithHeaders(headers);
1 ✔
1540
        return;
1 ✔
1541
      }
1542

1543
      this.savedHeaders = headers;
1 ✔
1544
      dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1 ✔
1545
          .setResponseHeaders(HttpHeaders.newBuilder()
1 ✔
1546
              .setHeaders(
1 ✔
1547
                  toHeaderMap(headers, dataPlaneClientCall.getConfig().getForwardRulesConfig()))
1 ✔
1548
              .build())
1 ✔
1549
          .build());
1 ✔
1550

1551
      if (dataPlaneClientCall.getConfig().getObservabilityMode()) {
1 ✔
1552
        proceedWithHeaders();
1 ✔
1553
      }
1554
    }
1 ✔
1555

1556
    @SuppressWarnings("unchecked")
1557
    @Override
1558
    public void onMessage(RespT message) {
1559
      synchronized (dataPlaneClientCall.streamLock) {
1 ✔
1560
        if (inboundPassThrough) {
1 ✔
1561
          dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message));
1 ✔
1562
          return;
1 ✔
1563
        }
1564

1565
        boolean checkDrain = dataPlaneClientCall.getExtProcStreamState().get().isDraining()
1 ✔
1566
            && dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode()
1 ✔
1567
                == ProcessingMode.BodySendMode.GRPC;
1568

1569
        if (savedHeaders != null || checkDrain) {
1 ✔
1570
          if (message instanceof InputStream) {
1 ✔
1571
            try {
1572
              ByteString copiedBody = ByteString.readFrom((InputStream) message);
1 ✔
1573
              savedMessages.add((RespT) new KnownLengthInputStream(copiedBody));
1 ✔
1574
            } catch (IOException e) {
×
1575
              dataPlaneClientCall.cancelDownstream(
×
1576
                  "Failed to copy inbound message for buffering", e);
1577
            }
1 ✔
1578
          } else {
1579
            savedMessages.add(message);
1 ✔
1580
          }
1581
          return;
1 ✔
1582
        }
1583

1584
        if (dataPlaneClientCall.getPassThroughMode().get()
1 ✔
1585
            || dataPlaneClientCall.getExtProcStreamState().get().isCompleted()
1 ✔
1586
            || dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode()
1 ✔
1587
                != ProcessingMode.BodySendMode.GRPC) {
1588
          dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(message));
1 ✔
1589
          return;
1 ✔
1590
        }
1591

1592
        try {
1593
          ByteString bodyByteString = ByteString.readFrom((InputStream) message);
1 ✔
1594
          // TODO: Consider having separate classes handling normal mode and observability mode
1595
          if (dataPlaneClientCall.getConfig().getObservabilityMode()) {
1 ✔
1596
            sendResponseBodyToExtProc(bodyByteString, false);
1 ✔
1597
            dataPlaneClientCall.bodyMessageSentToExtProc.set(true);
1 ✔
1598
            dataPlaneClientCall.getCallContext().run(
1 ✔
1599
                () -> delegate().onMessage((RespT) bodyByteString.newInput()));
1 ✔
1600
          } else {
1601
            if (dataPlaneClientCall.upstreamToSidestreamWindow <= 0 || !savedMessages.isEmpty()) {
1 ✔
1602
              savedMessages.add((RespT) new KnownLengthInputStream(bodyByteString));
1 ✔
1603
            } else {
1604
              dataPlaneClientCall.upstreamToSidestreamWindow -= bodyByteString.size();
1 ✔
1605
              sendResponseBodyToExtProc(bodyByteString, false);
1 ✔
1606
              dataPlaneClientCall.bodyMessageSentToExtProc.set(true);
1 ✔
1607
            }
1608
            dataPlaneClientCall.drainPendingRequests();
1 ✔
1609
          }
1610
        } catch (IOException e) {
×
1611
          dataPlaneClientCall.cancelDownstream("Failed to read server response", e);
×
1612
        }
1 ✔
1613
      }
1 ✔
1614
    }
1 ✔
1615

1616
    void drainSavedMessages() {
1617
      synchronized (dataPlaneClientCall.streamLock) {
1 ✔
1618
        if (dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode()
1 ✔
1619
            != ProcessingMode.BodySendMode.GRPC) {
1620
          return;
1 ✔
1621
        }
1622
        while (dataPlaneClientCall.isSidecarReady()
1 ✔
1623
            && dataPlaneClientCall.upstreamToSidestreamWindow > 0
1 ✔
1624
            && !savedMessages.isEmpty()) {
1 ✔
1625
          RespT msg = savedMessages.poll();
1 ✔
1626
          if (msg != null) {
1 ✔
1627
            try {
1628
              ByteString bodyByteString = ByteString.readFrom((InputStream) msg);
1 ✔
1629
              dataPlaneClientCall.upstreamToSidestreamWindow -= bodyByteString.size();
1 ✔
1630
              sendResponseBodyToExtProc(bodyByteString, false);
1 ✔
1631
              dataPlaneClientCall.bodyMessageSentToExtProc.set(true);
1 ✔
1632
            } catch (IOException e) {
×
1633
              dataPlaneClientCall.cancelDownstream("Failed to read buffered response body", e);
×
1634
            }
1 ✔
1635
          }
1636
        }
1 ✔
1637
        dataPlaneClientCall.drainPendingRequests();
1 ✔
1638
      }
1 ✔
1639
    }
1 ✔
1640

1641
    @Override
1642
    public void onClose(Status status, Metadata trailers) {
1643
      dataPlaneClientCall.setServerTrailersStartNanos(System.nanoTime());
1 ✔
1644
      ExtProcStreamState extProcStreamState =
1 ✔
1645
          dataPlaneClientCall.getExtProcStreamState().get();
1 ✔
1646
      if (extProcStreamState.isFailed()
1 ✔
1647
          && !dataPlaneClientCall.getConfig().getObservabilityMode()
1 ✔
1648
          && (!dataPlaneClientCall.getConfig().getFailureModeAllow()
1 ✔
1649
              || dataPlaneClientCall.bodyMessageSentToExtProc.get())) {
1 ✔
1650
        if (markDataPlaneCallClosed(dataPlaneClientCall.dataPlaneCallState)) {
1 ✔
1651
          proceedWithClose(Status.INTERNAL.withDescription("External processor stream failed")
1 ✔
1652
              .withCause(status.getCause()), new Metadata());
1 ✔
1653
        }
1654
        return;
1 ✔
1655
      }
1656
      if (dataPlaneClientCall.getPassThroughMode().get()) {
1 ✔
1657
        if (markDataPlaneCallClosed(dataPlaneClientCall.dataPlaneCallState)) {
1 ✔
1658
          proceedWithClose(status, trailers);
1 ✔
1659
        }
1660
        return;
1 ✔
1661
      }
1662

1663
      if (this.savedStatus == null) {
1 ✔
1664
        this.savedStatus = status;
1 ✔
1665
        this.savedTrailers = trailers;
1 ✔
1666
      }
1667

1668
      // If we are still waiting for the external processor to validate response headers,
1669
      // buffer the close status/trailers and defer the close until headers are processed.
1670
      if (savedHeaders != null) {
1 ✔
1671
        return;
1 ✔
1672
      }
1673

1674
      boolean sendResponseTrailers =
1 ✔
1675
          dataPlaneClientCall.getCurrentProcessingMode().getResponseTrailerMode()
1 ✔
1676
              == ProcessingMode.HeaderSendMode.SEND;
1677

1678
      if (dataPlaneClientCall.getExtProcStreamState().get().isDraining() && sendResponseTrailers) {
1 ✔
1679
        return;
×
1680
      }
1681

1682
      if (!responseHeadersSent.get()) {
1 ✔
1683
        trailersOnly.set(true);
1 ✔
1684
      }
1685

1686
      triggerCloseHandshake();
1 ✔
1687
    }
1 ✔
1688

1689
    void onReadyNotify() {
1690
      dataPlaneClientCall.getCallContext().run(() -> delegate().onReady());
1 ✔
1691
    }
1 ✔
1692

1693
    void proceedWithHeaders() {
1694
      if (savedHeaders != null) {
1 ✔
1695
        proceedWithHeaders(savedHeaders);
1 ✔
1696
        synchronized (dataPlaneClientCall.streamLock) {
1 ✔
1697
          savedHeaders = null;
1 ✔
1698
          if (!dataPlaneClientCall.getExtProcStreamState().get().isDraining()) {
1 ✔
1699
            RespT msg;
1700
            while ((msg = savedMessages.poll()) != null) {
1 ✔
1701
              onMessage(msg);
1 ✔
1702
            }
1703
          }
1704
        }
1 ✔
1705
        onReadyNotify();
1 ✔
1706
        if (savedStatus != null) {
1 ✔
1707
          triggerCloseHandshake();
1 ✔
1708
        }
1709
      }
1710
    }
1 ✔
1711

1712
    private void proceedWithHeaders(Metadata headers) {
1713
      if (dataPlaneClientCall.getServerHeadersStartNanos() > 0) {
1 ✔
1714
        long durationNanos = System.nanoTime() - dataPlaneClientCall.getServerHeadersStartNanos();
1 ✔
1715
        dataPlaneClientCall.recordDuration(serverHeadersDuration, durationNanos);
1 ✔
1716
        dataPlaneClientCall.setServerHeadersStartNanos(0);
1 ✔
1717
      }
1718
      dataPlaneClientCall.getCallContext().run(() -> delegate().onHeaders(headers));
1 ✔
1719
    }
1 ✔
1720

1721
    void proceedWithClose() {
1722
      if (savedStatus != null) {
1 ✔
1723
        if (markDataPlaneCallClosed(dataPlaneClientCall.dataPlaneCallState)) {
1 ✔
1724
          proceedWithClose(savedStatus, savedTrailers);
1 ✔
1725
        }
1726
        savedStatus = null;
1 ✔
1727
        savedTrailers = null;
1 ✔
1728
      }
1729
    }
1 ✔
1730

1731
    private void proceedWithClose(Status status, Metadata trailers) {
1732
      if (dataPlaneClientCall.getServerTrailersStartNanos() > 0) {
1 ✔
1733
        long durationNanos = System.nanoTime() - dataPlaneClientCall.getServerTrailersStartNanos();
1 ✔
1734
        dataPlaneClientCall.recordDuration(serverTrailersDuration, durationNanos);
1 ✔
1735
        dataPlaneClientCall.setServerTrailersStartNanos(0);
1 ✔
1736
      }
1737
      dataPlaneClientCall.getCallContext().run(() -> delegate().onClose(status, trailers));
1 ✔
1738
    }
1 ✔
1739

1740
    @SuppressWarnings("unchecked")
1741
    void onExternalBody(ByteString body) {
1742
      // If needed, downstream reading can be made more optimal by creating a wrapped
1743
      // Inputstream wraps the underlying bytestring and that implements HasByteBuffer,
1744
      // Detachable, KnownLength
1745
      dataPlaneClientCall.getCallContext().run(
1 ✔
1746
          () -> delegate().onMessage((RespT) body.newInput()));
1 ✔
1747
    }
1 ✔
1748

1749
    void unblockAfterStreamComplete() {
1750
      proceedWithHeaders();
1 ✔
1751
      // 1. Drain mutated responses first
1752
      dataPlaneClientCall.drainPendingMutatedResponseBodiesDirect(this);
1 ✔
1753
      // 2. Drain raw responses
1754
      proceedWithSavedMessages();
1 ✔
1755
      // 3. Drain outbound requests
1756
      dataPlaneClientCall.drainPendingDrainingMessages();
1 ✔
1757
      proceedWithClose();
1 ✔
1758
    }
1 ✔
1759

1760
    private void proceedWithSavedMessages() {
1761
      synchronized (dataPlaneClientCall.streamLock) {
1 ✔
1762
        RespT msg;
1763
        while ((msg = savedMessages.poll()) != null) {
1 ✔
1764
          final RespT finalMsg = msg;
×
1765
          dataPlaneClientCall.getCallContext().run(() -> delegate().onMessage(finalMsg));
×
1766
        }
×
1767
        inboundPassThrough = true;
1 ✔
1768
      }
1 ✔
1769
    }
1 ✔
1770

1771
    private void triggerCloseHandshake() {
1772
      if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted()
1 ✔
1773
          || !terminationTriggered.compareAndSet(false, true)) {
1 ✔
1774
        return;
1 ✔
1775
      }
1776

1777
      boolean sendResponseHeaders =
1 ✔
1778
          dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1 ✔
1779
              == ProcessingMode.HeaderSendMode.SEND
1780
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseHeaderMode()
1 ✔
1781
              == ProcessingMode.HeaderSendMode.DEFAULT;
1782

1783
      boolean sendResponseTrailers =
1 ✔
1784
          dataPlaneClientCall.getCurrentProcessingMode().getResponseTrailerMode()
1 ✔
1785
              == ProcessingMode.HeaderSendMode.SEND;
1786

1787
      if (trailersOnly.get()) {
1 ✔
1788
        if (sendResponseHeaders) {
1 ✔
1789
          dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1 ✔
1790
              .setResponseHeaders(HttpHeaders.newBuilder()
1 ✔
1791
                  .setHeaders(
1 ✔
1792
                      toHeaderMap(
1 ✔
1793
                          savedTrailers,
1794
                          dataPlaneClientCall.getConfig().getForwardRulesConfig()))
1 ✔
1795
                  .setEndOfStream(true)
1 ✔
1796
                  .build())
1 ✔
1797
              .build());
1 ✔
1798
        } else {
1799
          proceedWithClose();
1 ✔
1800
          if (!dataPlaneClientCall.getConfig().getObservabilityMode()) {
1 ✔
1801
            dataPlaneClientCall.closeExtProcStream();
1 ✔
1802
          }
1803
        }
1804
      } else if (sendResponseTrailers) {
1 ✔
1805
        dataPlaneClientCall.getIsProcessingTrailers().set(true);
1 ✔
1806
        dataPlaneClientCall.sendToExtProc(ProcessingRequest.newBuilder()
1 ✔
1807
            .setResponseTrailers(HttpTrailers.newBuilder()
1 ✔
1808
                .setTrailers(
1 ✔
1809
                    toHeaderMap(
1 ✔
1810
                        savedTrailers,
1811
                        dataPlaneClientCall.getConfig().getForwardRulesConfig()))
1 ✔
1812
                .build())
1 ✔
1813
            .build());
1 ✔
1814
      } else {
1815
        proceedWithClose();
1 ✔
1816
        if (!dataPlaneClientCall.getConfig().getObservabilityMode()) {
1 ✔
1817
          dataPlaneClientCall.closeExtProcStream();
1 ✔
1818
        }
1819
      }
1820

1821
      if (dataPlaneClientCall.getConfig().getObservabilityMode()) {
1 ✔
1822
        proceedWithClose();
1 ✔
1823
        @SuppressWarnings("unused")
1824
        ScheduledFuture<?> unused = dataPlaneClientCall.getScheduler().schedule(
1 ✔
1825
            dataPlaneClientCall::closeExtProcStream,
1 ✔
1826
            dataPlaneClientCall.getConfig().getDeferredCloseTimeoutNanos(),
1 ✔
1827
            TimeUnit.NANOSECONDS);
1828
      }
1829
    }
1 ✔
1830

1831
    @GuardedBy("dataPlaneClientCall.streamLock")
1832
    private void sendResponseBodyToExtProc(
1833
        @Nullable ByteString bodyByteString, boolean endOfStream) {
1834
      if (dataPlaneClientCall.getExtProcStreamState().get().isCompleted()
1 ✔
1835
          || dataPlaneClientCall.getCurrentProcessingMode().getResponseBodyMode()
1 ✔
1836
              != ProcessingMode.BodySendMode.GRPC) {
1837
        return;
×
1838
      }
1839

1840
      HttpBody.Builder bodyBuilder =
1841
          HttpBody.newBuilder();
1 ✔
1842
      if (bodyByteString != null) {
1 ✔
1843
        bodyBuilder.setBody(bodyByteString);
1 ✔
1844
      }
1845
      bodyBuilder.setEndOfStream(endOfStream);
1 ✔
1846

1847
      ProcessingRequest.Builder builder = ProcessingRequest.newBuilder()
1 ✔
1848
          .setResponseBody(bodyBuilder.build());
1 ✔
1849
      dataPlaneClientCall.mergeAccumulatedWindowUpdates(builder);
1 ✔
1850
      dataPlaneClientCall.sendToExtProc(builder.build());
1 ✔
1851
    }
1 ✔
1852
  }
1853
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc