All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.gatling.http.client.impl.Http2AppHandler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2011-2024 GatlingCorp (https://gatling.io)
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package io.gatling.http.client.impl;

import io.gatling.http.client.HttpListener;
import io.gatling.http.client.impl.request.WritableRequest;
import io.gatling.http.client.impl.request.WritableRequestBuilder;
import io.gatling.http.client.pool.ChannelPool;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.handler.codec.http2.*;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public final class Http2AppHandler extends ChannelDuplexHandler {

  public static final class StreamTimeout {
    private final int streamId;

    public StreamTimeout(int streamId) {
      this.streamId = streamId;
    }
  }

  public static final class GoAwayFrame {
    private final int lastStreamId;
    private final long errorCode;

    public GoAwayFrame(int lastStreamId, long errorCode) {
      this.lastStreamId = lastStreamId;
      this.errorCode = errorCode;
    }

    @Override
    public String toString() {
      return "GoAwayFrame{lastStreamId=" + lastStreamId + ", errorCode=" + errorCode + '}';
    }
  }

  private static final Logger LOGGER = LoggerFactory.getLogger(Http2AppHandler.class);
  private static final IOException REMOTELY_CLOSED_EXCEPTION =
      new IOException("Channel was closed before handshake completed");

  private final DefaultHttpClient client;
  private final Http2ConnectionHandler http2ConnectionHandler;
  private final ChannelPool channelPool;

  // mutable state
  private boolean writeReached = false;
  private int nextStreamId = 1;
  private final Map txByStreamId = new HashMap<>();

  Http2AppHandler(
      DefaultHttpClient client,
      Http2ConnectionHandler http2ConnectionHandler,
      ChannelPool channelPool) {
    this.client = client;
    this.http2ConnectionHandler = http2ConnectionHandler;
    this.channelPool = channelPool;
  }

  @Override
  public boolean isSharable() {
    return false;
  }

  @Override
  public void write(ChannelHandlerContext ctx, Object msg, ChannelPromise promise) {
    writeReached = true;
    HttpTx tx = (HttpTx) msg;

    if (tx.requestTimeout.isDone()) {
      channelPool.offer(ctx.channel());
      return;
    }
    nextStreamId += 2; // that's how Netty works, 1 is reserved for the connection itself
    int thisStreamId = nextStreamId;
    txByStreamId.put(thisStreamId, tx);

    try {
      WritableRequest request =
          WritableRequestBuilder.buildRequest(tx.request, ctx.alloc(), true, tx.listener);
      LOGGER.debug("Write request {}", request);
      tx.listener.onWrite(ctx.channel());

      request
          .getRequest()
          .headers()
          .setInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text(), thisStreamId);

      ChannelFuture whenWrite;
      if (HttpUtil.is100ContinueExpected(request.getRequest())) {
        LOGGER.debug("Delaying body write");
        tx.pendingRequestExpectingContinue = request;
        whenWrite = request.writeWithoutContent(ctx);
      } else {
        whenWrite = request.write(ctx);
      }

      whenWrite.addListener(
          f -> {
            if (f.isSuccess()) {
              if (tx.requestTimeout.isDone()) {
                resetStream(ctx, thisStreamId);
              } else {
                tx.requestTimeout.setStreamId(thisStreamId);
              }

            } else {
              tx.requestTimeout.cancel();
              tx.listener.onThrowable(f.cause());
            }
          });
    } catch (Exception e) {
      crash(ctx, e, tx.listener, true);
    }
  }

  private void channelReadHttpResponse(ChannelHandlerContext ctx, HttpResponse response) {
    Integer streamId =
        response.headers().getInt(HttpConversionUtil.ExtensionHeaderNames.STREAM_ID.text());
    HttpTx tx = txByStreamId.get(streamId);

    if (tx.requestTimeout.isDone()) {
      tx.releasePendingRequestExpectingContinue();
      resetStream(ctx, streamId);
      return;
    }

    HttpResponseStatus status = response.status();

    if (tx.pendingRequestExpectingContinue != null) {
      if (status.equals(HttpResponseStatus.CONTINUE)) {
        LOGGER.debug("Received 100-Continue");
        return;

      } else {
        // TODO implement 417 support
        LOGGER.debug(
            "Request was sent with Expect:100-Continue but received response with status {}, dropping",
            status);
        tx.releasePendingRequestExpectingContinue();
      }
    }

    tx.listener.onHttpResponse(status, response.headers());
  }

  private void channelReadHttp2Content(ChannelHandlerContext ctx, Http2Content content) {
    int streamId = content.streamId;
    HttpTx tx = txByStreamId.get(streamId);

    if (tx.requestTimeout.isDone()) {
      resetStream(ctx, streamId);
      return;
    }

    boolean last = content.last;

    if (tx.pendingRequestExpectingContinue != null) {
      if (last) {
        LOGGER.debug("Received 100-Continue' LastHttpContent, sending body");
        tx.pendingRequestExpectingContinue.writeContent(ctx);
        tx.pendingRequestExpectingContinue = null;
      }
      return;
    }

    tx.listener.onHttpResponseBodyChunk(content.httpContent.content(), last);
    if (last) {
      tx.requestTimeout.cancel();
      closeStream(ctx, streamId);
    }
  }

  @Override
  public void channelRead(ChannelHandlerContext ctx, Object msg) {
    if (msg instanceof DefaultHttpResponse) {
      // fast path
      channelReadHttpResponse(ctx, (DefaultHttpResponse) msg);

    } else if (msg instanceof Http2Content) {
      // fast path
      channelReadHttp2Content(ctx, (Http2Content) msg);

    } else if (msg instanceof HttpResponse) {
      // slow path
      channelReadHttpResponse(ctx, (HttpResponse) msg);
    }
  }

  private void crash(
      ChannelHandlerContext ctx,
      Throwable cause,
      HttpListener nonActiveStreamListener,
      boolean close) {
    try {
      if (nonActiveStreamListener != null) {
        nonActiveStreamListener.onThrowable(cause);
      }
      txByStreamId.forEach(
          (id, tx) -> {
            tx.releasePendingRequestExpectingContinue();
            tx.listener.onThrowable(cause);
          });
    } finally {
      if (close) {
        // FIXME shouldn't we close the connection?
        ctx.close();
      }
    }

    if (cause instanceof Error) {
      LOGGER.error("Fatal error", cause);
      System.exit(1);
    }
  }

  @Override
  public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
    crash(ctx, cause, null, true);
  }

  @Override
  public void channelInactive(ChannelHandlerContext ctx) {
    if (!writeReached) {
      crash(ctx, REMOTELY_CLOSED_EXCEPTION, null, false);
    }
  }

  @Override
  public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
    if (evt instanceof GoAwayFrame) {
      GoAwayFrame goAway = (GoAwayFrame) evt;

      LOGGER.debug("Received GOAWAY frame: {}", goAway);
      ChannelPool.markAsGoAway(ctx.channel());

      List retryTxs = new ArrayList<>(3);

      List> droppedStreams =
          txByStreamId.entrySet().stream()
              .filter(entry -> entry.getKey() > goAway.lastStreamId)
              .collect(Collectors.toList());

      droppedStreams.forEach(
          entry -> {
            txByStreamId.remove(entry.getKey());
            HttpTx tx = entry.getValue();
            if (goAway.errorCode == Http2Error.NO_ERROR.code() && client.canRetry(tx)) {
              retryTxs.add(tx);
            } else {
              tx.listener.onThrowable(REMOTELY_CLOSED_EXCEPTION);
            }
          });

      if (!retryTxs.isEmpty()) {
        client.retryHttp2(retryTxs, ctx.channel().eventLoop());
      }

    } else if (evt instanceof StreamTimeout) {
      resetStream(ctx, ((StreamTimeout) evt).streamId);
    }
  }

  private void closeStream(ChannelHandlerContext ctx, int streamId) {
    txByStreamId.remove(streamId);
    http2ConnectionHandler.connection().stream(streamId).close();
    channelPool.offer(ctx.channel());
  }

  private void resetStream(ChannelHandlerContext ctx, int streamId) {
    txByStreamId.remove(streamId);
    http2ConnectionHandler
        .resetStream(ctx, streamId, Http2Error.CANCEL.code(), ctx.newPromise())
        .addListener((ChannelFutureListener) future -> channelPool.offer(ctx.channel()));
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy