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

grpc / grpc-java / #20536

16 Oct 2026 01:01PM UTC coverage: 89.418% (-0.05%) from 89.464%
#20536

push

github

web-flow
netty: Ignore inbound frames without a transport state (#13073)

HEADERS and DATA callbacks dereference a null `TransportState` when the
HTTP/2 stream exists without an associated gRPC stream, which can shut
down the client connection. Ignore these headers and return the
discarded DATA payload and padding to Netty's inbound flow controller.
Keepalive still observes the received frames.

Add four regression tests that feed serialized frames through the
decoder, covering headers, trailers, and DATA with and without
END_STREAM. They check buffer release, consumed flow-control bytes,
keepalive notifications, and continued delivery to another stream.

Fixes #13065.

## Validation

- The four new tests fail with the reported null dereference against
upstream `9e0ff283e`.
- `NettyClientHandlerTest`: 63 tests passed after the fix.
- Netty main/test Checkstyle and Animal Sniffer checks passed.
- Full Netty suite on macOS with JDK 21: 574 passed, 79 skipped, and 2
timed out. `NettyServerTest.multiPortConnections` and
`NettyServerTest.connectionSettingsPropagated` both also time out after
60 seconds at the same stack locations in a clean checkout of upstream
`9e0ff283e`. The full suite is not green locally.

The tests construct the missing-transport-state condition
deterministically; they do not reproduce the original Pub/Sub startup
race.

39359 of 44017 relevant lines covered (89.42%)

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