• 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

93.56
/../netty/src/main/java/io/grpc/netty/NettyClientHandler.java
1
/*
2
 * Copyright 2014 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.netty;
18

19
import static io.netty.handler.codec.http2.DefaultHttp2LocalFlowController.DEFAULT_WINDOW_UPDATE_RATIO;
20
import static io.netty.util.CharsetUtil.UTF_8;
21
import static io.netty.util.internal.ObjectUtil.checkNotNull;
22

23
import com.google.common.annotations.VisibleForTesting;
24
import com.google.common.base.Preconditions;
25
import com.google.common.base.Stopwatch;
26
import com.google.common.base.Supplier;
27
import com.google.common.base.Ticker;
28
import io.grpc.Attributes;
29
import io.grpc.ChannelLogger;
30
import io.grpc.InternalChannelz;
31
import io.grpc.InternalStatus;
32
import io.grpc.Metadata;
33
import io.grpc.MetricRecorder;
34
import io.grpc.Status;
35
import io.grpc.StatusException;
36
import io.grpc.internal.ClientStreamListener.RpcProgress;
37
import io.grpc.internal.ClientTransport.PingCallback;
38
import io.grpc.internal.DisconnectError;
39
import io.grpc.internal.GoAwayDisconnectError;
40
import io.grpc.internal.GrpcAttributes;
41
import io.grpc.internal.GrpcUtil;
42
import io.grpc.internal.Http2Ping;
43
import io.grpc.internal.InUseStateAggregator;
44
import io.grpc.internal.KeepAliveManager;
45
import io.grpc.internal.SimpleDisconnectError;
46
import io.grpc.internal.TransportTracer;
47
import io.grpc.netty.GrpcHttp2HeadersUtils.GrpcHttp2ClientHeadersDecoder;
48
import io.netty.buffer.ByteBuf;
49
import io.netty.buffer.ByteBufUtil;
50
import io.netty.buffer.Unpooled;
51
import io.netty.channel.Channel;
52
import io.netty.channel.ChannelFuture;
53
import io.netty.channel.ChannelFutureListener;
54
import io.netty.channel.ChannelHandlerContext;
55
import io.netty.channel.ChannelPromise;
56
import io.netty.handler.codec.http2.DecoratingHttp2FrameWriter;
57
import io.netty.handler.codec.http2.DefaultHttp2Connection;
58
import io.netty.handler.codec.http2.DefaultHttp2ConnectionDecoder;
59
import io.netty.handler.codec.http2.DefaultHttp2ConnectionEncoder;
60
import io.netty.handler.codec.http2.DefaultHttp2FrameReader;
61
import io.netty.handler.codec.http2.DefaultHttp2FrameWriter;
62
import io.netty.handler.codec.http2.DefaultHttp2HeadersEncoder;
63
import io.netty.handler.codec.http2.DefaultHttp2LocalFlowController;
64
import io.netty.handler.codec.http2.DefaultHttp2RemoteFlowController;
65
import io.netty.handler.codec.http2.Http2CodecUtil;
66
import io.netty.handler.codec.http2.Http2Connection;
67
import io.netty.handler.codec.http2.Http2ConnectionAdapter;
68
import io.netty.handler.codec.http2.Http2ConnectionDecoder;
69
import io.netty.handler.codec.http2.Http2ConnectionEncoder;
70
import io.netty.handler.codec.http2.Http2Error;
71
import io.netty.handler.codec.http2.Http2Exception;
72
import io.netty.handler.codec.http2.Http2FrameAdapter;
73
import io.netty.handler.codec.http2.Http2FrameLogger;
74
import io.netty.handler.codec.http2.Http2FrameReader;
75
import io.netty.handler.codec.http2.Http2FrameWriter;
76
import io.netty.handler.codec.http2.Http2Headers;
77
import io.netty.handler.codec.http2.Http2HeadersDecoder;
78
import io.netty.handler.codec.http2.Http2HeadersEncoder;
79
import io.netty.handler.codec.http2.Http2HeadersEncoder.SensitivityDetector;
80
import io.netty.handler.codec.http2.Http2InboundFrameLogger;
81
import io.netty.handler.codec.http2.Http2OutboundFrameLogger;
82
import io.netty.handler.codec.http2.Http2Settings;
83
import io.netty.handler.codec.http2.Http2Stream;
84
import io.netty.handler.codec.http2.Http2StreamVisitor;
85
import io.netty.handler.codec.http2.StreamBufferingEncoder;
86
import io.netty.handler.codec.http2.UniformStreamByteDistributor;
87
import io.netty.handler.logging.LogLevel;
88
import io.netty.util.AsciiString;
89
import io.perfmark.PerfMark;
90
import io.perfmark.Tag;
91
import io.perfmark.TaskCloseable;
92
import java.nio.channels.ClosedChannelException;
93
import java.util.LinkedHashMap;
94
import java.util.Map;
95
import java.util.Set;
96
import java.util.concurrent.Executor;
97
import java.util.logging.Level;
98
import java.util.logging.Logger;
99
import javax.annotation.Nullable;
100

101
/**
102
 * Client-side Netty handler for GRPC processing. All event handlers are executed entirely within
103
 * the context of the Netty Channel thread.
104
 */
