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

com.google.apphosting.runtime.http.JettyHttpApiHostClient Maven / Gradle / Ivy

There is a newer version: 2.0.32
Show newest version
/*
 * Copyright 2021 Google LLC
 *
 * 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
 *
 *     https://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 com.google.apphosting.runtime.http;

import static java.lang.Math.max;
import static java.util.concurrent.TimeUnit.NANOSECONDS;

import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.base.protos.RuntimePb.APIResponse;
import com.google.apphosting.runtime.anyrpc.AnyRpcCallback;
import com.google.common.base.Preconditions;
import com.google.common.flogger.GoogleLogger;
import com.google.common.primitives.Longs;
import java.net.HttpURLConnection;
import java.nio.ByteBuffer;
import java.nio.channels.ClosedByInterruptException;
import java.nio.channels.ClosedChannelException;
import java.nio.channels.ClosedSelectorException;
import java.util.Arrays;
import java.util.Map;
import java.util.concurrent.Executors;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.atomic.AtomicInteger;
import org.eclipse.jetty.client.BytesRequestContent;
import org.eclipse.jetty.client.HttpClient;
import org.eclipse.jetty.client.HttpResponseException;
import org.eclipse.jetty.client.Request;
import org.eclipse.jetty.client.Response;
import org.eclipse.jetty.client.Response.CompleteListener;
import org.eclipse.jetty.client.Result;
import org.eclipse.jetty.http.HttpFields;
import org.eclipse.jetty.http.HttpHeader;
import org.eclipse.jetty.http.HttpMethod;
import org.eclipse.jetty.io.EofException;
import org.eclipse.jetty.util.thread.ScheduledExecutorScheduler;
import org.eclipse.jetty.util.thread.Scheduler;

/**
 * A client of the APIHost service over HTTP, implemented using the Jetty client API.
 *
 */
class JettyHttpApiHostClient extends HttpApiHostClient {
  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();

  private static final AtomicInteger threadCount = new AtomicInteger();

  private final String url;
  private final HttpClient httpClient;

  private JettyHttpApiHostClient(String url, HttpClient httpClient, Config config) {
    super(config);
    this.url = url;
    this.httpClient = httpClient;
  }

  static JettyHttpApiHostClient create(String url, Config config) {
    Preconditions.checkNotNull(url);
    HttpClient httpClient = new HttpClient();
    String schedulerName =
        HttpClient.class.getSimpleName() + "@" + httpClient.hashCode() + "-scheduler";
    ClassLoader myLoader = JettyHttpApiHostClient.class.getClassLoader();
    ThreadGroup myThreadGroup = Thread.currentThread().getThreadGroup();
    boolean daemon = false;
    Scheduler scheduler =
        new ScheduledExecutorScheduler(schedulerName, daemon, myLoader, myThreadGroup);
    ThreadFactory factory =
        runnable -> {
          Thread t = new Thread(myThreadGroup, runnable);
          t.setName("JettyHttpApiHostClient-" + threadCount.incrementAndGet());
          t.setDaemon(true);
          return t;
        };
    // By default HttpClient will use a QueuedThreadPool with minThreads=8 and maxThreads=200.
    // 8 threads is probably too much for most apps, especially since asynchronous I/O means that
    // 8 concurrent API requests probably don't need that many threads. It's also not clear
    // what advantage we'd get from using a QueuedThreadPool with a smaller minThreads value, versus
    // just one of the standard java.util.concurrent pools. Here we have minThreads=1, maxThreads=∞,
    // and idleTime=60 seconds. maxThreads=200 and maxThreads=∞ are probably equivalent in practice.
    httpClient.setExecutor(Executors.newCachedThreadPool(factory));
    httpClient.setScheduler(scheduler);
    config.maxConnectionsPerDestination().ifPresent(httpClient::setMaxConnectionsPerDestination);
    try {
      httpClient.start();
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
    return new JettyHttpApiHostClient(url, httpClient, config);
  }

  private class Listener implements Response.Listener {

    private static final int MAX_LENGTH = MAX_PAYLOAD + EXTRA_CONTENT_BYTES;

    private final Context context;
    private final AnyRpcCallback callback;
    private byte[] buffer;
    private int offset;

    Listener(Context context, AnyRpcCallback callback) {
      this.context = context;
      this.callback = callback;
    }

    @Override
    public void onHeaders(Response response) {
      HttpFields headers = response.getHeaders();
      String lengthString = headers.get(HttpHeader.CONTENT_LENGTH.asString());
      Long length = (lengthString == null) ? null : Longs.tryParse(lengthString);
      if (length == null || config().ignoreContentLength()) {
        // We expect there to be a Content-Length, but we should be correct if less efficient
        // even if not.
        buffer = new byte[2048];
      } else if (length > MAX_LENGTH) {
        abortBecauseTooLarge(response);
        return;
      } else {
        buffer = new byte[length.intValue()];
      }
      offset = 0;
    }

    @Override
    public void onContent(Response response, ByteBuffer byteBuffer) {
      int byteCount = byteBuffer.remaining();
      if (offset + byteCount > MAX_LENGTH) {
        abortBecauseTooLarge(response);
        return;
      }
      int bufferRemaining = buffer.length - offset;
      if (byteCount > bufferRemaining) {
        int newSize = max((int) (buffer.length * 1.5), offset + byteCount);
        logger.atInfo().log(
            "Had to resize buffer, %d > %d; resizing to %d", byteCount, bufferRemaining, newSize);
        buffer = Arrays.copyOf(buffer, newSize);
        bufferRemaining = buffer.length - offset;
        Preconditions.checkState(byteCount <= bufferRemaining);
      }
      byteBuffer.get(buffer, offset, byteCount);
      offset += byteCount;
    }

    private void abortBecauseTooLarge(Response response) {
      response.abort(new ApiProxy.ResponseTooLargeException(null, null));
      // This exception will be replaced with a proper one in onComplete().
    }

    @Override
    public void onComplete(Result result) {
      if (result.isFailed()) {
        Throwable failure = result.getFailure();
        if (failure instanceof ApiProxy.ResponseTooLargeException) {
          responseTooBig(callback);
        } else if (failure instanceof TimeoutException) {
          logger.atWarning().withCause(failure).log("HTTP communication timed out");
          timeout(callback);
        } else if (failure instanceof EofException
            && failure.getCause() instanceof ClosedByInterruptException) {
          // This is a very specific combination of exceptions, which we observe is produced with
          // the particular Jetty client we're using. HttpApiProxyImplTest#interruptedApiCall
          // should detect if a future Jetty version produces a different combination.
          logger.atWarning().withCause(failure).log("HTTP communication interrupted");
          cancelled(callback);
        } else if ((failure instanceof ClosedChannelException
                || failure instanceof ClosedSelectorException)
            && config().treatClosedChannelAsCancellation()) {
          logger.atWarning().log("Treating %s as cancellation", failure.getClass().getSimpleName());
          cancelled(callback);
        } else if (failure instanceof RejectedExecutionException) {
          logger.atWarning().withCause(failure).log("API connection appears to be disabled");
          cancelled(callback);
        } else if (failure instanceof HttpResponseException) {
          // TODO(b/111131627) remove this once upgraded to Jetty that includes the cause
          HttpResponseException hre = (HttpResponseException) failure;
          Response response = hre.getResponse();
          String httpError = response.getStatus() + " " + response.getReason();
          logger.atWarning().withCause(failure).log("HTTP communication failed: %s", httpError);
          if (hre.getCause() == null) {
            failure = new Exception(httpError, hre);
          }
          communicationFailure(context, failure + ": " + httpError, callback, failure);
        } else {
          logger.atWarning().withCause(failure).log("HTTP communication failed");
          communicationFailure(context, String.valueOf(failure), callback, failure);
        }
      } else {
        Response response = result.getResponse();
        if (response.getStatus() == HttpURLConnection.HTTP_OK) {
          receivedResponse(buffer, offset, context, callback);
        } else {
          String httpError = response.getStatus() + " " + response.getReason();
          logger.atWarning().log("HTTP communication got error: %s", httpError);
          communicationFailure(context, httpError, callback, null);
        }
      }
    }
  }

  @Override
  void send(byte[] requestBytes, HttpApiHostClient.Context context,
      AnyRpcCallback callback) {
    Request request = httpClient
        .newRequest(url)
        .method(HttpMethod.POST)
        .body(new BytesRequestContent(CONTENT_TYPE_VALUE, requestBytes));

    request = request.headers(headers ->
    {
      for (Map.Entry header : HEADERS.entrySet()) {
        headers.add(header.getKey(), header.getValue());
      }
    });

    if (context.getDeadlineNanos().isPresent()) {
      double deadlineSeconds = context.getDeadlineNanos().get() / 1e9;

      request = request.headers(headers ->
              headers.add(DEADLINE_HEADER, Double.toString(deadlineSeconds)));

      // If the request exceeds the deadline, one of two things can happen: (1) the API server
      // returns with a deadline-exceeded status; (2) ApiProxyImpl will time out because of the
      // TimedFuture class that it uses. The only purpose of this fallback deadline is to ensure
      // that, if the server is genuinely unresponsive, we will eventually free up the resources
      // associated with the HTTP request.
      // If ApiProxyImpl times out, it will be 0.5 seconds after the called-for time out, which is
      // sooner than here with the default value of extraTimeoutSeconds.
      double fallbackDeadlineSeconds = deadlineSeconds + config().extraTimeoutSeconds();
      request.timeout((long) (fallbackDeadlineSeconds * 1e9), NANOSECONDS);
    }
    CompleteListener completeListener = new Listener(context, callback);
    request.send(completeListener);
  }

  @Override
  public synchronized void disable() {
    try {
      httpClient.stop();
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }

  @Override
  public synchronized void enable() {
    try {
      httpClient.start();
    } catch (RuntimeException e) {
      throw e;
    } catch (Exception e) {
      throw new RuntimeException(e);
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy