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

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

There is a newer version: 2.0.31
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 com.google.apphosting.base.protos.RuntimePb.APIRequest;
import com.google.apphosting.base.protos.RuntimePb.APIResponse;
import com.google.apphosting.base.protos.RuntimePb.APIResponse.ERROR;
import com.google.apphosting.base.protos.RuntimePb.APIResponse.RpcError;
import com.google.apphosting.base.protos.Status.StatusProto;
import com.google.apphosting.base.protos.api.RemoteApiPb;
import com.google.apphosting.runtime.anyrpc.APIHostClientInterface;
import com.google.apphosting.runtime.anyrpc.AnyRpcCallback;
import com.google.apphosting.runtime.anyrpc.AnyRpcClientContext;
import com.google.apphosting.utils.runtime.ApiProxyUtils;
import com.google.auto.value.AutoValue;
import com.google.common.base.Preconditions;
import com.google.common.collect.ImmutableMap;
import com.google.common.flogger.GoogleLogger;
import com.google.protobuf.ByteString;
import com.google.protobuf.CodedInputStream;
import com.google.protobuf.ExtensionRegistry;
import com.google.protobuf.UninitializedMessageException;
import java.io.IOException;
import java.util.Optional;
import java.util.OptionalInt;

/**
 * A client of the APIHost service over HTTP.
 *
 */
abstract class HttpApiHostClient implements APIHostClientInterface {
  private static final GoogleLogger logger = GoogleLogger.forEnclosingClass();

  /**
   * Extra timeout that will be used for the HTTP request. If the API timeout is 5 seconds, the
   * HTTP request will have a timeout of 5 + {@value #DEFAULT_EXTRA_TIMEOUT_SECONDS} seconds.
   * Usually another timeout will happen first, either the API timeout on the server or the
   * TimedFuture timeout on the client, but this one enables us to clean up the HttpClient if the
   * server is unresponsive.
   */
  static final double DEFAULT_EXTRA_TIMEOUT_SECONDS = 2.0;

  static final ImmutableMap HEADERS = ImmutableMap.of(
      "X-Google-RPC-Service-Endpoint", "app-engine-apis",
      "X-Google-RPC-Service-Method", "/VMRemoteAPI.CallRemoteAPI");
  static final String CONTENT_TYPE_VALUE = "application/octet-stream";
  static final String REQUEST_ENDPOINT = "/rpc_http";
  static final String DEADLINE_HEADER = "X-Google-RPC-Service-Deadline";

  private static final int UNKNOWN_ERROR_CODE = 1;

  // TODO: study the different limits that we have for different transports and
  // make them more consistent, as well as sharing definitions like this one.
  /** The maximum size in bytes that we will allow in a request or a response payload. */
  static final int MAX_PAYLOAD = 50 * 1024 * 1024;
  /**
   * Extra bytes that we allow in the HTTP content, basically to support serializing the other
   * proto fields besides the payload.
   */
  static final int EXTRA_CONTENT_BYTES = 4096;

  @AutoValue
  abstract static class Config {
    abstract double extraTimeoutSeconds();
    abstract OptionalInt maxConnectionsPerDestination();

    /** For testing that we handle missing Content-Length correctly. */
    abstract boolean ignoreContentLength();

