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

grpc / grpc-java / #20494

30 Sep 2026 08:29AM UTC coverage: 89.351% (+0.05%) from 89.306%
#20494

push

github

web-flow
core: Implement [A121](https://github.com/grpc/proposal/pull/556) (#12893)

39251 of 43929 relevant lines covered (89.35%)

0.89 hits per line

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

87.63
/../inprocess/src/main/java/io/grpc/inprocess/InProcessTransport.java
1
/*
2
 * Copyright 2015 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.inprocess;
18

19
import static com.google.common.base.Preconditions.checkNotNull;
20
import static io.grpc.internal.GrpcUtil.TIMEOUT_KEY;
21

22
import com.google.common.base.MoreObjects;
23
import com.google.common.io.ByteStreams;
24
import com.google.common.util.concurrent.ListenableFuture;
25
import com.google.common.util.concurrent.SettableFuture;
26
import com.google.errorprone.annotations.CheckReturnValue;
27
import com.google.errorprone.annotations.concurrent.GuardedBy;
28
import io.grpc.Attributes;
29
import io.grpc.CallOptions;
30
import io.grpc.ClientStreamTracer;
31
import io.grpc.Compressor;
32
import io.grpc.Deadline;
33
import io.grpc.Decompressor;
34
import io.grpc.DecompressorRegistry;
35
import io.grpc.Grpc;
36
import io.grpc.InternalChannelz.SocketStats;
37
import io.grpc.InternalLogId;
38
import io.grpc.InternalMetadata;
39
import io.grpc.KnownLength;
40
import io.grpc.Metadata;
41
import io.grpc.MethodDescriptor;
42
import io.grpc.SecurityLevel;
43
import io.grpc.ServerStreamTracer;
44
import io.grpc.Status;
45
import io.grpc.SynchronizationContext;
46
import io.grpc.internal.ClientStream;
47
import io.grpc.internal.ClientStreamListener;
48
import io.grpc.internal.ClientStreamListener.RpcProgress;
49
import io.grpc.internal.ConnectionClientTransport;
50
import io.grpc.internal.GrpcAttributes;
51
import io.grpc.internal.GrpcUtil;
52
import io.grpc.internal.InUseStateAggregator;
53
import io.grpc.internal.InsightBuilder;
54
import io.grpc.internal.ManagedClientTransport;
55
import io.grpc.internal.NoopClientStream;
56
import io.grpc.internal.ObjectPool;
57
import io.grpc.internal.ServerStream;
58
import io.grpc.internal.ServerStreamListener;
59
import io.grpc.internal.ServerTransport;
60
import io.grpc.internal.ServerTransportListener;
61
import io.grpc.internal.SimpleDisconnectError;
62
import io.grpc.internal.StatsTraceContext;
63
import io.grpc.internal.StreamListener;
64
import java.io.ByteArrayInputStream;
65
import java.io.InputStream;
66
import java.net.SocketAddress;
67
import java.util.ArrayDeque;
68
import java.util.ArrayList;
69
import java.util.Collections;
70
import java.util.IdentityHashMap;
71
import java.util.List;
72
import java.util.Locale;
73
import java.util.Set;
74
import java.util.concurrent.Executor;
75
import java.util.concurrent.ScheduledExecutorService;
76
import java.util.concurrent.TimeUnit;
77
import java.util.logging.Level;
78
import java.util.logging.Logger;
79
import javax.annotation.Nullable;
80
import javax.annotation.concurrent.ThreadSafe;
81

82
@ThreadSafe
83
final class InProcessTransport implements ServerTransport, ConnectionClientTransport {
84
  private static final Logger log = Logger.getLogger(InProcessTransport.class.getName());
1 ✔
85
  static boolean isEnabledSupportTracingMessageSizes =
1 ✔
86
      GrpcUtil.getFlag("GRPC_EXPERIMENTAL_SUPPORT_TRACING_MESSAGE_SIZES", false);
1 ✔
87

88
  private final InternalLogId logId;
89
  private final SocketAddress address;
90
  private final int clientMaxInboundMetadataSize;
91
  private final String authority;
92
  private final String userAgent;
93
  private int serverMaxInboundMetadataSize;
94
  private final boolean includeCauseWithStatus;
95
  private ObjectPool<ScheduledExecutorService> serverSchedulerPool;
96
  private ScheduledExecutorService serverScheduler;
97
  private ServerTransportListener serverTransportListener;
98
  private Attributes serverStreamAttributes;
99
  private ManagedClientTransport.Listener clientTransportListener;
100
  // The size is assumed from the sender's side.
101
  private final long assumedMessageSize;
102
  @GuardedBy("this")
103
  private boolean shutdown;
104
  @GuardedBy("this")
105
  private boolean terminated;
106
  @GuardedBy("this")
107
  private Status shutdownStatus;
108
  @GuardedBy("this")
1 ✔
109
  private final Set<InProcessStream> streams = Collections.newSetFromMap(
1 ✔
110
          new IdentityHashMap<InProcessStream, Boolean>());
111
  @GuardedBy("this")
112
  private List<ServerStreamTracer.Factory> serverStreamTracerFactories;
113
  private Attributes attributes;
114

115
  private Thread.UncaughtExceptionHandler uncaughtExceptionHandler =
1 ✔
116
      new Thread.UncaughtExceptionHandler() {
1 ✔
117
        @Override
118
        public void uncaughtException(Thread t, Throwable e) {
119
          if (e instanceof Error) {
×
120
            throw new Error(e);
×
121
          }
122
          throw new RuntimeException(e);
×
123
        }
124
      };
125

126

127
  @GuardedBy("this")
1 ✔
128
  private final InUseStateAggregator<InProcessStream> inUseState =
129
      new InUseStateAggregator<InProcessStream>() {
1 ✔
130
        @Override
131
        protected void handleInUse() {
132
          clientTransportListener.transportInUse(true);
1 ✔
133
        }
1 ✔
134

135
        @Override
136
        protected void handleNotInUse() {
137
          clientTransportListener.transportInUse(false);
1 ✔
138
        }
1 ✔
139
      };
140

141
  public InProcessTransport(SocketAddress address, int maxInboundMetadataSize, String authority,
142
      String userAgent, Attributes eagAttrs,
143
      boolean includeCauseWithStatus, long assumedMessageSize) {
1 ✔
144
    this.address = address;
1 ✔
145
    this.clientMaxInboundMetadataSize = maxInboundMetadataSize;
1 ✔
146
    this.authority = authority;
1 ✔
147
    this.userAgent = GrpcUtil.getGrpcUserAgent("inprocess", userAgent);
1 ✔
148
    checkNotNull(eagAttrs, "eagAttrs");
1 ✔
149
    this.attributes = Attributes.newBuilder()
1 ✔
150
        .set(GrpcAttributes.ATTR_SECURITY_LEVEL, SecurityLevel.PRIVACY_AND_INTEGRITY)
1 ✔
151
        .set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttrs)
1 ✔
152
        .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, address)
1 ✔
153
        .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, address)
1 ✔
154
        .build();
1 ✔
155
    logId = InternalLogId.allocate(getClass(), address.toString());
1 ✔
156
    this.includeCauseWithStatus = includeCauseWithStatus;
1 ✔
157
    this.assumedMessageSize = assumedMessageSize;
1 ✔
158
  }
1 ✔
159

160
  @CheckReturnValue
161
  @Override
162
  public synchronized Runnable start(ManagedClientTransport.Listener listener) {
163
    this.clientTransportListener = listener;
1 ✔
164
    InProcessServer server = InProcessServer.findServer(address);
1 ✔
165
    if (server != null) {
1 ✔
166
      serverMaxInboundMetadataSize = server.getMaxInboundMetadataSize();
1 ✔
167
      serverSchedulerPool = server.getScheduledExecutorServicePool();
1 ✔
168
      serverScheduler = serverSchedulerPool.getObject();
1 ✔
169
      serverStreamTracerFactories = server.getStreamTracerFactories();
1 ✔
170
      // Must be semi-initialized; past this point, can begin receiving requests
171
      serverTransportListener = server.register(this);
1 ✔
172
    }
173
    if (serverTransportListener == null) {
1 ✔
174
      shutdownStatus = Status.UNAVAILABLE.withDescription("Could not find server: " + address);
1 ✔
175
      final Status localShutdownStatus = shutdownStatus;
1 ✔
176
      return new Runnable() {
1 ✔
177
        @Override
178
        public void run() {
179
          synchronized (InProcessTransport.this) {
1 ✔
180
            notifyShutdown(localShutdownStatus);
1 ✔
181
            notifyTerminated();
1 ✔
182
          }
1 ✔
183
        }
1 ✔
184
      };
185
    }
186
    Attributes serverTransportAttrs = Attributes.newBuilder()
1 ✔
187
        .set(Grpc.TRANSPORT_ATTR_REMOTE_ADDR, address)
1 ✔
188
        .set(Grpc.TRANSPORT_ATTR_LOCAL_ADDR, address)
1 ✔
189
        .build();
1 ✔
190
    serverStreamAttributes = serverTransportListener.transportReady(serverTransportAttrs);
1 ✔
191
    attributes = clientTransportListener.filterTransport(attributes);
1 ✔
192
    clientTransportListener.transportReady();
1 ✔
193
    return null;
1 ✔
194
  }
195

196
  @Override
197
  public synchronized ClientStream newStream(
198
      MethodDescriptor<?, ?> method, Metadata headers, CallOptions callOptions,
199
      ClientStreamTracer[] tracers) {
200
    StatsTraceContext statsTraceContext =
1 ✔
201
        StatsTraceContext.newClientContext(tracers, getAttributes(), headers);
1 ✔
202
    if (shutdownStatus != null) {
1 ✔
203
      return failedClientStream(statsTraceContext, shutdownStatus);
1 ✔
204
    }
205

206
    headers.put(GrpcUtil.USER_AGENT_KEY, userAgent);
1 ✔
207

208
    if (serverMaxInboundMetadataSize != Integer.MAX_VALUE) {
1 ✔
209
      int metadataSize = metadataSize(headers);
1 ✔
210
      if (metadataSize > serverMaxInboundMetadataSize) {
1 ✔
211
        // Other transports would compute a status with:
212
        //   GrpcUtil.httpStatusToGrpcStatus(431 /* Request Header Fields Too Large */);
213
        // However, that isn't handled specially today, so we'd leak HTTP-isms even though we're
214
        // in-process. We go ahead and make a Status, which may need to be updated if
215
        // statuscodes.md is updated.
216
        Status status = Status.RESOURCE_EXHAUSTED.withDescription(
1 ✔
217
            String.format(
1 ✔
218
                Locale.US,
219
                "Request metadata larger than %d: %d",
220
                serverMaxInboundMetadataSize,
1 ✔
221
                metadataSize));
1 ✔
222
        return failedClientStream(statsTraceContext, status);
1 ✔
223
      }
224
    }
225

226
    return new InProcessStream(method, headers, callOptions, authority, statsTraceContext)
1 ✔
227
        .clientStream;
1 ✔
228
  }
229

230
  private ClientStream failedClientStream(
231
      final StatsTraceContext statsTraceCtx, final Status status) {
232
    return new NoopClientStream() {
1 ✔
233
        @Override
234
        public void start(ClientStreamListener listener) {
235
          statsTraceCtx.clientOutboundHeaders();
1 ✔
236
          statsTraceCtx.streamClosed(status);
1 ✔
237
          listener.closed(status, RpcProgress.PROCESSED, new Metadata());
1 ✔
238
        }
1 ✔
239
      };
240
  }
241

242
  @Override
243
  public synchronized void ping(final PingCallback callback, Executor executor) {
244
    if (terminated) {
1 ✔
245
      final Status shutdownStatus = this.shutdownStatus;
1 ✔
246
      executor.execute(new Runnable() {
1 ✔
247
        @Override
248
        public void run() {
249
          callback.onFailure(shutdownStatus);
1 ✔
250
        }
1 ✔
251
      });
252
    } else {
1 ✔
253
      executor.execute(new Runnable() {
1 ✔
254
        @Override
255
        public void run() {
256
          callback.onSuccess(0);
1 ✔
257
        }
1 ✔
258
      });
259
    }
260
  }
1 ✔
261

262
  @Override
263
  public synchronized void shutdown(Status reason) {
264
    // Can be called multiple times: once for ManagedClientTransport, once for ServerTransport.
265
    if (shutdown) {
1 ✔
266
      return;
1 ✔
267
    }
268
    shutdownStatus = reason;
1 ✔
269
    notifyShutdown(reason);
1 ✔
270
    if (streams.isEmpty()) {
1 ✔
271
      notifyTerminated();
1 ✔
272
    }
273
  }
1 ✔
274

275
  @Override
276
  public synchronized void shutdown() {
277
    shutdown(Status.UNAVAILABLE.withDescription("InProcessTransport shutdown by the server-side"));
1 ✔
278
  }
1 ✔
279

280
  @Override
281
  public void shutdownNow(Status reason) {
282
    checkNotNull(reason, "reason");
1 ✔
283
    List<InProcessStream> streamsCopy;
284
    synchronized (this) {
1 ✔
285
      shutdown(reason);
1 ✔
286
      if (terminated) {
1 ✔
287
        return;
1 ✔
288
      }
289
      streamsCopy = new ArrayList<>(streams);
1 ✔
290
    }
1 ✔
291
    for (InProcessStream stream : streamsCopy) {
1 ✔
292
      stream.clientStream.cancel(reason);
1 ✔
293
    }
1 ✔
294
  }
1 ✔
295

296
  @Override
297
  public String toString() {
298
    return MoreObjects.toStringHelper(this)
×
299
        .add("logId", logId.getId())
×
300
        .add("address", address)
×
301
        .toString();
×
302
  }
303

304
  @Override
305
  public InternalLogId getLogId() {
306
    return logId;
1 ✔
307
  }
308

309
  @Override
310
  public Attributes getAttributes() {
311
    return attributes;
1 ✔
312
  }
313

314
  @Override
315
  public ScheduledExecutorService getScheduledExecutorService() {
316
    return serverScheduler;
1 ✔
317
  }
318

319
  @Override
320
  public ListenableFuture<SocketStats> getStats() {
321
    SettableFuture<SocketStats> ret = SettableFuture.create();
×
322
    ret.set(null);
×
323
    return ret;
×
324
  }
325

326
  private synchronized void notifyShutdown(Status s) {
327
    if (shutdown) {
1 ✔
328
      return;
×
329
    }
330
    shutdown = true;
1 ✔
331
    clientTransportListener.transportShutdown(s, SimpleDisconnectError.SUBCHANNEL_SHUTDOWN);
1 ✔
332
  }
1 ✔
333

334
  private synchronized void notifyTerminated() {
335
    if (terminated) {
1 ✔
336
      return;
×
337
    }
338
    terminated = true;
1 ✔
339
    if (serverScheduler != null) {
1 ✔
340
      serverScheduler = serverSchedulerPool.returnObject(serverScheduler);
1 ✔
341
    }
342
    clientTransportListener.transportTerminated();
1 ✔
343
    if (serverTransportListener != null) {
1 ✔
344
      serverTransportListener.transportTerminated();
1 ✔
345
    }
346
  }
1 ✔
347

348
  private static int metadataSize(Metadata metadata) {
349
    byte[][] serialized = InternalMetadata.serialize(metadata);
1 ✔
350
    if (serialized == null) {
1 ✔
351
      return 0;
×
352
    }
353
    // Calculate based on SETTINGS_MAX_HEADER_LIST_SIZE in RFC 7540 §6.5.2. We could use something
354
    // different, but it's "sane."
355
    long size = 0;
1 ✔
356
    for (int i = 0; i < serialized.length; i += 2) {
1 ✔
357
      size += 32 + serialized[i].length + serialized[i + 1].length;
1 ✔
358
    }
359
    size = Math.min(size, Integer.MAX_VALUE);
1 ✔
360
    return (int) size;
1 ✔
361
  }
362

363
  private class InProcessStream {
364
    private final InProcessClientStream clientStream;
365
    private final InProcessServerStream serverStream;
366
    private final CallOptions callOptions;
367
    private final Metadata headers;
368
    private final MethodDescriptor<?, ?> method;
369
    private volatile String authority;
370

371
    private InProcessStream(
372
        MethodDescriptor<?, ?> method, Metadata headers, CallOptions callOptions,
373
        String authority , StatsTraceContext statsTraceContext) {
1 ✔
374
      this.method = checkNotNull(method, "method");
1 ✔
375
      this.headers = checkNotNull(headers, "headers");
1 ✔
376
      this.callOptions = checkNotNull(callOptions, "callOptions");
1 ✔
377
      this.authority = authority;
1 ✔
378
      this.clientStream = new InProcessClientStream(callOptions, statsTraceContext);
1 ✔
379
      this.serverStream = new InProcessServerStream(method, headers);
1 ✔
380
    }
1 ✔
381

382
    // Can be called multiple times due to races on both client and server closing at same time.
383
    private void streamClosed() {
384
      synchronized (InProcessTransport.this) {
1 ✔
385
        boolean justRemovedAnElement = streams.remove(this);
1 ✔
386
        if (GrpcUtil.shouldBeCountedForInUse(callOptions)) {
1 ✔
387
          inUseState.updateObjectInUse(this, false);
1 ✔
388
        }
389
        if (streams.isEmpty() && justRemovedAnElement) {
1 ✔
390
          if (shutdown) {
1 ✔
391
            notifyTerminated();
1 ✔
392
          }
393
        }
394
      }
1 ✔
395
    }
1 ✔
396

397
    private class InProcessServerStream implements ServerStream {
398
      final StatsTraceContext statsTraceCtx;
399
      // All callbacks must run in syncContext to avoid possibility of deadlock in direct executors
400
      private ClientStreamListener clientStreamListener;
401
      private final SynchronizationContext syncContext =
1 ✔
402
          new SynchronizationContext(uncaughtExceptionHandler);
1 ✔
403
      @GuardedBy("this")
404
      private int clientRequested;
405
      @GuardedBy("this")
1 ✔
406
      private ArrayDeque<StreamListener.MessageProducer> clientReceiveQueue =
407
          new ArrayDeque<>();
408
      @GuardedBy("this")
409
      private Status clientNotifyStatus;
410
      @GuardedBy("this")
411
      private Metadata clientNotifyTrailers;
412
      // Only is intended to prevent double-close when client cancels.
413
      @GuardedBy("this")
414
      private boolean closed;
415
      @GuardedBy("this")
416
      private int outboundSeqNo;
417

418
      InProcessServerStream(MethodDescriptor<?, ?> method, Metadata headers) {
1 ✔
419
        statsTraceCtx = StatsTraceContext.newServerContext(
1 ✔
420
            serverStreamTracerFactories, method.getFullMethodName(), headers);
1 ✔
421
      }
1 ✔
422

423
      private synchronized void setListener(ClientStreamListener listener) {
424
        clientStreamListener = listener;
1 ✔
425
      }
1 ✔
426

427
      @Override
428
      public void setListener(ServerStreamListener serverStreamListener) {
429
        clientStream.setListener(serverStreamListener);
1 ✔
430
      }
1 ✔
431

432
      @Override
433
      public void triggerEvent(Object event) {
434
        clientStream.triggerServerEvent(event);
1 ✔
435
      }
1 ✔
436

437
      @Override
438
      public void request(int numMessages) {
439
        boolean onReady = clientStream.serverRequested(numMessages);
1 ✔
440
        if (onReady) {
1 ✔
441
          synchronized (this) {
1 ✔
442
            if (!closed) {
1 ✔
443
              syncContext.executeLater(() -> clientStreamListener.onReady());
1 ✔
444
            }
445
          }
1 ✔
446
        }
447
        syncContext.drain();
1 ✔
448
      }
1 ✔
449

450
      // This method is the only reason we have to synchronize field accesses.
451
      /**
452
       * Client requested more messages.
453
       *
454
       * @return whether onReady should be called on the server
455
       */
456
      private boolean clientRequested(int numMessages) {
457
        boolean previouslyReady;
458
        boolean nowReady;
459
        synchronized (this) {
1 ✔
460
          if (closed) {
1 ✔
461
            return false;
1 ✔
462
          }
463

464
          previouslyReady = clientRequested > 0;
1 ✔
465
          clientRequested += numMessages;
1 ✔
466
          while (clientRequested > 0 && !clientReceiveQueue.isEmpty()) {
1 ✔
467
            clientRequested--;
1 ✔
468
            StreamListener.MessageProducer producer = clientReceiveQueue.poll();
1 ✔
469
            syncContext.executeLater(() -> clientStreamListener.messagesAvailable(producer));
1 ✔
470
          }
1 ✔
471

472
          if (clientReceiveQueue.isEmpty() && clientNotifyStatus != null) {
1 ✔
473
            closed = true;
1 ✔
474
            clientStream.statsTraceCtx.clientInboundTrailers(clientNotifyTrailers);
1 ✔
475
            clientStream.statsTraceCtx.streamClosed(clientNotifyStatus);
1 ✔
476
            Status notifyStatus = this.clientNotifyStatus;
1 ✔
477
            Metadata notifyTrailers = this.clientNotifyTrailers;
1 ✔
478
            syncContext.executeLater(() ->
1 ✔
479
                clientStreamListener.closed(notifyStatus, RpcProgress.PROCESSED, notifyTrailers));
1 ✔
480
          }
481

482
          nowReady = clientRequested > 0;
1 ✔
483
        }
1 ✔
484

485
        syncContext.drain();
1 ✔
486
        return !previouslyReady && nowReady;
1 ✔
487
      }
488

489
      private void clientCancelled(Status status) {
490
        internalCancel(status);
1 ✔
491
      }
1 ✔
492

493
      @Override
494
      public void writeMessage(InputStream message) {
495
        long messageLength = 0;
1 ✔
496
        if (isEnabledSupportTracingMessageSizes) {
1 ✔
497
          try {
498
            if (assumedMessageSize != -1) {
×
499
              messageLength = assumedMessageSize;
×
500
            } else if (message instanceof KnownLength || message instanceof ByteArrayInputStream) {
×
501
              messageLength = message.available();
×
502
            } else {
503
              InputStream oldMessage = message;
×
504
              byte[] payload = ByteStreams.toByteArray(message);
×
505
              messageLength = payload.length;
×
506
              message = new ByteArrayInputStream(payload);
×
507
              oldMessage.close();
×
508
            }
509
          } catch (Exception e) {
×
510
            throw new RuntimeException("Error processing the message length", e);
×
511
          }
×
512
        }
513

514
        synchronized (this) {
1 ✔
515
          if (closed) {
1 ✔
516
            return;
1 ✔
517
          }
518
          statsTraceCtx.outboundMessage(outboundSeqNo);
1 ✔
519
          statsTraceCtx.outboundMessageSent(outboundSeqNo, -1, -1);
1 ✔
520
          clientStream.statsTraceCtx.inboundMessage(outboundSeqNo);
1 ✔
521
          clientStream.statsTraceCtx.inboundMessageRead(outboundSeqNo, -1, -1);
1 ✔
522
          if (isEnabledSupportTracingMessageSizes) {
1 ✔
523
            statsTraceCtx.outboundUncompressedSize(messageLength);
×
524
            statsTraceCtx.outboundWireSize(messageLength);
×
525
            // messageLength should be same at receiver's end as no actual wire is involved.
526
            clientStream.statsTraceCtx.inboundUncompressedSize(messageLength);
×
527
            clientStream.statsTraceCtx.inboundWireSize(messageLength);
×
528
          }
529
          outboundSeqNo++;
1 ✔
530
          StreamListener.MessageProducer producer = new SingleMessageProducer(message);
1 ✔
531
          if (clientRequested > 0) {
1 ✔
532
            clientRequested--;
1 ✔
533
            syncContext.executeLater(() -> clientStreamListener.messagesAvailable(producer));
1 ✔
534
          } else {
535
            clientReceiveQueue.add(producer);
1 ✔
536
          }
537
        }
1 ✔
538
        syncContext.drain();
1 ✔
539
      }
1 ✔
540

541
      @Override
542
      public void flush() {}
1 ✔
543

544
      @Override
545
      public synchronized boolean isReady() {
546
        if (closed) {
1 ✔
547
          return false;
×
548
        }
549
        return clientRequested > 0;
1 ✔
550
      }
551

552
      @Override
553
      public void writeHeaders(Metadata headers, boolean flush) {
554
        if (clientMaxInboundMetadataSize != Integer.MAX_VALUE) {
1 ✔
555
          int metadataSize = metadataSize(headers);
1 ✔
556
          if (metadataSize > clientMaxInboundMetadataSize) {
1 ✔
557
            Status serverStatus = Status.CANCELLED.withDescription("Client cancelled the RPC");
1 ✔
558
            clientStream.serverClosed(serverStatus, serverStatus);
1 ✔
559
            // Other transports provide very little information in this case. We go ahead and make a
560
            // Status, which may need to be updated if statuscodes.md is updated.
561
            Status failedStatus = Status.RESOURCE_EXHAUSTED.withDescription(
1 ✔
562
                String.format(
1 ✔
563
                    Locale.US,
564
                    "Response header metadata larger than %d: %d",
565
                    clientMaxInboundMetadataSize,
1 ✔
566
                    metadataSize));
1 ✔
567
            notifyClientClose(failedStatus, new Metadata());
1 ✔
568
            return;
1 ✔
569
          }
570
        }
571

572
        synchronized (this) {
1 ✔
573
          if (closed) {
1 ✔
574
            return;
1 ✔
575
          }
576

577
          clientStream.statsTraceCtx.clientInboundHeaders(headers);
1 ✔
578
          syncContext.executeLater(() -> clientStreamListener.headersRead(headers));
1 ✔
579
        }
1 ✔
580
        syncContext.drain();
1 ✔
581
      }
1 ✔
582

583
      @Override
584
      public void close(Status status, Metadata trailers) {
585
        // clientStream.serverClosed must happen before clientStreamListener.closed, otherwise
586
        // clientStreamListener.closed can trigger clientStream.cancel (see code in
587
        // ClientCalls.blockingUnaryCall), which may race with clientStream.serverClosed as both are
588
        // calling internalCancel().
589
        clientStream.serverClosed(Status.OK, status);
1 ✔
590

591
        if (clientMaxInboundMetadataSize != Integer.MAX_VALUE) {
1 ✔
592
          int statusSize = status.getDescription() == null ? 0 : status.getDescription().length();
1 ✔
593
          // Go ahead and throw in the status description's length, since that could be very long.
594
          int metadataSize = metadataSize(trailers) + statusSize;
1 ✔
595
          if (metadataSize > clientMaxInboundMetadataSize) {
1 ✔
596
            // Override the status for the client, but not the server. Transports do not guarantee
597
            // notifying the server of the failure.
598

599
            // Other transports provide very little information in this case. We go ahead and make a
600
            // Status, which may need to be updated if statuscodes.md is updated.
601
            status = Status.RESOURCE_EXHAUSTED.withDescription(
1 ✔
602
                String.format(
1 ✔
603
                    Locale.US,
604
                    "Response header metadata larger than %d: %d",
605
                    clientMaxInboundMetadataSize,
1 ✔
606
                    metadataSize));
1 ✔
607
            trailers = new Metadata();
1 ✔
608
          }
609
        }
610

611
        notifyClientClose(status, trailers);
1 ✔
612
      }
1 ✔
613

614
      /** clientStream.serverClosed() must be called before this method. */
615
      private void notifyClientClose(Status status, Metadata trailers) {
616
        Status clientStatus = cleanStatus(status, includeCauseWithStatus);
1 ✔
617
        synchronized (this) {
1 ✔
618
          if (closed) {
1 ✔
619
            return;
1 ✔
620
          }
621
          if (clientReceiveQueue.isEmpty()) {
1 ✔
622
            closed = true;
1 ✔
623
            clientStream.statsTraceCtx.clientInboundTrailers(trailers);
1 ✔
624
            clientStream.statsTraceCtx.streamClosed(clientStatus);
1 ✔
625
            syncContext.executeLater(
1 ✔
626
                () -> clientStreamListener.closed(clientStatus, RpcProgress.PROCESSED, trailers));
1 ✔
627
          } else {
628
            clientNotifyStatus = clientStatus;
1 ✔
629
            clientNotifyTrailers = trailers;
1 ✔
630
          }
631
        }
1 ✔
632
        syncContext.drain();
1 ✔
633
        streamClosed();
1 ✔
634
      }
1 ✔
635

636
      @Override
637
      public void cancel(Status status) {
638
        if (!internalCancel(Status.CANCELLED.withDescription("server cancelled stream"))) {
1 ✔
639
          return;
1 ✔
640
        }
641
        clientStream.serverClosed(status, status);
1 ✔
642
        streamClosed();
1 ✔
643
      }
1 ✔
644

645
      private boolean internalCancel(Status clientStatus) {
646
        synchronized (this) {
1 ✔
647
          if (closed) {
1 ✔
648
            return false;
1 ✔
649
          }
650
          closed = true;
1 ✔
651
          StreamListener.MessageProducer producer;
652
          while ((producer = clientReceiveQueue.poll()) != null) {
1 ✔
653
            InputStream message;
654
            while ((message = producer.next()) != null) {
1 ✔
655
              try {
656
                message.close();
1 ✔
657
              } catch (Throwable t) {
×
658
                log.log(Level.WARNING, "Exception closing stream", t);
×
659
              }
1 ✔
660
            }
661
          }
1 ✔
662
          clientStream.statsTraceCtx.streamClosed(clientStatus);
1 ✔
663
          syncContext.executeLater(
1 ✔
664
              () ->
665
                  clientStreamListener.closed(clientStatus, RpcProgress.PROCESSED, new Metadata()));
1 ✔
666
        }
1 ✔
667
        syncContext.drain();
1 ✔
668
        return true;
1 ✔
669
      }
670

671
      @Override
672
      public void setMessageCompression(boolean enable) {
673
        // noop
674
      }
×
675

676
      @Override
677
      public void optimizeForDirectExecutor() {}
1 ✔
678

679
      @Override
680
      public void setCompressor(Compressor compressor) {}
1 ✔
681

682
      @Override
683
      public void setDecompressor(Decompressor decompressor) {}
×
684

685
      @Override public Attributes getAttributes() {
686
        return serverStreamAttributes;
1 ✔
687
      }
688

689
      @Override
690
      public String getAuthority() {
691
        return InProcessStream.this.authority;
1 ✔
692
      }
693

694
      @Override
695
      public StatsTraceContext statsTraceContext() {
696
        return statsTraceCtx;
1 ✔
697
      }
698

699
      @Override
700
      public int streamId() {
701
        return -1;
1 ✔
702
      }
703

704
      @Override
705
      public void setOnReadyThreshold(int numBytes) {
706
        // noop
707
      }
×
708
    }
709

710
    private class InProcessClientStream implements ClientStream {
711
      final StatsTraceContext statsTraceCtx;
712
      final CallOptions callOptions;
713
      // All callbacks must run in syncContext to avoid possibility of deadlock in direct executors
714
      private ServerStreamListener serverStreamListener;
715
      private final SynchronizationContext syncContext =
1 ✔
716
          new SynchronizationContext(uncaughtExceptionHandler);
1 ✔
717
      @GuardedBy("this")
718
      private int serverRequested;
719
      @GuardedBy("this")
1 ✔
720
      private ArrayDeque<StreamListener.MessageProducer> serverReceiveQueue =
721
          new ArrayDeque<>();
722
      @GuardedBy("this")
723
      private boolean serverNotifyHalfClose;
724
      // Only is intended to prevent double-close when server closes.
725
      @GuardedBy("this")
726
      private boolean closed;
727
      @GuardedBy("this")
728
      private int outboundSeqNo;
729

730
      InProcessClientStream(
731
          CallOptions callOptions, StatsTraceContext statsTraceContext) {
1 ✔
732
        this.callOptions = callOptions;
1 ✔
733
        statsTraceCtx = statsTraceContext;
1 ✔
734
      }
1 ✔
735

736
      private synchronized void setListener(ServerStreamListener listener) {
737
        this.serverStreamListener = listener;
1 ✔
738
      }
1 ✔
739

740
      void triggerServerEvent(final Object event) {
741
        synchronized (this) {
1 ✔
742
          if (!closed) {
1 ✔
743
            syncContext.executeLater(new Runnable() {
1 ✔
744
              @Override
745
              public void run() {
746
                serverStreamListener.triggerEvent(event);
1 ✔
747
              }
1 ✔
748
            });
749
          }
750
        }
1 ✔
751
        syncContext.drain();
1 ✔
752
      }
1 ✔
753

754
      @Override
755
      public void request(int numMessages) {
756
        boolean onReady = serverStream.clientRequested(numMessages);
1 ✔
757
        if (onReady) {
1 ✔
758
          synchronized (this) {
1 ✔
759
            if (!closed) {
1 ✔
760
              syncContext.executeLater(() -> serverStreamListener.onReady());
1 ✔
761
            }
762
          }
1 ✔
763
          syncContext.drain();
1 ✔
764
        }
765
      }
1 ✔
766

767
      // This method is the only reason we have to synchronize field accesses.
768
      /**
769
       * Client requested more messages.
770
       *
771
       * @return whether onReady should be called on the server
772
       */
773
      private boolean serverRequested(int numMessages) {
774
        boolean previouslyReady;
775
        boolean nowReady;
776
        synchronized (this) {
1 ✔
777
          if (closed) {
1 ✔
778
            return false;
1 ✔
779
          }
780
          previouslyReady = serverRequested > 0;
1 ✔
781
          serverRequested += numMessages;
1 ✔
782

783
          while (serverRequested > 0 && !serverReceiveQueue.isEmpty()) {
1 ✔
784
            serverRequested--;
1 ✔
785
            StreamListener.MessageProducer producer = serverReceiveQueue.poll();
1 ✔
786
            syncContext.executeLater(() -> serverStreamListener.messagesAvailable(producer));
1 ✔
787
          }
1 ✔
788

789
          if (serverReceiveQueue.isEmpty() && serverNotifyHalfClose) {
1 ✔
790
            serverNotifyHalfClose = false;
1 ✔
791
            syncContext.executeLater(() -> serverStreamListener.halfClosed());
1 ✔
792
          }
793
          nowReady = serverRequested > 0;
1 ✔
794
        }
1 ✔
795
        syncContext.drain();
1 ✔
796
        return !previouslyReady && nowReady;
1 ✔
797
      }
798

799
      private void serverClosed(Status serverListenerStatus, Status serverTracerStatus) {
800
        internalCancel(serverListenerStatus, serverTracerStatus);
1 ✔
801
      }
1 ✔
802

803
      @Override
804
      public void writeMessage(InputStream message) {
805
        long messageLength = 0;
1 ✔
806
        if (isEnabledSupportTracingMessageSizes) {
1 ✔
807
          try {
808
            if (assumedMessageSize != -1) {
×
809
              messageLength = assumedMessageSize;
×
810
            } else if (message instanceof KnownLength || message instanceof ByteArrayInputStream) {
×
811
              messageLength = message.available();
×
812
            } else {
813
              InputStream oldMessage = message;
×
814
              byte[] payload = ByteStreams.toByteArray(message);
×
815
              messageLength = payload.length;
×
816
              message = new ByteArrayInputStream(payload);
×
817
              oldMessage.close();
×
818
            }
819
          } catch (Exception e) {
×
820
            throw new RuntimeException("Error processing the message length", e);
×
821
          }
×
822
        }
823
        synchronized (this) {
1 ✔
824
          if (closed) {
1 ✔
825
            return;
1 ✔
826
          }
827
          statsTraceCtx.outboundMessage(outboundSeqNo);
1 ✔
828
          statsTraceCtx.outboundMessageSent(outboundSeqNo, -1, -1);
1 ✔
829
          serverStream.statsTraceCtx.inboundMessage(outboundSeqNo);
1 ✔
830
          serverStream.statsTraceCtx.inboundMessageRead(outboundSeqNo, -1, -1);
1 ✔
831
          if (isEnabledSupportTracingMessageSizes) {
1 ✔
832
            statsTraceCtx.outboundUncompressedSize(messageLength);
×
833
            statsTraceCtx.outboundWireSize(messageLength);
×
834
            // messageLength should be same at receiver's end as no actual wire is involved.
835
            serverStream.statsTraceCtx.inboundUncompressedSize(messageLength);
×
836
            serverStream.statsTraceCtx.inboundWireSize(messageLength);
×
837
          }
838
          outboundSeqNo++;
1 ✔
839
          StreamListener.MessageProducer producer = new SingleMessageProducer(message);
1 ✔
840
          if (serverRequested > 0) {
1 ✔
841
            serverRequested--;
1 ✔
842
            syncContext.executeLater(() -> serverStreamListener.messagesAvailable(producer));
1 ✔
843
          } else {
844
            serverReceiveQueue.add(producer);
1 ✔
845
          }
846
        }
1 ✔
847
        syncContext.drain();
1 ✔
848
      }
1 ✔
849

850
      @Override
851
      public void flush() {}
1 ✔
852

853
      @Override
854
      public synchronized boolean isReady() {
855
        if (closed) {
1 ✔
856
          return false;
1 ✔
857
        }
858
        return serverRequested > 0;
1 ✔
859
      }
860

861
      // Must be thread-safe for shutdownNow()
862
      @Override
863
      public void cancel(Status reason) {
864
        Status serverStatus = cleanStatus(reason, includeCauseWithStatus);
1 ✔
865
        if (!internalCancel(serverStatus, serverStatus)) {
1 ✔
866
          return;
1 ✔
867
        }
868
        serverStream.clientCancelled(reason);
1 ✔
869
        streamClosed();
1 ✔
870
      }
1 ✔
871

872
      private boolean internalCancel(
873
          Status serverListenerStatus, Status serverTracerStatus) {
874
        synchronized (this) {
1 ✔
875
          if (closed) {
1 ✔
876
            return false;
1 ✔
877
          }
878
          closed = true;
1 ✔
879

880
          StreamListener.MessageProducer producer;
881
          while ((producer = serverReceiveQueue.poll()) != null) {
1 ✔
882
            InputStream message;
883
            while ((message = producer.next()) != null) {
1 ✔
884
              try {
885
                message.close();
1 ✔
886
              } catch (Throwable t) {
×
887
                log.log(Level.WARNING, "Exception closing stream", t);
×
888
              }
1 ✔
889
            }
890
          }
1 ✔
891
          serverStream.statsTraceCtx.streamClosed(serverTracerStatus);
1 ✔
892
          syncContext.executeLater(() -> serverStreamListener.closed(serverListenerStatus));
1 ✔
893
        }
1 ✔
894
        syncContext.drain();
1 ✔
895
        return true;
1 ✔
896
      }
897

898
      @Override
899
      public void halfClose() {
900
        synchronized (this) {
1 ✔
901
          if (closed) {
1 ✔
902
            return;
1 ✔
903
          }
904
          if (serverReceiveQueue.isEmpty()) {
1 ✔
905
            syncContext.executeLater(() -> serverStreamListener.halfClosed());
1 ✔
906
          } else {
907
            serverNotifyHalfClose = true;
1 ✔
908
          }
909
        }
1 ✔
910
        syncContext.drain();
1 ✔
911
      }
1 ✔
912

913
      @Override
914
      public void setMessageCompression(boolean enable) {}
×
915

916
      @Override
917
      public void setAuthority(String string) {
918
        InProcessStream.this.authority = string;
×
919
      }
×
920

921
      @Override
922
      public void start(ClientStreamListener listener) {
923
        serverStream.setListener(listener);
1 ✔
924

925
        synchronized (InProcessTransport.this) {
1 ✔
926
          statsTraceCtx.clientOutboundHeaders();
1 ✔
927
          streams.add(InProcessTransport.InProcessStream.this);
1 ✔
928
          if (GrpcUtil.shouldBeCountedForInUse(callOptions)) {
1 ✔
929
            inUseState.updateObjectInUse(InProcessTransport.InProcessStream.this, true);
1 ✔
930
          }
931
          serverTransportListener.streamCreated(serverStream, method.getFullMethodName(), headers);
1 ✔
932
        }
1 ✔
933
      }
1 ✔
934

935
      @Override
936
      public Attributes getAttributes() {
937
        return attributes;
1 ✔
938
      }
939

940
      @Override
941
      public void optimizeForDirectExecutor() {}
1 ✔
942

943
      @Override
944
      public void setCompressor(Compressor compressor) {}
1 ✔
945

946
      @Override
947
      public void setFullStreamDecompression(boolean fullStreamDecompression) {}
×
948

949
      @Override
950
      public void setDecompressorRegistry(DecompressorRegistry decompressorRegistry) {}
1 ✔
951

952
      @Override
953
      public void setMaxInboundMessageSize(int maxSize) {}
1 ✔
954

955
      @Override
956
      public void setMaxOutboundMessageSize(int maxSize) {}
1 ✔
957

958
      @Override
959
      public void setDeadline(Deadline deadline) {
960
        headers.discardAll(TIMEOUT_KEY);
1 ✔
961
        headers.put(TIMEOUT_KEY, deadline.timeRemaining(TimeUnit.NANOSECONDS));
1 ✔
962
      }
1 ✔
963

964
      @Override
965
      public void appendTimeoutInsight(InsightBuilder insight) {
966
      }
1 ✔
967
    }
968
  }
969

970
  /**
971
   * Returns a new status with the same code and description.
972
   * If includeCauseWithStatus is true, cause is also included.
973
   *
974
   * <p>For InProcess transport to behave in the same way as the other transports,
975
   * when exchanging statuses between client and server and vice versa,
976
   * the cause should be excluded from the status.
977
   * For easier debugging, the status may be optionally included.
978
   */
979
  private static Status cleanStatus(Status status, boolean includeCauseWithStatus) {
980
    if (status == null) {
1 ✔
981
      return null;
×
982
    }
983
    Status clientStatus = Status
1 ✔
984
        .fromCodeValue(status.getCode().value())
1 ✔
985
        .withDescription(status.getDescription());
1 ✔
986
    if (includeCauseWithStatus) {
1 ✔
987
      clientStatus = clientStatus.withCause(status.getCause());
1 ✔
988
    }
989
    return clientStatus;
1 ✔
990
  }
991

992
  private static class SingleMessageProducer implements StreamListener.MessageProducer {
993
    private InputStream message;
994

995
    private SingleMessageProducer(InputStream message) {
1 ✔
996
      this.message = message;
1 ✔
997
    }
1 ✔
998

999
    @Nullable
1000
    @Override
1001
    public InputStream next() {
1002
      InputStream messageToReturn = message;
1 ✔
1003
      message = null;
1 ✔
1004
      return messageToReturn;
1 ✔
1005
    }
1006
  }
1007
}
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