105
class NettyClientHandler extends AbstractNettyHandler {
106
  private static final Logger logger = Logger.getLogger(NettyClientHandler.class.getName());
1 ✔
107
  static boolean enablePerRpcAuthorityCheck =
1 ✔
108
      GrpcUtil.getFlag("GRPC_ENABLE_PER_RPC_AUTHORITY_CHECK", false);
1 ✔
109

110
  /**
111
   * A message that simply passes through the channel without any real processing. It is useful to
112
   * check if buffers have been drained and test the health of the channel in a single operation.
113
   */
114
  static final Object NOOP_MESSAGE = new Object();
1 ✔
115

116
  /**
117
   * Status used when the transport has exhausted the number of streams.
118
   */
119
  private static final Status EXHAUSTED_STREAMS_STATUS =
1 ✔
120
          Status.UNAVAILABLE.withDescription("Stream IDs have been exhausted");
1 ✔
121
  private static final long USER_PING_PAYLOAD = 1111;
122

123
  private final Http2Connection.PropertyKey streamKey;
124
  private final ClientTransportLifecycleManager lifecycleManager;
125
  private final KeepAliveManager keepAliveManager;
126
  // Returns new unstarted stopwatches
127
  private final Supplier<Stopwatch> stopwatchFactory;
128
  private final TransportTracer transportTracer;
129
  private final Attributes eagAttributes;
130
  private final TcpMetrics tcpMetrics;
131
  private final String authority;
132
  private final InUseStateAggregator<Http2Stream> inUseState =
1 ✔
133
      new InUseStateAggregator<Http2Stream>() {
1 ✔
134
        @Override
135
        protected void handleInUse() {
136
          lifecycleManager.notifyInUse(true);
1 ✔
137
        }
1 ✔
138

139
        @Override
140
        protected void handleNotInUse() {
141
          lifecycleManager.notifyInUse(false);
1 ✔
142
        }
1 ✔
143
      };
144
  private final Map<String, Status> peerVerificationResults =
1 ✔
145
      new LinkedHashMap<String, Status>() {
1 ✔
146
        @Override
147
        protected boolean removeEldestEntry(Map.Entry<String, Status> eldest) {
148
          return size() > 100;
1 ✔
149
        }
150
      };
151

152
  private WriteQueue clientWriteQueue;
153
  private Http2Ping ping;
154
  private Attributes attributes;
155
  private InternalChannelz.Security securityInfo;
156
  private Status abruptGoAwayStatus;
157
  private Status channelInactiveReason;
158

159
  static NettyClientHandler newHandler(
160
      ClientTransportLifecycleManager lifecycleManager,
161
      @Nullable KeepAliveManager keepAliveManager,
162
      boolean autoFlowControl,
163
      int flowControlWindow,
164
      Set<AsciiString> neverIndexedMetadataKeys,
165
      int maxHeaderListSize,
166
      int softLimitHeaderListSize,
167
      Supplier<Stopwatch> stopwatchFactory,
168
      Runnable tooManyPingsRunnable,
169
      TransportTracer transportTracer,
170
      Attributes eagAttributes,
171
      String authority,
172
      ChannelLogger negotiationLogger,
173
      Ticker ticker,
174
      MetricRecorder metricRecorder) {
175
    Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive");
1 ✔
176
    Http2HeadersDecoder headersDecoder = new GrpcHttp2ClientHeadersDecoder(maxHeaderListSize);
1 ✔
177
    Http2FrameReader frameReader = new DefaultHttp2FrameReader(headersDecoder);
1 ✔
178
    Http2HeadersEncoder encoder = new DefaultHttp2HeadersEncoder(
1 ✔
179
        sensitivityDetector(neverIndexedMetadataKeys), false, 64, Integer.MAX_VALUE);
1 ✔
180
    Http2FrameWriter frameWriter = new DefaultHttp2FrameWriter(encoder);
1 ✔
181
    Http2Connection connection = new DefaultHttp2Connection(false);
1 ✔
182
    UniformStreamByteDistributor dist = new UniformStreamByteDistributor(connection);
1 ✔
183
    dist.minAllocationChunk(MIN_ALLOCATED_CHUNK); // Increased for benchmarks performance.
1 ✔
184
    DefaultHttp2RemoteFlowController controller =
1 ✔
185
        new DefaultHttp2RemoteFlowController(connection, dist);
186
    connection.remote().flowController(controller);
1 ✔
187

188
    return newHandler(
1 ✔
189
        connection,
190
        frameReader,
191
        frameWriter,
192
        lifecycleManager,
193
        keepAliveManager,
194
        autoFlowControl,
195
        flowControlWindow,
196
        maxHeaderListSize,
197
        softLimitHeaderListSize,
198
        stopwatchFactory,
199
        tooManyPingsRunnable,
200
        transportTracer,
201
        eagAttributes,
202
        authority,
203
        negotiationLogger,
204
        ticker,
205
        metricRecorder);
206
  }
207

208
  @VisibleForTesting
209
  static NettyClientHandler newHandler(
210
      final Http2Connection connection,
211
      Http2FrameReader frameReader,
212
      Http2FrameWriter frameWriter,
213
      ClientTransportLifecycleManager lifecycleManager,
214
      KeepAliveManager keepAliveManager,
215
      boolean autoFlowControl,
216
      int flowControlWindow,
217
      int maxHeaderListSize,
218
      int softLimitHeaderListSize,
219
      Supplier<Stopwatch> stopwatchFactory,
220
      Runnable tooManyPingsRunnable,
221
      TransportTracer transportTracer,
222
      Attributes eagAttributes,
223
      String authority,
224
      ChannelLogger negotiationLogger,
225
      Ticker ticker,
226
      MetricRecorder metricRecorder) {
227
    Preconditions.checkNotNull(connection, "connection");
1 ✔
228
    Preconditions.checkNotNull(frameReader, "frameReader");
1 ✔
229
    Preconditions.checkNotNull(lifecycleManager, "lifecycleManager");
1 ✔
230
    Preconditions.checkArgument(flowControlWindow > 0, "flowControlWindow must be positive");
1 ✔
231
    Preconditions.checkArgument(maxHeaderListSize > 0, "maxHeaderListSize must be positive");
1 ✔
232
    Preconditions.checkArgument(softLimitHeaderListSize > 0,
1 ✔
233
        "softLimitHeaderListSize must be positive");
234
    Preconditions.checkNotNull(stopwatchFactory, "stopwatchFactory");
1 ✔
235
    Preconditions.checkNotNull(tooManyPingsRunnable, "tooManyPingsRunnable");
1 ✔
236
    Preconditions.checkNotNull(eagAttributes, "eagAttributes");
1 ✔
237
    Preconditions.checkNotNull(authority, "authority");
1 ✔
238

239
    Http2FrameLogger frameLogger = new Http2FrameLogger(LogLevel.DEBUG, NettyClientHandler.class);
1 ✔
240
    frameReader = new Http2InboundFrameLogger(frameReader, frameLogger);
1 ✔
241
    frameWriter = new Http2OutboundFrameLogger(frameWriter, frameLogger);
1 ✔
242

243
    PingCountingFrameWriter pingCounter;
244
    frameWriter = pingCounter = new PingCountingFrameWriter(frameWriter);
1 ✔
245

246
    StreamBufferingEncoder encoder =
1 ✔
247
        new StreamBufferingEncoder(
248
            new DefaultHttp2ConnectionEncoder(connection, frameWriter));
249

250
    // Create the local flow controller configured to auto-refill the connection window.
251
    connection.local().flowController(
1 ✔
252
        new DefaultHttp2LocalFlowController(connection, DEFAULT_WINDOW_UPDATE_RATIO, true));
253

254
    Http2ConnectionDecoder decoder = new DefaultHttp2ConnectionDecoder(connection, encoder,
1 ✔
255
        frameReader);
256

257
    transportTracer.setFlowControlWindowReader(new Utils.FlowControlReader(connection));
1 ✔
258

259
    Http2Settings settings = new Http2Settings();
1 ✔
260
    settings.pushEnabled(false);
1 ✔
261
    settings.initialWindowSize(flowControlWindow);
1 ✔
262
    settings.maxConcurrentStreams(0);
1 ✔
263
    settings.maxHeaderListSize(maxHeaderListSize);
1 ✔
264

265
    return new NettyClientHandler(
1 ✔
266
        decoder,
267
        encoder,
268
        settings,
269
        negotiationLogger,
270
        lifecycleManager,
271
        keepAliveManager,
272
        stopwatchFactory,
273
        tooManyPingsRunnable,
274
        transportTracer,
275
        eagAttributes,
276
        authority,
277
        autoFlowControl,
278
        pingCounter,
279
        ticker,
280
        maxHeaderListSize,
281
        softLimitHeaderListSize,
282
        metricRecorder);
283
  }
284

285
  @VisibleForTesting
286
  static SensitivityDetector sensitivityDetector(
287
      final Set<AsciiString> neverIndexedMetadataKeys) {
288
    if (neverIndexedMetadataKeys.isEmpty()) {
1 ✔
289
      return Http2HeadersEncoder.NEVER_SENSITIVE;
1 ✔
290
    }
291
    return new SensitivityDetector() {
1 ✔
292
      @Override
293
      public boolean isSensitive(CharSequence name, CharSequence value) {
294
        return neverIndexedMetadataKeys.contains(AsciiString.of(name));
1 ✔
295
      }
296
    };
297
  }
298

299
  private NettyClientHandler(
300
      Http2ConnectionDecoder decoder,
301
      Http2ConnectionEncoder encoder,
302
      Http2Settings settings,
303
      ChannelLogger negotiationLogger,
304
      ClientTransportLifecycleManager lifecycleManager,
305
      KeepAliveManager keepAliveManager,
306
      Supplier<Stopwatch> stopwatchFactory,
307
      final Runnable tooManyPingsRunnable,
308
      TransportTracer transportTracer,
309
      Attributes eagAttributes,
310
      String authority,
311
      boolean autoFlowControl,
312
      PingLimiter pingLimiter,
313
      Ticker ticker,
314
      int maxHeaderListSize,
315
      int softLimitHeaderListSize,
316
      MetricRecorder metricRecorder) {
317
    super(
1 ✔
318
        /* channelUnused= */ null,
319
        decoder,
320
        encoder,
321
        settings,
322
        negotiationLogger,
323
        autoFlowControl,
324
        pingLimiter,
325
        ticker,
326
        maxHeaderListSize,
327
        softLimitHeaderListSize);
328
    this.lifecycleManager = lifecycleManager;
1 ✔
329
    this.keepAliveManager = keepAliveManager;
1 ✔
330
    this.stopwatchFactory = stopwatchFactory;
1 ✔
331
    this.transportTracer = Preconditions.checkNotNull(transportTracer);
1 ✔
332
    this.eagAttributes = eagAttributes;
1 ✔
333
    this.authority = authority;
1 ✔
334
    this.attributes = Attributes.newBuilder()
1 ✔
335
        .set(GrpcAttributes.ATTR_CLIENT_EAG_ATTRS, eagAttributes).build();
1 ✔
336
    this.tcpMetrics = new TcpMetrics(metricRecorder);
1 ✔
337

338
    // Set the frame listener on the decoder.
339
    decoder().frameListener(new FrameListener());
1 ✔
340

341
    Http2Connection connection = encoder.connection();
1 ✔
342
    streamKey = connection.newKey();
1 ✔
343

344
    connection.addListener(new Http2ConnectionAdapter() {
1 ✔
345
      @Override
346
      public void onGoAwayReceived(int lastStreamId, long errorCode, ByteBuf debugData) {
347
        byte[] debugDataBytes = ByteBufUtil.getBytes(debugData);
1 ✔
348
        goingAway(errorCode, debugDataBytes);
1 ✔
349
        if (errorCode == Http2Error.ENHANCE_YOUR_CALM.code()) {
1 ✔
350
          String data = new String(debugDataBytes, UTF_8);
1 ✔
351
          logger.log(
1 ✔
352
              Level.WARNING, "Received GOAWAY with ENHANCE_YOUR_CALM. Debug data: {0}", data);
353
          if ("too_many_pings".equals(data)) {
1 ✔
354
            tooManyPingsRunnable.run();
1 ✔
355
          }
356
        }
357
      }
1 ✔
358

359
      @Override
360
      public void onStreamActive(Http2Stream stream) {
361
        if (connection().numActiveStreams() == 1
1 ✔
362
            && NettyClientHandler.this.keepAliveManager != null) {
1 ✔
363
          NettyClientHandler.this.keepAliveManager.onTransportActive();
1 ✔
364
        }
365
      }
1 ✔
366

367
      @Override
368
      public void onStreamClosed(Http2Stream stream) {
369
        // Although streams with CALL_OPTIONS_RPC_OWNED_BY_BALANCER are not marked as "in-use" in
370
        // the first place, we don't propagate that option here, and it's safe to reset the in-use
371
        // state for them, which will be a cheap no-op.
372
        inUseState.updateObjectInUse(stream, false);
1 ✔
373
        if (connection().numActiveStreams() == 0
1 ✔
374
            && NettyClientHandler.this.keepAliveManager != null) {
1 ✔
375
          NettyClientHandler.this.keepAliveManager.onTransportIdle();
1 ✔
376
        }
377
      }
1 ✔
378
    });
379
  }
1 ✔
380

381
  /**
382
   * The protocol negotiation attributes, available once the protocol negotiation completes;
383
   * otherwise returns {@code Attributes.EMPTY}.
384
   */
385
  Attributes getAttributes() {
386
    return attributes;
1 ✔
387
  }
388

389
  /**
390
   * Handler for commands sent from the stream.
391
   */
392
  @Override
393
  public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise)
394
          throws Exception {
395
    if (msg instanceof CreateStreamCommand) {
1 ✔
396
      createStream((CreateStreamCommand) msg, promise);
1 ✔
397
    } else if (msg instanceof SendGrpcFrameCommand) {
1 ✔
398
      sendGrpcFrame(ctx, (SendGrpcFrameCommand) msg, promise);
1 ✔
399
    } else if (msg instanceof CancelClientStreamCommand) {
1 ✔
400
      cancelStream(ctx, (CancelClientStreamCommand) msg, promise);
1 ✔
401
    } else if (msg instanceof SendPingCommand) {
1 ✔
402
      sendPingFrame(ctx, (SendPingCommand) msg, promise);
1 ✔
403
    } else if (msg instanceof GracefulCloseCommand) {
1 ✔
404
      gracefulClose(ctx, (GracefulCloseCommand) msg, promise);
1 ✔
405
    } else if (msg instanceof ForcefulCloseCommand) {
1 ✔
406
      forcefulClose(ctx, (ForcefulCloseCommand) msg, promise);
1 ✔
407
    } else if (msg == NOOP_MESSAGE) {
1 ✔
408
      ctx.write(Unpooled.EMPTY_BUFFER, promise);
1 ✔
409
    } else {
410
      throw new AssertionError("Write called for unexpected type: " + msg.getClass().getName());
×
411
    }
412
  }
1 ✔
413

414
  void startWriteQueue(Channel channel) {
415
    clientWriteQueue = new WriteQueue(channel);
1 ✔
416
  }
1 ✔
417

418
  WriteQueue getWriteQueue() {
419
    return clientWriteQueue;
1 ✔
420
  }
421

422
  ClientTransportLifecycleManager getLifecycleManager() {
423
    return lifecycleManager;
1 ✔
424
  }
425

426
  /**
427
   * Returns the given processed bytes back to inbound flow control.
428
   */
429
  void returnProcessedBytes(Http2Stream stream, int bytes) {
430
    try {
431
      decoder().flowController().consumeBytes(stream, bytes);
1 ✔
432
    } catch (Http2Exception e) {
×
433
      throw new RuntimeException(e);
×
434
    }
1 ✔
435
  }
1 ✔
436

437
  private void onHeadersRead(int streamId, Http2Headers headers, boolean endStream) {
438
    if (keepAliveManager != null) {
1 ✔
439
      keepAliveManager.onDataReceived();
1 ✔
440
    }
441
    // Stream 1 is reserved for the Upgrade response, so we should ignore its headers here:
442
    if (streamId != Http2CodecUtil.HTTP_UPGRADE_STREAM_ID) {
1 ✔
443
      NettyClientStream.TransportState stream = clientStream(requireHttp2Stream(streamId));
1 ✔
444
      if (stream == null) {
1 ✔
445
        return;
1 ✔
446
      }
447
      PerfMark.event("NettyClientHandler.onHeadersRead", stream.tag());
1 ✔
448
      // check metadata size vs soft limit
449
      int h2HeadersSize = Utils.getH2HeadersSize(headers);
1 ✔
450
      boolean shouldFail =
1 ✔
451
          Utils.shouldRejectOnMetadataSizeSoftLimitExceeded(
1 ✔
452
              h2HeadersSize, softLimitHeaderListSize, maxHeaderListSize);
453
      if (shouldFail && endStream) {
1 ✔
454
        stream.transportReportStatus(Status.RESOURCE_EXHAUSTED
×
455
            .withDescription(
×
456
                String.format(
×
457
                    "Server Status + Trailers of size %d exceeded Metadata size soft limit: %d",
458
                    h2HeadersSize,
×
459
                    softLimitHeaderListSize)), true, new Metadata());
×
460
        return;
×
461
      } else if (shouldFail) {
1 ✔
462
        stream.transportReportStatus(Status.RESOURCE_EXHAUSTED
1 ✔
463
            .withDescription(
1 ✔
464
                String.format(
1 ✔
465
                    "Server Headers of size %d exceeded Metadata size soft limit: %d",
466
                    h2HeadersSize,
1 ✔
467
                    softLimitHeaderListSize)), true, new Metadata());
1 ✔
468
        return;
1 ✔
469
      }
470
      stream.transportHeadersReceived(headers, endStream);
1 ✔
471
    }
472
  }
1 ✔
473

474
  /**
475
   * Handler for an inbound HTTP/2 DATA frame.
476
   */
477
  private int onDataRead(int streamId, ByteBuf data, int padding, boolean endOfStream) {
478
    flowControlPing().onDataRead(data.readableBytes(), padding);
1 ✔
479
    if (keepAliveManager != null) {
1 ✔
480
      keepAliveManager.onDataReceived();
1 ✔
481
    }
482
    NettyClientStream.TransportState stream = clientStream(requireHttp2Stream(streamId));
1 ✔
483
    if (stream == null) {
1 ✔
484
      // No deframer will consume this payload, so return its flow-control credit immediately.
485
      return data.readableBytes() + padding;
1 ✔
486
    }
487
    PerfMark.event("NettyClientHandler.onDataRead", stream.tag());
1 ✔
488
    stream.transportDataReceived(data, endOfStream);
1 ✔
489
    return padding;
1 ✔
490
  }
491

492
  /**
493
   * Handler for an inbound HTTP/2 RST_STREAM frame, terminating a stream.
494
   */
495
  private void onRstStreamRead(int streamId, long errorCode) {
496
    NettyClientStream.TransportState stream = clientStream(connection().stream(streamId));
1 ✔
497
    if (stream != null) {
1 ✔
498
      PerfMark.event("NettyClientHandler.onRstStreamRead", stream.tag());
1 ✔
499
      Status status = statusFromH2Error(null, "RST_STREAM closed stream", errorCode, null);
1 ✔
500
      stream.transportReportStatus(
1 ✔
501
          status,
502
          errorCode == Http2Error.REFUSED_STREAM.code()
1 ✔
503
              ? RpcProgress.REFUSED : RpcProgress.PROCESSED,
1 ✔
504
          false /*stop delivery*/,
505
          new Metadata());
506
      if (keepAliveManager != null) {
1 ✔
507
        keepAliveManager.onDataReceived();
×
508
      }
509
    }
510
  }
1 ✔
511

512
  @Override
513
  public void close(ChannelHandlerContext ctx, ChannelPromise promise) throws Exception {
514
    tcpMetrics.recordTcpInfo(ctx.channel());
1 ✔
515
    logger.fine("Network channel being closed by the application.");
1 ✔
516
    if (ctx.channel().isActive()) { // Ignore notification that the socket was closed
1 ✔
517
      lifecycleManager.notifyShutdown(
1 ✔
518
          Status.UNAVAILABLE.withDescription("Transport closed for unknown reason"),
1 ✔
519
          SimpleDisconnectError.UNKNOWN);
520
    }
521
    super.close(ctx, promise);
1 ✔
522
  }
1 ✔
523

524
  /**
525
   * Handler for the Channel shutting down.
526
   */
527
  @Override
528
  public void channelActive(ChannelHandlerContext ctx) throws Exception {
529
    tcpMetrics.channelActive(ctx.channel());
1 ✔
530
    super.channelActive(ctx);
1 ✔
531
  }
1 ✔
532

533
  @Override
534
  public void channelInactive(ChannelHandlerContext ctx) throws Exception {
535
    try {
536
      logger.fine("Network channel is closed");
1 ✔
537
      tcpMetrics.channelInactive(ctx.channel());
1 ✔
538
      Status status = Status.UNAVAILABLE.withDescription("Network closed for unknown reason");
1 ✔
539
      lifecycleManager.notifyShutdown(status, SimpleDisconnectError.UNKNOWN);
1 ✔
540
      final Status streamStatus;
541
      if (channelInactiveReason != null) {
1 ✔
542
        streamStatus = channelInactiveReason;
1 ✔
543
      } else {
544
        streamStatus = lifecycleManager.getShutdownStatus();
1 ✔
545
      }
546
      try {
547
        cancelPing(lifecycleManager.getShutdownStatus());
1 ✔
548
        // Report status to the application layer for any open streams
549
        connection().forEachActiveStream(new Http2StreamVisitor() {
1 ✔
550
          @Override
551
          public boolean visit(Http2Stream stream) throws Http2Exception {
552
            NettyClientStream.TransportState clientStream = clientStream(stream);
1 ✔
553
            if (clientStream != null) {
1 ✔
554
              clientStream.transportReportStatus(streamStatus, false, new Metadata());
1 ✔
555
            }
556
            return true;
1 ✔
557
          }
558
        });
559
      } finally {
560
        lifecycleManager.notifyTerminated(status, SimpleDisconnectError.UNKNOWN);
1 ✔
561
      }
562
    } finally {
563
      // Close any open streams
564
      super.channelInactive(ctx);
1 ✔
565
      if (keepAliveManager != null) {
1 ✔
566
        keepAliveManager.onTransportTermination();
1 ✔
567
      }
568
    }
569
  }
1 ✔
570

571
  @Override
572
  public void handleProtocolNegotiationCompleted(
573
      Attributes attributes, InternalChannelz.Security securityInfo) {
574
    this.attributes = this.attributes.toBuilder().setAll(attributes).build();
1 ✔
575
    this.securityInfo = securityInfo;
1 ✔
576
    super.handleProtocolNegotiationCompleted(attributes, securityInfo);
1 ✔
577
    writeBufferingAndRemove(ctx().channel());
1 ✔
578
  }
1 ✔
579

580
  static void writeBufferingAndRemove(Channel channel) {
581
    checkNotNull(channel, "channel");
1 ✔
582
    ChannelHandlerContext handlerCtx =
1 ✔
583
        channel.pipeline().context(WriteBufferingAndExceptionHandler.class);
1 ✔
584
    if (handlerCtx == null) {
1 ✔
585
      return;
1 ✔
586
    }
587
    ((WriteBufferingAndExceptionHandler) handlerCtx.handler()).writeBufferedAndRemove(handlerCtx);
1 ✔
588
  }
1 ✔
589

590
  @Override
591
  public Attributes getEagAttributes() {
592
    return eagAttributes;
1 ✔
593
  }
594

595
  @Override
596
  public String getAuthority() {
597
    return authority;
1 ✔
598
  }
599

600
  InternalChannelz.Security getSecurityInfo() {
601
    return securityInfo;
1 ✔
602
  }
603

604
  @Override
605
  protected void onConnectionError(ChannelHandlerContext ctx,  boolean outbound, Throwable cause,
606
      Http2Exception http2Ex) {
607
    logger.log(Level.FINE, "Caught a connection error", cause);
1 ✔
608
    lifecycleManager.notifyShutdown(Utils.statusFromThrowable(cause),
1 ✔
609
        SimpleDisconnectError.SOCKET_ERROR);
610
    // Parent class will shut down the Channel
611
    super.onConnectionError(ctx, outbound, cause, http2Ex);
1 ✔
612
  }
1 ✔
613

614
  @Override
615
  protected void onStreamError(ChannelHandlerContext ctx, boolean outbound, Throwable cause,
616
      Http2Exception.StreamException http2Ex) {
617
    // Close the stream with a status that contains the cause.
618
    NettyClientStream.TransportState stream = clientStream(connection().stream(http2Ex.streamId()));
1 ✔
619
    if (stream != null) {
1 ✔
620
      stream.transportReportStatus(Utils.statusFromThrowable(cause), false, new Metadata());
1 ✔
621
    } else {
622
      logger.log(Level.FINE, "Stream error for unknown stream " + http2Ex.streamId(), cause);
1 ✔
623
    }
624

625
    // Delegate to the base class to send a RST_STREAM.
626
    super.onStreamError(ctx, outbound, cause, http2Ex);
1 ✔
627
  }
1 ✔
628

629
  @Override
630
  protected boolean isGracefulShutdownComplete() {
631
    // Only allow graceful shutdown to complete after all pending streams have completed.
632
    return super.isGracefulShutdownComplete()
1 ✔
633
        && ((StreamBufferingEncoder) encoder()).numBufferedStreams() == 0;
1 ✔
634
  }
635

636
  /**
637
   * Attempts to create a new stream from the given command. If there are too many active streams,
638
   * the creation request is queued.
639
   */
640
  private void createStream(CreateStreamCommand command, ChannelPromise promise)
641
          throws Exception {
642
    if (lifecycleManager.getShutdownStatus() != null) {
1 ✔
643
      command.stream().setNonExistent();
1 ✔
644
      // The connection is going away (it is really the GOAWAY case),
645
      // just terminate the stream now.
646
      command.stream().transportReportStatus(
1 ✔
647
          lifecycleManager.getShutdownStatus(), RpcProgress.MISCARRIED, true, new Metadata());
1 ✔
648
      promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace(
1 ✔
649
              lifecycleManager.getShutdownStatus(), null));
1 ✔
650
      return;
1 ✔
651
    }
652

653
    CharSequence authorityHeader = command.headers().authority();
1 ✔
654
    if (authorityHeader == null) {
1 ✔
655
      Status authorityVerificationStatus = Status.UNAVAILABLE.withDescription(
1 ✔
656
              "Missing authority header");
657
      command.stream().setNonExistent();
1 ✔
658
      command.stream().transportReportStatus(
1 ✔
659
              Status.UNAVAILABLE, RpcProgress.PROCESSED, true, new Metadata());
660
      promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace(
1 ✔
661
              authorityVerificationStatus, null));
662
      return;
1 ✔
663
    }
664
    // No need to verify authority for the rpc outgoing header if it is same as the authority
665
    // for the transport
666
    if (!authority.contentEquals(authorityHeader)) {
1 ✔
667
      Status authorityVerificationStatus = peerVerificationResults.get(
1 ✔
668
              authorityHeader.toString());
1 ✔
669
      if (authorityVerificationStatus == null) {
1 ✔
670
        if (attributes.get(GrpcAttributes.ATTR_AUTHORITY_VERIFIER) == null) {
1 ✔
671
          authorityVerificationStatus = Status.UNAVAILABLE.withDescription(
1 ✔
672
                  "Authority verifier not found to verify authority");
673
          command.stream().setNonExistent();
1 ✔
674
          command.stream().transportReportStatus(
1 ✔
675
                  authorityVerificationStatus, RpcProgress.PROCESSED, true, new Metadata());
676
          promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace(
1 ✔
677
                  authorityVerificationStatus, null));
678
          return;
1 ✔
679
        }
680
        authorityVerificationStatus = attributes.get(GrpcAttributes.ATTR_AUTHORITY_VERIFIER)
1 ✔
681
                .verifyAuthority(authorityHeader.toString());
1 ✔
682
        peerVerificationResults.put(authorityHeader.toString(), authorityVerificationStatus);
1 ✔
683
        if (!authorityVerificationStatus.isOk() && !enablePerRpcAuthorityCheck) {
1 ✔
684
          logger.log(Level.WARNING, String.format("%s.%s",
1 ✔
685
                          authorityVerificationStatus.getDescription(),
1 ✔
686
                          enablePerRpcAuthorityCheck
1 ✔
687
                                  ? "" : " This will be an error in the future."),
1 ✔
688
                  InternalStatus.asRuntimeExceptionWithoutStacktrace(
1 ✔
689
                          authorityVerificationStatus, null));
690
        }
691
      }
692
      if (!authorityVerificationStatus.isOk()) {
1 ✔
693
        if (enablePerRpcAuthorityCheck) {
1 ✔
694
          command.stream().setNonExistent();
1 ✔
695
          command.stream().transportReportStatus(
1 ✔
696
                  authorityVerificationStatus, RpcProgress.PROCESSED, true, new Metadata());
697
          promise.setFailure(InternalStatus.asRuntimeExceptionWithoutStacktrace(
1 ✔
698
                  authorityVerificationStatus, null));
699
          return;
1 ✔
700
        }
701
      }
702
    }
703
    // Get the stream ID for the new stream.
704
    int streamId;
705
    try {
706
      streamId = incrementAndGetNextStreamId();
1 ✔
707
    } catch (StatusException e) {
1 ✔
708
      command.stream().setNonExistent();
1 ✔
709
      // Stream IDs have been exhausted for this connection. Fail the promise immediately.
710
      promise.setFailure(e);
1 ✔
711

712
      // Initiate a graceful shutdown if we haven't already.
713
      if (!connection().goAwaySent()) {
1 ✔
714
        logger.fine("Stream IDs have been exhausted for this connection. "
1 ✔
715
                + "Initiating graceful shutdown of the connection.");
716
        lifecycleManager.notifyShutdown(e.getStatus(), SimpleDisconnectError.UNKNOWN);
1 ✔
717
        close(ctx(), ctx().newPromise());
1 ✔
718
      }
719
      return;
1 ✔
720
    }
1 ✔
721
    if (connection().goAwayReceived()) {
1 ✔
722
      Status s = abruptGoAwayStatus;
1 ✔
723
      int maxActiveStreams = connection().local().maxActiveStreams();
1 ✔
724
      int lastStreamId = connection().local().lastStreamKnownByPeer();
1 ✔
725
      if (s == null) {
1 ✔
726
        // Should be impossible, but handle pseudo-gracefully
727
        s = Status.INTERNAL.withDescription(
×
728
            "Failed due to abrupt GOAWAY, but can't find GOAWAY details");
729
      } else if (streamId > lastStreamId) {
1 ✔
730
        s = s.augmentDescription(
1 ✔
731
            "stream id: " + streamId + ", GOAWAY Last-Stream-ID:" + lastStreamId);
732
      } else if (connection().local().numActiveStreams() == maxActiveStreams) {
1 ✔
733
        s = s.augmentDescription("At MAX_CONCURRENT_STREAMS limit. limit: " + maxActiveStreams);
1 ✔
734
      }
735
      if (streamId > lastStreamId || connection().local().numActiveStreams() == maxActiveStreams) {
1 ✔
736
        // This should only be reachable during onGoAwayReceived, as otherwise
737
        // getShutdownThrowable() != null
738
        command.stream().setNonExistent();
1 ✔
739
        command.stream().transportReportStatus(s, RpcProgress.MISCARRIED, true, new Metadata());
1 ✔
740
        promise.setFailure(s.asRuntimeException());
1 ✔
741
        return;
1 ✔
742
      }
743
    }
744

745
    NettyClientStream.TransportState stream = command.stream();
1 ✔
746
    Http2Headers headers = command.headers();
1 ✔
747
    stream.setId(streamId);
1 ✔
748

749
    try (TaskCloseable ignore = PerfMark.traceTask("NettyClientHandler.createStream")) {
1 ✔
750
      PerfMark.linkIn(command.getLink());
1 ✔
751
      PerfMark.attachTag(stream.tag());
1 ✔
752
      createStreamTraced(
1 ✔
753
          streamId, stream, headers, command.isGet(), command.shouldBeCountedForInUse(), promise);
1 ✔
754
    }
755
  }
1 ✔
756

757
  private void createStreamTraced(
758
      final int streamId,
759
      final NettyClientStream.TransportState stream,
760
      final Http2Headers headers,
761
      boolean isGet,
762
      final boolean shouldBeCountedForInUse,
763
      final ChannelPromise promise) {
764
    // Create an intermediate promise so that we can intercept the failure reported back to the
765
    // application.
766
    ChannelPromise tempPromise = ctx().newPromise();
1 ✔
767
    encoder().writeHeaders(ctx(), streamId, headers, 0, isGet, tempPromise)
1 ✔
768
        .addListener(new ChannelFutureListener() {
1 ✔
769
          @Override
770
          public void operationComplete(ChannelFuture future) throws Exception {
771
            if (future.isSuccess()) {
1 ✔
772
              // The http2Stream will be null in case a stream buffered in the encoder
773
              // was canceled via RST_STREAM.
774
              Http2Stream http2Stream = connection().stream(streamId);
1 ✔
775
              if (http2Stream != null) {
1 ✔
776
                stream.getStatsTraceContext().clientOutboundHeaders();
1 ✔
777
                http2Stream.setProperty(streamKey, stream);
1 ✔
778

779
                // This delays the in-use state until the I/O completes, which technically may
780
                // be later than we would like.
781
                if (shouldBeCountedForInUse) {
1 ✔
782
                  inUseState.updateObjectInUse(http2Stream, true);
1 ✔
783
                }
784

785
                // Attach the client stream to the HTTP/2 stream object as user data.
786
                stream.setHttp2Stream(http2Stream);
1 ✔
787
                promise.setSuccess();
1 ✔
788
              } else {
789
                // Otherwise, the stream has been cancelled and Netty is sending a
790
                // RST_STREAM frame which causes it to purge pending writes from the
791
                // flow-controller and delete the http2Stream. The stream listener has already
792
                // been notified of cancellation so there is nothing to do.
793
                //
794
                // This process has been observed to fail in some circumstances, leaving listeners
795
                // unanswered. Ensure that some exception has been delivered consistent with the
796
                // implied RST_STREAM result above.
797
                Status status = Status.INTERNAL.withDescription("unknown stream for connection");
1 ✔
798
                promise.setFailure(status.asRuntimeException());
1 ✔
799
              }
800
            } else {
1 ✔
801
              Throwable cause = future.cause();
1 ✔
802
              if (cause instanceof StreamBufferingEncoder.Http2GoAwayException) {
1 ✔
803
                StreamBufferingEncoder.Http2GoAwayException e =
1 ✔
804
                    (StreamBufferingEncoder.Http2GoAwayException) cause;
805
                Status status = statusFromH2Error(
1 ✔
806
                    Status.Code.UNAVAILABLE, "GOAWAY closed buffered stream",
807
                    e.errorCode(), e.debugData());
1 ✔
808
                cause = status.asRuntimeException();
1 ✔
809
                stream.transportReportStatus(status, RpcProgress.MISCARRIED, true, new Metadata());
1 ✔
810
              } else if (cause instanceof StreamBufferingEncoder.Http2ChannelClosedException) {
1 ✔
811
                Status status = lifecycleManager.getShutdownStatus();
1 ✔
812
                if (status == null) {
1 ✔
813
                  status = Status.UNAVAILABLE.withCause(cause)
×
814
                      .withDescription("Connection closed while stream is buffered");
×
815
                }
816
                stream.transportReportStatus(status, RpcProgress.MISCARRIED, true, new Metadata());
1 ✔
817
              }
818
              promise.setFailure(cause);
1 ✔
819
            }
820
          }
1 ✔
821
        });
822
    // When the HEADERS are not buffered because of MAX_CONCURRENT_STREAMS in
823
    // StreamBufferingEncoder, the stream is created immediately even if the bytes of the HEADERS
824
    // are delayed because the OS may have too much buffered and isn't accepting the write. The
825
    // write promise is also delayed until flush(). However, we need to associate the netty stream
826
    // with the transport state so that goingAway() and forcefulClose() and able to notify the
827
    // stream of failures.
828
    //
829
    // This leaves a hole when MAX_CONCURRENT_STREAMS is reached, as http2Stream will be null, but
830
    // it is better than nothing.
831
    Http2Stream http2Stream = connection().stream(streamId);
1 ✔
832
    if (http2Stream != null) {
1 ✔
833
      http2Stream.setProperty(streamKey, stream);
1 ✔
834
    }
835
  }
1 ✔
836

837
  /**
838
   * Cancels this stream.
839
   */
840
  private void cancelStream(ChannelHandlerContext ctx, CancelClientStreamCommand cmd,
841
      ChannelPromise promise) {
842
    NettyClientStream.TransportState stream = cmd.stream();
1 ✔
843
    try (TaskCloseable ignore = PerfMark.traceTask("NettyClientHandler.cancelStream")) {
1 ✔
844
      PerfMark.attachTag(stream.tag());
1 ✔
845
      PerfMark.linkIn(cmd.getLink());
1 ✔
846
      Status reason = cmd.reason();
1 ✔
847
      if (reason != null) {
1 ✔
848
        stream.transportReportStatus(reason, true, new Metadata());
1 ✔
849
      }
850
      if (!cmd.stream().isNonExistent()) {
1 ✔
851
        encoder().writeRstStream(ctx, stream.id(), Http2Error.CANCEL.code(), promise);
1 ✔
852
      } else {
853
        promise.setSuccess();
1 ✔
854
      }
855
    }
856
  }
1 ✔
857

858
  /**
859
   * Sends the given GRPC frame for the stream.
860
   */
861
  private void sendGrpcFrame(ChannelHandlerContext ctx, SendGrpcFrameCommand cmd,
862
      ChannelPromise promise) {
863
    try (TaskCloseable ignore = PerfMark.traceTask("NettyClientHandler.sendGrpcFrame")) {
1 ✔
864
      PerfMark.attachTag(cmd.stream().tag());
1 ✔
865
      PerfMark.linkIn(cmd.getLink());
1 ✔
866
      // Call the base class to write the HTTP/2 DATA frame.
867
      // Note: no need to flush since this is handled by the outbound flow controller.
868
      encoder().writeData(ctx, cmd.stream().id(), cmd.content(), 0, cmd.endStream(), promise);
1 ✔
869
    }
870
  }
1 ✔
871

872
  private void sendPingFrame(ChannelHandlerContext ctx, SendPingCommand msg,
873
      ChannelPromise promise) {
874
    try (TaskCloseable ignore = PerfMark.traceTask("NettyClientHandler.sendPingFrame")) {
1 ✔
875
      PerfMark.linkIn(msg.getLink());
1 ✔
876
      sendPingFrameTraced(ctx, msg, promise);
1 ✔
877
    }
878
  }
1 ✔
879

880
  /**
881
   * Sends a PING frame. If a ping operation is already outstanding, the callback in the message is
882
   * registered to be called when the existing operation completes, and no new frame is sent.
883
   */
884
  private void sendPingFrameTraced(ChannelHandlerContext ctx, SendPingCommand msg,
885
      ChannelPromise promise) {
886
    // Don't check lifecycleManager.getShutdownStatus() since we want to allow pings after shutdown
887
    // but before termination. After termination, messages will no longer arrive because the
888
    // pipeline clears all handlers on channel close.
889

890
    PingCallback callback = msg.callback();
1 ✔
891
    Executor executor = msg.executor();
1 ✔
892
    // we only allow one outstanding ping at a time, so just add the callback to
893
    // any outstanding operation
894
    if (ping != null) {
1 ✔
895
      promise.setSuccess();
1 ✔
896
      ping.addCallback(callback, executor);
1 ✔
897
      return;
1 ✔
898
    }
899

900
    // Use a new promise to prevent calling the callback twice on write failure: here and in
901
    // NettyClientTransport.ping(). It may appear strange, but it will behave the same as if
902
    // ping != null above.
903
    promise.setSuccess();
1 ✔
904
    promise = ctx().newPromise();
1 ✔
905
    // set outstanding operation
906
    long data = USER_PING_PAYLOAD;
1 ✔
907
    Stopwatch stopwatch = stopwatchFactory.get();
1 ✔
908
    stopwatch.start();
1 ✔
909
    ping = new Http2Ping(data, stopwatch);
1 ✔
910
    ping.addCallback(callback, executor);
1 ✔
911
    // and then write the ping
912
    encoder().writePing(ctx, false, USER_PING_PAYLOAD, promise);
1 ✔
913
    ctx.flush();
1 ✔
914
    final Http2Ping finalPing = ping;
1 ✔
915
    promise.addListener(new ChannelFutureListener() {
1 ✔
916
      @Override
917
      public void operationComplete(ChannelFuture future) throws Exception {
918
        if (future.isSuccess()) {
1 ✔
919
          transportTracer.reportKeepAliveSent();
1 ✔
920
          return;
1 ✔
921
        }
922
        Throwable cause = future.cause();
×
923
        Status status = lifecycleManager.getShutdownStatus();
×
924
        if (cause instanceof ClosedChannelException) {
×
925
          if (status == null) {
×
926
            status = Status.UNKNOWN.withDescription("Ping failed but for unknown reason.")
×
927
                    .withCause(future.cause());
×
928
          }
929
        } else {
930
          status = Utils.statusFromThrowable(cause);
×
931
        }
932
        finalPing.failed(status);
×
933
        if (ping == finalPing) {
×
934
          ping = null;
×
935
        }
936
      }
×
937
    });
938
  }
1 ✔
939

940
  private void gracefulClose(ChannelHandlerContext ctx, GracefulCloseCommand msg,
941
      ChannelPromise promise) throws Exception {
942
    lifecycleManager.notifyShutdown(msg.getStatus(), SimpleDisconnectError.SUBCHANNEL_SHUTDOWN);
1 ✔
943
    // Explicitly flush to create any buffered streams before sending GOAWAY.
944
    // TODO(ejona): determine if the need to flush is a bug in Netty
945
    flush(ctx);
1 ✔
946
    close(ctx, promise);
1 ✔
947
  }
1 ✔
948

949
  private void forcefulClose(final ChannelHandlerContext ctx, final ForcefulCloseCommand msg,
950
      ChannelPromise promise) throws Exception {
951
    connection().forEachActiveStream(new Http2StreamVisitor() {
1 ✔
952
      @Override
953
      public boolean visit(Http2Stream stream) throws Http2Exception {
954
        NettyClientStream.TransportState clientStream = clientStream(stream);
1 ✔
955
        Tag tag = clientStream != null ? clientStream.tag() : PerfMark.createTag();
1 ✔
956
        try (TaskCloseable ignore = PerfMark.traceTask("NettyClientHandler.forcefulClose")) {
1 ✔
957
          PerfMark.linkIn(msg.getLink());
1 ✔
958
          PerfMark.attachTag(tag);
1 ✔
959
          if (clientStream != null) {
1 ✔
960
            clientStream.transportReportStatus(msg.getStatus(), true, new Metadata());
1 ✔
961
            resetStream(ctx, stream.id(), Http2Error.CANCEL.code(), ctx.newPromise());
1 ✔
962
          }
963
          stream.close();
1 ✔
964
          return true;
1 ✔
965
        }
966
      }
967
    });
968
    close(ctx, promise);
1 ✔
969
  }
1 ✔
970

971
  /**
972
   * Handler for a GOAWAY being received. Fails any streams created after the
973
   * last known stream. May only be called during a read.
974
   */
975
  private void goingAway(long errorCode, byte[] debugData) {
976
    Status finalStatus = statusFromH2Error(
1 ✔
977
        Status.Code.UNAVAILABLE, "GOAWAY shut down transport", errorCode, debugData);
978
    DisconnectError disconnectError = new GoAwayDisconnectError(
1 ✔
979
        GrpcUtil.Http2Error.forCode(errorCode));
1 ✔
980
    lifecycleManager.notifyGracefulShutdown(finalStatus, disconnectError);
1 ✔
981
    abruptGoAwayStatus = statusFromH2Error(
1 ✔
982
        Status.Code.UNAVAILABLE, "Abrupt GOAWAY closed unsent stream", errorCode, debugData);
983
    // While this _should_ be UNAVAILABLE, Netty uses the wrong stream id in the GOAWAY when it
984
    // fails streams due to HPACK failures (e.g., header list too large). To be more conservative,
985
    // we assume any sent streams may be related to the GOAWAY. This should rarely impact users
986
    // since the main time servers should use abrupt GOAWAYs if there is a protocol error, and if
987
    // there wasn't a protocol error the error code was probably NO_ERROR which is mapped to
988
    // UNAVAILABLE. https://github.com/netty/netty/issues/10670
989
    final Status abruptGoAwayStatusConservative = statusFromH2Error(
1 ✔
990
        null, "Abrupt GOAWAY closed sent stream", errorCode, debugData);
991
    final boolean mayBeHittingNettyBug = errorCode != Http2Error.NO_ERROR.code();
1 ✔
992
    // Try to allocate as many in-flight streams as possible, to reduce race window of
993
    // https://github.com/grpc/grpc-java/issues/2562 . To be of any help, the server has to
994
    // gracefully shut down the connection with two GOAWAYs. gRPC servers generally send a PING
995
    // after the first GOAWAY, so they can very precisely detect when the GOAWAY has been
996
    // processed and thus this processing must be in-line before processing additional reads.
997

998
    // This can cause reentrancy, but should be minor since it is normal to handle writes in
999
    // response to a read. Also, the call stack is rather shallow at this point
1000
    clientWriteQueue.drainNow();
1 ✔
1001
    if (lifecycleManager.notifyShutdown(finalStatus, disconnectError)) {
1 ✔
1002
      // This is for the only RPCs that are actually covered by the GOAWAY error code. All other
1003
      // RPCs were not observed by the remote and so should be UNAVAILABLE.
1004
      channelInactiveReason = statusFromH2Error(
1 ✔
1005
          null, "Connection closed after GOAWAY", errorCode, debugData);
1006
    }
1007

1008
    final int lastKnownStream = connection().local().lastStreamKnownByPeer();
1 ✔
1009
    try {
1010
      connection().forEachActiveStream(new Http2StreamVisitor() {
1 ✔
1011
        @Override
1012
        public boolean visit(Http2Stream stream) throws Http2Exception {
1013
          if (stream.id() > lastKnownStream) {
1 ✔
1014
            NettyClientStream.TransportState clientStream = clientStream(stream);
1 ✔
1015
            if (clientStream != null) {
1 ✔
1016
              // RpcProgress _should_ be REFUSED, but are being conservative. See comment for
1017
              // abruptGoAwayStatusConservative. This does reduce our ability to perform transparent
1018
              // retries, but only if something else caused a connection failure.
1019
              RpcProgress progress = mayBeHittingNettyBug
1 ✔
1020
                  ? RpcProgress.PROCESSED
1 ✔
1021
                  : RpcProgress.REFUSED;
1 ✔
1022
              clientStream.transportReportStatus(
1 ✔
1023
                  abruptGoAwayStatusConservative, progress, false, new Metadata());
1024
            }
1025
            stream.close();
1 ✔
1026
          }
1027
          return true;
1 ✔
1028
        }
1029
      });
1030
    } catch (Http2Exception e) {
×
1031
      throw new RuntimeException(e);
×
1032
    }
1 ✔
1033
  }
1 ✔
1034

1035
  private void cancelPing(Status s) {
1036
    if (ping != null) {
1 ✔
1037
      ping.failed(s);
1 ✔
1038
      ping = null;
1 ✔
1039
    }
1040
  }
1 ✔
1041

1042
  /** If {@code statusCode} is non-null, it will be used instead of the http2 error code mapping. */
1043
  private Status statusFromH2Error(
1044
      Status.Code statusCode, String context, long errorCode, byte[] debugData) {
1045
    Status status = GrpcUtil.Http2Error.statusForCode(errorCode);
1 ✔
1046
    if (statusCode == null) {
1 ✔
1047
      statusCode = status.getCode();
1 ✔
1048
    }
1049
    String debugString = "";
1 ✔
1050
    if (debugData != null && debugData.length > 0) {
1 ✔
1051
      // If a debug message was provided, use it.
1052
      debugString = ", debug data: " + new String(debugData, UTF_8);
1 ✔
1053
    }
1054
    return statusCode.toStatus()
1 ✔
1055
        .withDescription(context + ". " + status.getDescription() + debugString);
1 ✔
1056
  }
1057

1058
  /**
1059
   * Gets the client stream associated to the given HTTP/2 stream object.
1060
   */
1061
  private NettyClientStream.TransportState clientStream(Http2Stream stream) {
1062
    return stream == null ? null : (NettyClientStream.TransportState) stream.getProperty(streamKey);
1 ✔
1063
  }
1064

1065
  private int incrementAndGetNextStreamId() throws StatusException {
1066
    int nextStreamId = connection().local().incrementAndGetNextStreamId();
1 ✔
1067
    if (nextStreamId < 0) {
1 ✔
1068
      logger.fine("Stream IDs have been exhausted for this connection. "
1 ✔
1069
              + "Initiating graceful shutdown of the connection.");
1070
      throw EXHAUSTED_STREAMS_STATUS.asException();
1 ✔
1071
    }
1072
    return nextStreamId;
1 ✔
1073
  }
1074

1075
  private Http2Stream requireHttp2Stream(int streamId) {
1076
    Http2Stream stream = connection().stream(streamId);
1 ✔
1077
    if (stream == null) {
1 ✔
1078
      // This should never happen.
1079
      throw new AssertionError("Stream does not exist: " + streamId);
×
1080
    }
1081
    return stream;
1 ✔
1082
  }
1083

1084
  private class FrameListener extends Http2FrameAdapter {
1 ✔
1085
    private boolean firstSettings = true;
1 ✔
1086

1087
    @Override
1088
    public void onSettingsRead(ChannelHandlerContext ctx, Http2Settings settings) {
1089
      if (firstSettings) {
1 ✔
1090
        firstSettings = false;
1 ✔
1091
        attributes = lifecycleManager.filterAttributes(attributes);
1 ✔
1092
        lifecycleManager.notifyReady();
1 ✔
1093
      }
1094
    }
1 ✔
1095

1096
    @Override
1097
    public int onDataRead(ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding,
1098
        boolean endOfStream) throws Http2Exception {
1099
      return NettyClientHandler.this.onDataRead(streamId, data, padding, endOfStream);
1 ✔
1100
    }
1101

1102
    @Override
1103
    public void onHeadersRead(ChannelHandlerContext ctx,
1104
        int streamId,
1105
        Http2Headers headers,
1106
        int streamDependency,
1107
        short weight,
1108
        boolean exclusive,
1109
        int padding,
1110
        boolean endStream) throws Http2Exception {
1111
      NettyClientHandler.this.onHeadersRead(streamId, headers, endStream);
1 ✔
1112
    }
1 ✔
1113

1114
    @Override
1115
    public void onRstStreamRead(ChannelHandlerContext ctx, int streamId, long errorCode)
1116
        throws Http2Exception {
1117
      NettyClientHandler.this.onRstStreamRead(streamId, errorCode);
1 ✔
1118
    }
1 ✔
1119

1120
    @Override
1121
    public void onPingAckRead(ChannelHandlerContext ctx, long ackPayload) throws Http2Exception {
1122
      Http2Ping p = ping;
1 ✔
1123
      if (ackPayload == flowControlPing().payload()) {
1 ✔
1124
        flowControlPing().updateWindow();
1 ✔
1125
        logger.log(Level.FINE, "Window: {0}",
1 ✔
1126
            decoder().flowController().initialWindowSize(connection().connectionStream()));
1 ✔
1127
      } else if (p != null) {
1 ✔
1128
        if (p.payload() == ackPayload) {
1 ✔
1129
          p.complete();
1 ✔
1130
          ping = null;
1 ✔
1131
        } else {
1132
          logger.log(Level.WARNING,
1 ✔
1133
              "Received unexpected ping ack. Expecting {0}, got {1}",
1134
              new Object[] {p.payload(), ackPayload});
1 ✔
1135
        }
1136
      } else {
1137
        logger.warning("Received unexpected ping ack. No ping outstanding");
×
1138
      }
1139
      if (keepAliveManager != null) {
1 ✔
1140
        keepAliveManager.onDataReceived();
1 ✔
1141
      }
1142
    }
1 ✔
1143

1144
    @Override
1145
    public void onPingRead(ChannelHandlerContext ctx, long data) throws Http2Exception {
1146
      if (keepAliveManager != null) {
1 ✔
1147
        keepAliveManager.onDataReceived();
×
1148
      }
1149
    }
1 ✔
1150
  }
1151

1152
  private static class PingCountingFrameWriter extends DecoratingHttp2FrameWriter
1153
      implements AbstractNettyHandler.PingLimiter {
1154
    private int pingCount;
1155

1156
    public PingCountingFrameWriter(Http2FrameWriter delegate) {
1157
      super(delegate);
1 ✔
1158
    }
1 ✔
1159

1160
    @Override
1161
    public boolean isPingAllowed() {
1162
      // "3 strikes" may cause the server to complain, so we limit ourselves to 2 or below.
1163
      return pingCount < 2;
1 ✔
1164
    }
1165

1166
    @Override
1167
    public ChannelFuture writeHeaders(
1168
        ChannelHandlerContext ctx, int streamId, Http2Headers headers,
1169
        int padding, boolean endStream, ChannelPromise promise) {
1170
      pingCount = 0;
1 ✔
1171
      return super.writeHeaders(ctx, streamId, headers, padding, endStream, promise);
1 ✔
1172
    }
1173

1174
    @Override
1175
    public ChannelFuture writeHeaders(
1176
        ChannelHandlerContext ctx, int streamId, Http2Headers headers,
1177
        int streamDependency, short weight, boolean exclusive,
1178
        int padding, boolean endStream, ChannelPromise promise) {
1179
      pingCount = 0;
×
1180
      return super.writeHeaders(ctx, streamId, headers, streamDependency, weight, exclusive,
×
1181
          padding, endStream, promise);
1182
    }
1183

1184
    @Override
1185
    public ChannelFuture writeWindowUpdate(
1186
        ChannelHandlerContext ctx, int streamId, int windowSizeIncrement, ChannelPromise promise) {
1187
      pingCount = 0;
1 ✔
1188
      return super.writeWindowUpdate(ctx, streamId, windowSizeIncrement, promise);
1 ✔
1189
    }
1190

1191
    @Override
1192
    public ChannelFuture writePing(
1193
        ChannelHandlerContext ctx, boolean ack, long data, ChannelPromise promise) {
1194
      if (!ack) {
1 ✔
1195
        pingCount++;
1 ✔
1196
      }
1197
      return super.writePing(ctx, ack, data, promise);
1 ✔
1198
    }
1199

1200
    @Override
1201
    public ChannelFuture writeData(
1202
        ChannelHandlerContext ctx, int streamId, ByteBuf data, int padding, boolean endStream,
1203
        ChannelPromise promise) {
1204
      if (data.isReadable()) {
1 ✔
1205
        pingCount = 0;
1 ✔
1206
      }
1207
      return super.writeData(ctx, streamId, data, padding, endStream, promise);
1 ✔
1208
    }
1209
  }
1210
}
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