    /**
     * Treat {@link java.nio.channels.ClosedChannelException} as indicating cancellation. We know
     * that this happens occasionally in a test that generates many interrupts. But we don't know if
     * there are other reasons for which it might arise, so for now we do not do this in production.
     *
     * 

See this bug for further background. */ abstract boolean treatClosedChannelAsCancellation(); static Builder builder() { return new AutoValue_HttpApiHostClient_Config.Builder() .setExtraTimeoutSeconds(DEFAULT_EXTRA_TIMEOUT_SECONDS) .setIgnoreContentLength(false) .setTreatClosedChannelAsCancellation(false); } abstract Builder toBuilder(); @AutoValue.Builder abstract static class Builder { abstract Builder setMaxConnectionsPerDestination(OptionalInt value); abstract Builder setExtraTimeoutSeconds(double value); abstract Builder setIgnoreContentLength(boolean value); abstract Builder setTreatClosedChannelAsCancellation(boolean value); abstract Config build(); } } private final Config config; HttpApiHostClient(Config config) { this.config = config; } Config config() { return config; } static HttpApiHostClient create(String url, Config config) { if (System.getenv("APPENGINE_API_CALLS_USING_JDK_CLIENT") != null) { logger.atInfo().log("Using JDK HTTP client for API calls"); return JdkHttpApiHostClient.create(url, config); } else { return JettyHttpApiHostClient.create(url, config); } } static class Context implements AnyRpcClientContext { private final long startTimeMillis; private int applicationError; private String errorDetail; private StatusProto status; private Throwable exception; private Optional deadlineNanos = Optional.empty(); Context() { this.startTimeMillis = System.currentTimeMillis(); } @Override public int getApplicationError() { return applicationError; } void setApplicationError(int applicationError) { this.applicationError = applicationError; } @Override public String getErrorDetail() { return errorDetail; } void setErrorDetail(String errorDetail) { this.errorDetail = errorDetail; } @Override public Throwable getException() { return exception; } void setException(Throwable exception) { this.exception = exception; } @Override public long getStartTimeMillis() { return startTimeMillis; } @Override public StatusProto getStatus() { return status; } void setStatus(StatusProto status) { this.status = status; } @Override public void setDeadline(double seconds) { Preconditions.checkArgument(seconds >= 0); double nanos = 1_000_000_000 * seconds; Preconditions.checkArgument(nanos <= Long.MAX_VALUE); this.deadlineNanos = Optional.of((long) nanos); } Optional getDeadlineNanos() { return deadlineNanos; } @Override public void startCancel() { logger.atWarning().log("Canceling HTTP API call has no effect"); } } @Override public Context newClientContext() { return new Context(); } static void communicationFailure( Context context, String errorDetail, AnyRpcCallback callback, Throwable cause) { context.setApplicationError(0); context.setErrorDetail(errorDetail); context.setStatus( StatusProto.newBuilder() .setSpace("RPC") .setCode(UNKNOWN_ERROR_CODE) .setCanonicalCode(UNKNOWN_ERROR_CODE) .setMessage(errorDetail) .build()); context.setException(cause); callback.failure(); } // This represents a timeout of our HTTP request. We don't usually expect this, because we // include a timeout in the API call which the server should respect. However, this fallback // logic ensures that we will get an appropriate and timely exception if the server is very slow // to respond for some reason. // ApiProxyImpl will normally have given up before this happens, so the main purpose of the // timeout is to free up resources from the failed HTTP request. static void timeout(AnyRpcCallback callback) { APIResponse apiResponse = APIResponse.newBuilder() .setError(APIResponse.ERROR.RPC_ERROR_VALUE) .setRpcError(RpcError.DEADLINE_EXCEEDED) .build(); callback.success(apiResponse); // This is "success" in the sense that we got back a response, but one that will provoke // an ApiProxy.ApiDeadlineExceededException. } static void cancelled(AnyRpcCallback callback) { APIResponse apiResponse = APIResponse.newBuilder().setError(ERROR.CANCELLED_VALUE).build(); callback.success(apiResponse); // This is "success" in the sense that we got back a response, but one that will provoke // an ApiProxy.CancelledException. } @Override public void call(AnyRpcClientContext ctx, APIRequest req, AnyRpcCallback cb) { Context context = (Context) ctx; ByteString payload = req.getPb(); if (payload.size() > MAX_PAYLOAD) { requestTooBig(cb); return; } RemoteApiPb.Request requestPb = RemoteApiPb.Request.newBuilder() .setServiceName(req.getApiPackage()) .setMethod(req.getCall()) .setRequest(payload) .setRequestId(req.getSecurityTicket()) .setTraceContext(req.getTraceContext().toByteString()) .build(); send(requestPb.toByteArray(), context, cb); } static void receivedResponse( byte[] responseBytes, int responseLength, Context context, AnyRpcCallback callback) { logger.atFine().log("Response size %d", responseLength); CodedInputStream input = CodedInputStream.newInstance(responseBytes, 0, responseLength); RemoteApiPb.Response responsePb; try { responsePb = RemoteApiPb.Response.parseFrom(input, ExtensionRegistry.getEmptyRegistry()); } catch (UninitializedMessageException | IOException e) { String errorDetail = "Failed to parse RemoteApiPb.Response"; logger.atWarning().withCause(e).log("%s", errorDetail); communicationFailure(context, errorDetail, callback, e); return; } if (responsePb.hasApplicationError()) { RemoteApiPb.ApplicationError applicationError = responsePb.getApplicationError(); context.setApplicationError(applicationError.getCode()); context.setErrorDetail(applicationError.getDetail()); context.setStatus(StatusProto.getDefaultInstance()); callback.failure(); return; } APIResponse apiResponse = APIResponse.newBuilder() .setError(ApiProxyUtils.remoteApiErrorToApiResponseError(responsePb).getNumber()) .setPb(responsePb.getResponse()) .build(); callback.success(apiResponse); } abstract void send(byte[] requestBytes, Context context, AnyRpcCallback callback); private static void requestTooBig(AnyRpcCallback cb) { APIResponse apiResponse = APIResponse.newBuilder().setError(ERROR.REQUEST_TOO_LARGE_VALUE).build(); cb.success(apiResponse); // This is "success" in the sense that we got back a response, but one that will provoke // an ApiProxy.RequestTooLargeException. } static void responseTooBig(AnyRpcCallback cb) { APIResponse apiResponse = APIResponse.newBuilder().setError(ERROR.RESPONSE_TOO_LARGE_VALUE).build(); cb.success(apiResponse); // This is "success" in the sense that we got back a response, but one that will provoke // an ApiProxy.ResponseTooLargeException. } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy