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

grpc / grpc-java / #20535

16 Oct 2026 01:02PM UTC coverage: 89.464% (+0.04%) from 89.428%
#20535

push

github

web-flow
netty: drain before closing a gracefully shut down server connection (#13086)

Fixes #9566.

After the second GOAWAY of a graceful shutdown (`maxConnectionAge`,
`maxConnectionIdle`, `Server.shutdown()`), `Http2ConnectionHandler`
closes the socket as soon as the last stream's final frame has been
written to the kernel. If the client hasn't read the response yet, the
response is still in the server's send queue. The client's next frame (a
BDP PING or WINDOW_UPDATE) then reaches a socket the server has closed,
so the server's kernel answers with RST and drops the unsent bytes,
trailers included. The client fails an RPC the server completed, with
`UNAVAILABLE: Connection closed after GOAWAY. HTTP/2 error code:
NO_ERROR, debug data: max_age`. The response headers had already
arrived, so the call is committed and isn't retried. The full analysis
and a standalone reproducer are in
https://github.com/grpc/grpc-java/issues/9566#issuecomment-5930328142.

### Change

Once the second GOAWAY has been sent and no streams are active,
`NettyServerHandler` keeps the connection open and reading for up to 1
second before closing. The client normally closes first, as soon as it
has read the GOAWAY and its streams have completed, so the drain usually
ends early. This is the approach Go's HTTP/2 server took
(golang/net@cd69bc3).

- It overrides `isGracefulShutdownComplete()`, the hook
`NettyClientHandler` already uses, so the close still goes through
`Http2ConnectionHandler` and works the same with TLS.
- It never extends the grace time: if `maxConnectionAgeGrace` (or the
shutdown grace) runs out first, the connection closes then.
- The drain is cancelled when the channel becomes inactive.

### Tests

- `NettyServerHandlerTest`: 9 tests asserted that the channel closes
immediately after the second GOAWAY. They now advance the fake clock by
the drain time first. Two new tests:
`maxConnectionAge_drainStartsWhenLastStreamCloses` and
`maxConnectionAgeGrace_shorterThanDrain_clo... (continued)

39401 of 44041 relevant lines covered (89.46%)

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