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

com.google.appengine.tools.development.ApiServlet Maven / Gradle / Ivy

Go to download

SDK for dev_appserver (local development) with some of the dependencies shaded (repackaged)

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.appengine.tools.development;

import com.google.apphosting.api.ApiProxy.ApiProxyException;
import com.google.apphosting.api.ApiProxy.CallNotFoundException;
import com.google.apphosting.base.protos.api.RemoteApiPb;
import com.google.apphosting.base.protos.api.RemoteApiPb.RpcError;
import com.google.appengine.repackaged.com.google.common.annotations.VisibleForTesting;
import com.google.appengine.repackaged.com.google.common.base.Ascii;
import com.google.appengine.repackaged.com.google.common.primitives.Doubles;
import com.google.appengine.repackaged.com.google.protobuf.ByteString;
import com.google.appengine.repackaged.com.google.protobuf.ExtensionRegistry;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetSocketAddress;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Servlet handling POST requests to serve App Engine Standard API calls implemented by the API stub
 * implementations used by the dev app server. This can be used in a local dev environment to
 * emulate App Engine APIs, or in a test environment. The protocol buffer used is the same as the
 * App Engine remote APIs documented at
 * https://cloud.google.com/appengine/docs/standard/java/tools/remoteapi and the one used from the
 * Java clones in production to make API calls.
 */
public class ApiServlet extends HttpServlet {
  private static final Logger logger = Logger.getLogger(ApiServlet.class.getName());

  private static final String RPC_ENDPOINT_HEADER = "X-Google-RPC-Service-Endpoint";
  private static final String RPC_ENDPOINT_VALUE = "app-engine-apis";
  private static final String RPC_METHOD_HEADER = "X-Google-RPC-Service-Method";
  private static final String RPC_METHOD_VALUE = "/VMRemoteAPI.CallRemoteAPI";
  private static final String CONTENT_TYPE_HEADER = "Content-Type";
  private static final String CONTENT_TYPE_VALUE = "application/octet-stream";
  private static final String DEADLINE_HEADER = "X-Google-RPC-Service-Deadline";
  private static final String RUNTIME_PORT_CONFIG = "java_runtime_port";
  private static final String RUNTIME_HOST_CONFIG = "java_runtime_host";
  private static final String EXECUTOR_POOL_SIZE = "executor_pool_size";

  private final Map methodCache = new ConcurrentHashMap<>();
  private ApiProxyLocal apiProxyLocal;
  private int serverPort;
  private String serverHost;
  private ExecutorService executor;
  private static final int EXECUTOR_THREAD_POOL_DEFAULT_SIZE = 10;

  /**
   * Configure the APIServlet with 2 servlet init paramerters:
   *
   * 
   * java_runtime_port:  the local port of the java clone. This is needed for the taskqueue APIs to
   * be able to post callback to the clone.
   * java_runtime_host:  the hostname of the java clone. (default to localhost).
   *    * executor_pool_size: size of the threadpool handling API calls. Default is 10.
   * 
*/ @Override public void init(ServletConfig config) throws ServletException { super.init(config); if (config.getInitParameter(RUNTIME_PORT_CONFIG) == null) { throw new NumberFormatException("Missing " + RUNTIME_PORT_CONFIG + " init parameter."); } serverPort = Integer.parseInt(config.getInitParameter(RUNTIME_PORT_CONFIG)); if (config.getInitParameter(RUNTIME_HOST_CONFIG) == null) { serverHost = "localhost"; } else { serverHost = config.getInitParameter(RUNTIME_HOST_CONFIG); } apiProxyLocal = new ApiProxyLocalFactory().create(new LocalEnv(serverHost, serverPort)); // True to put the datastore into "memory-only" mode. apiProxyLocal.setProperty("datastore.no_storage", "true"); String executorSize = config.getInitParameter(EXECUTOR_POOL_SIZE); int poolSize = (executorSize == null) ? EXECUTOR_THREAD_POOL_DEFAULT_SIZE : Integer.parseInt(executorSize); executor = Executors.newFixedThreadPool(poolSize); } @Override public void destroy() { executor.shutdown(); } @Override public void doPost(HttpServletRequest request, HttpServletResponse response) throws IOException { Double deadline = validateHeaders(request); try (InputStream in = request.getInputStream()) { RemoteApiPb.Request requestPb = RemoteApiPb.Request.parseFrom(in, ExtensionRegistry.getEmptyRegistry()); if (in.read() >= 0) { throw new IllegalArgumentException("Extra data after request"); } response.addHeader(CONTENT_TYPE_HEADER, CONTENT_TYPE_VALUE); RemoteApiPb.Response responsePb = handleRequestInThread(apiProxyLocal, requestPb, deadline); try (OutputStream out = response.getOutputStream()) { out.write(responsePb.toByteArray()); } } catch (RuntimeException e) { logger.log(Level.WARNING, "bad request:", e); response.setStatus(HttpServletResponse.SC_BAD_REQUEST); } } // Checks mandatory headers, and return the expected deadline (in seconds) for the request. @VisibleForTesting Double validateHeaders(HttpServletRequest request) { String endpoint = request.getHeader(RPC_ENDPOINT_HEADER); if (!RPC_ENDPOINT_VALUE.equals(endpoint)) { throw new IllegalArgumentException( RPC_ENDPOINT_HEADER + " should be " + RPC_ENDPOINT_VALUE + ", not " + endpoint + "."); } String method = request.getHeader(RPC_METHOD_HEADER); if (!RPC_METHOD_VALUE.equals(method)) { throw new IllegalArgumentException( RPC_METHOD_HEADER + " should be " + RPC_METHOD_VALUE + ", not " + method + "."); } String contentType = request.getHeader(CONTENT_TYPE_HEADER); if (!CONTENT_TYPE_VALUE.equals(contentType)) { throw new IllegalArgumentException( CONTENT_TYPE_HEADER + " should be " + CONTENT_TYPE_VALUE + ", not " + contentType + "."); } String deadlineString = request.getHeader(DEADLINE_HEADER); Double deadline = (deadlineString == null) ? null : Doubles.tryParse(deadlineString); if (deadline == null) { throw new IllegalArgumentException( "Missing or incorrect deadline header in request: " + deadlineString + "."); } return deadline; } // Simulates deadline handling by running the request in a separate thread and waiting for // the result with a deadline. private RemoteApiPb.Response handleRequestInThread( final ApiProxyLocal apiProxy, final RemoteApiPb.Request requestPb, double deadline) { Callable task = new Callable() { @Override public RemoteApiPb.Response call() { return handle(apiProxy, requestPb); } }; Future future = executor.submit(task); try { return future.get((long) (deadline * 1000), TimeUnit.MILLISECONDS); } catch (ExecutionException e) { return exceptionResponse(e); } catch (InterruptedException | TimeoutException e) { future.cancel(/* mayInterruptIfRunning= */ true); return timeoutResponse(deadline); } } /** * Invokes an API call using the Java implementations. * * @param apiProxy the local ApiProxy environment * @param packageName the name of the API service, eg datastore_v3 * @param methodName the name of the API method, eg Query * @param requestBytes the serialized proto, eg DatastoreV3Pb.Query * @return the serialized API response */ @VisibleForTesting byte[] invokeApiMethodJava( ApiProxyLocal apiProxy, String packageName, String methodName, byte[] requestBytes) throws IllegalAccessException, InstantiationException, InvocationTargetException, NoSuchMethodException { LocalRpcService service = apiProxy.getService(packageName); if (service == null) { throw new CallNotFoundException(packageName, methodName); } Method method = getDispatchMethod(service, packageName, methodName); LocalRpcService.Status status = new LocalRpcService.Status(); // For a method like public QueryResult runQuery(Status status, Query query) {...} // return the Query class. Class requestClass = method.getParameterTypes()[1]; Object request = ApiUtils.convertBytesToPb(requestBytes, requestClass); return ApiUtils.convertPbToBytes(method.invoke(service, status, request)); } @VisibleForTesting Method getDispatchMethod(LocalRpcService service, String packageName, String methodName) { // e.g. RunQuery --> runQuery String dispatchName = Ascii.toLowerCase(methodName.charAt(0)) + methodName.substring(1); // e.g. datastore_v3.runQuery String methodId = packageName + "." + dispatchName; Method method = methodCache.get(methodId); if (method != null) { return method; } for (Method candidate : service.getClass().getMethods()) { if (dispatchName.equals(candidate.getName())) { methodCache.put(methodId, candidate); return candidate; } } throw new CallNotFoundException(packageName, methodName); } @VisibleForTesting RemoteApiPb.Response handle(ApiProxyLocal apiProxy, RemoteApiPb.Request request) { byte[] resp; try { resp = invokeApiMethodJava( apiProxy, request.getServiceName(), request.getMethod(), request.getRequest().toByteArray()); } catch (InvocationTargetException ex) { logger.log( Level.INFO, "Exception calling service" + request.getServiceName() + " and method: " + request.getMethod(), ex); throw new ApiProxyException( "API invocation error for service: " + request.getServiceName() + " and method: " + request.getMethod(), ex.getCause()); } catch (ReflectiveOperationException | RuntimeException | Error ex) { logger.log( Level.INFO, "Exception calling service" + request.getServiceName() + " and method: " + request.getMethod(), ex); throw new CallNotFoundException(request.getServiceName(), request.getMethod()); } return RemoteApiPb.Response.newBuilder().setResponse(ByteString.copyFrom(resp)).build(); } private RemoteApiPb.Response timeoutResponse(double deadline) { return RemoteApiPb.Response.newBuilder() .setRpcError( RemoteApiPb.RpcError.newBuilder() .setCode(RemoteApiPb.RpcError.ErrorCode.DEADLINE_EXCEEDED.getNumber()) .setDetail("Deadline of " + deadline + "s was exceeded")) .build(); } private RemoteApiPb.Response exceptionResponse(ExecutionException exception) { RpcError rpcError = RemoteApiPb.RpcError.newBuilder() .setCode(RemoteApiPb.RpcError.ErrorCode.BAD_REQUEST.getNumber()) .setDetail("Execution exception " + exception.getMessage()) .build(); RemoteApiPb.Response.Builder response = RemoteApiPb.Response.newBuilder(); ByteArrayOutputStream byteStream = new ByteArrayOutputStream(); try (ObjectOutput out = new ObjectOutputStream(byteStream)) { out.writeObject(exception); } catch (IOException e) { logger.log(Level.SEVERE, "Cannot serialize the exception: ", e); } byte[] serializedException = byteStream.toByteArray(); response.setJavaException(ByteString.copyFrom(serializedException)); response.setRpcError(rpcError); return response.build(); } private static class LocalEnv implements LocalServerEnvironment { private final String javaRuntimeHost; private final int javaRuntimePort; LocalEnv(String javaRuntimeHost, int javaRuntimePort) { this.javaRuntimeHost = javaRuntimeHost; this.javaRuntimePort = javaRuntimePort; } @Override public File getAppDir() { return new File("."); } @Override public String getAddress() { return new InetSocketAddress(javaRuntimePort).getHostString(); } @Override public String getHostName() { return javaRuntimeHost; } @Override public int getPort() { return javaRuntimePort; } @Override public void waitForServerToStart() throws InterruptedException {} @Override public boolean simulateProductionLatencies() { return false; } @Override public boolean enforceApiDeadlines() { // Not used by this servlet. Instead, the value of DEADLINE_HEADER is used for deadlines. return false; } } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy