/**
* Copyright 2012 Google Inc. All Rights Reserved.
*
* 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 com.google.apphosting.vmruntime;
import com.google.appengine.api.appidentity.AppIdentityServiceFailureException;
import com.google.appengine.api.blobstore.BlobstoreFailureException;
import com.google.appengine.api.channel.ChannelFailureException;
import com.google.appengine.api.datastore.DatastoreFailureException;
import com.google.appengine.api.images.ImagesServiceFailureException;
import com.google.appengine.api.log.LogServiceException;
import com.google.appengine.api.memcache.MemcacheServiceException;
import com.google.appengine.api.modules.ModulesException;
import com.google.appengine.api.search.SearchException;
import com.google.appengine.api.taskqueue.TransientFailureException;
import com.google.appengine.api.users.UserServiceFailureException;
import com.google.appengine.api.xmpp.XMPPFailureException;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.ApiConfig;
import com.google.apphosting.api.ApiProxy.ApiProxyException;
import com.google.apphosting.api.ApiProxy.LogRecord;
import com.google.apphosting.api.ApiProxy.RPCFailedException;
import com.google.apphosting.utils.remoteapi.RemoteApiPb;
import com.google.appengine.repackaged.com.google.common.collect.Lists;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.params.ConnManagerPNames;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.http.entity.ContentType;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.params.HttpParams;
import org.apache.http.protocol.BasicHttpContext;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.List;
import java.util.Scanner;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.logging.Logger;
/**
* Delegates AppEngine API calls to a local http API proxy when running inside a VM.
*
* <p>Instances should be registered using ApiProxy.setDelegate(ApiProxy.Delegate).
*
*/
public class VmApiProxyDelegate implements ApiProxy.Delegate<VmApiProxyEnvironment> {
private static final Logger logger = Logger.getLogger(VmApiProxyDelegate.class.getName());
public static final String RPC_DEADLINE_HEADER = "X-Google-RPC-Service-Deadline";
public static final String RPC_STUB_ID_HEADER = "X-Google-RPC-Service-Endpoint";
public static final String RPC_METHOD_HEADER = "X-Google-RPC-Service-Method";
public static final String REQUEST_ENDPOINT = "/rpc_http";
public static final String REQUEST_STUB_ID = "app-engine-apis";
public static final String REQUEST_STUB_METHOD = "/VMRemoteAPI.CallRemoteAPI";
// This is the same definition as com.google.apphosting.api.API_DEADLINE_KEY. It is also defined
// here to avoid being exposed to the users in appengine-api.jar.
protected static final String API_DEADLINE_KEY =
"com.google.apphosting.api.ApiProxy.api_deadline_key";
// Default timeout for RPC calls.
static final int DEFAULT_RPC_TIMEOUT_MS = 60 * 1000;
// Wait for 1000 ms in addition to the RPC timeout before closing the HTTP connection.
static final int ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS = 1000;
protected int defaultTimeoutMs;
protected final ExecutorService executor;
protected final HttpClient httpclient;
final IdleConnectionMonitorThread monitorThread;
private static ClientConnectionManager createConnectionManager() {
PoolingClientConnectionManager connectionManager = new PoolingClientConnectionManager();
connectionManager.setMaxTotal(VmApiProxyEnvironment.MAX_CONCURRENT_API_CALLS);
connectionManager.setDefaultMaxPerRoute(VmApiProxyEnvironment.MAX_CONCURRENT_API_CALLS);
return connectionManager;
}
public VmApiProxyDelegate() {
this(new DefaultHttpClient(createConnectionManager()));
}
VmApiProxyDelegate(HttpClient httpclient) {
this.defaultTimeoutMs = DEFAULT_RPC_TIMEOUT_MS;
this.executor = Executors.newCachedThreadPool();
this.httpclient = httpclient;
this.monitorThread = new IdleConnectionMonitorThread(httpclient.getConnectionManager());
this.monitorThread.start();
}
@Override
public byte[] makeSyncCall(
VmApiProxyEnvironment environment,
String packageName,
String methodName,
byte[] requestData)
throws ApiProxyException {
return makeSyncCallWithTimeout(environment, packageName, methodName, requestData,
defaultTimeoutMs);
}
private byte[] makeSyncCallWithTimeout(
VmApiProxyEnvironment environment,
String packageName,
String methodName,
byte[] requestData,
int timeoutMs)
throws ApiProxyException {
return makeApiCall(environment, packageName, methodName, requestData, timeoutMs, false);
}
private byte[] makeApiCall(VmApiProxyEnvironment environment,
String packageName,
String methodName,
byte[] requestData,
int timeoutMs,
boolean wasAsync) {
// If this was caused by an async call we need to return the pending call semaphore.
environment.apiCallStarted(VmRuntimeUtils.MAX_USER_API_CALL_WAIT_MS, wasAsync);
try {
return runSyncCall(environment, packageName, methodName, requestData, timeoutMs);
} finally {
environment.apiCallCompleted();
}
}
protected byte[] runSyncCall(VmApiProxyEnvironment environment, String packageName,
String methodName, byte[] requestData, int timeoutMs) {
HttpPost request = createRequest(environment, packageName, methodName, requestData, timeoutMs);
try {
// Create a new http context for each call as the default context is not thread safe.
BasicHttpContext context = new BasicHttpContext();
HttpResponse response = httpclient.execute(request, context);
// Check for HTTP error status and return early.
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
try (Scanner errorStreamScanner =
new Scanner(new BufferedInputStream(response.getEntity().getContent()));) {
logger.info("Error body: " + errorStreamScanner.useDelimiter("\\Z").next());
throw new RPCFailedException(packageName, methodName);
}
}
try (BufferedInputStream bis = new BufferedInputStream(response.getEntity().getContent())) {
RemoteApiPb.Response remoteResponse = new RemoteApiPb.Response();
if (!remoteResponse.parseFrom(bis)) {
logger.info(
"HTTP ApiProxy unable to parse response for " + packageName + "." + methodName);
throw new RPCFailedException(packageName, methodName);
}
// If the response contains an error, convert it to the expected api exception and throw.
if (remoteResponse.hasRpcError() || remoteResponse.hasApplicationError()) {
throw convertRemoteError(remoteResponse, packageName, methodName, logger);
}
// Success, return the response.
return remoteResponse.getResponseAsBytes();
}
} catch (IOException e) {
logger.info(
"HTTP ApiProxy I/O error for " + packageName + "." + methodName + ": " + e.getMessage());
throw constructApiException(packageName, methodName);
} finally {
request.releaseConnection();
}
}
// TODO(ludo) remove when the correct exceptions have public constructor.
private RuntimeException constructException(
String exceptionClassName, String message, String packageName, String methodName) {
try {
Class<?> c = Class.forName(exceptionClassName);
Constructor<?> constructor = c.getDeclaredConstructor(String.class);
constructor.setAccessible(true);
return (RuntimeException) constructor.newInstance(message);
} catch (Exception e) {
return new RPCFailedException(packageName, methodName);
}
}
RuntimeException constructApiException(String packageName, String methodName) {
String message = "RCP Failure for API call: " + packageName + " " + methodName;
switch (packageName) {
case "taskqueue":
return new TransientFailureException(message);
case "app_identity_service":
return new AppIdentityServiceFailureException(message);
case "blobstore":
return new BlobstoreFailureException(message);
case "channel":
return new ChannelFailureException(message);
case "images":
return new ImagesServiceFailureException(message);
case "logservice":
return constructException(
LogServiceException.class.getName(), message, packageName, methodName);
case "memcache":
return new MemcacheServiceException(message);
case "modules":
return constructException(
ModulesException.class.getName(), message, packageName, methodName);
case "search":
return new SearchException(message);
case "user":
return new UserServiceFailureException(message);
case "xmpp":
return new XMPPFailureException(message);
default:
// Cover all datastore versions:
if (packageName.startsWith("datastore")) {
return new DatastoreFailureException(message);
} else {
return new RPCFailedException(packageName, methodName);
}
}
}
/**
* Create an HTTP post request suitable for sending to the API server.
*
* @param environment The current VMApiProxyEnvironment
* @param packageName The API call package
* @param methodName The API call method
* @param requestData The POST payload.
* @param timeoutMs The timeout for this request
* @return an HttpPost object to send to the API.
*/
//
static HttpPost createRequest(VmApiProxyEnvironment environment, String packageName,
String methodName, byte[] requestData, int timeoutMs) {
// Wrap the payload in a RemoteApi Request.
RemoteApiPb.Request remoteRequest = new RemoteApiPb.Request();
remoteRequest.setServiceName(packageName);
remoteRequest.setMethod(methodName);
remoteRequest.setRequestId(environment.getTicket());
remoteRequest.setRequestAsBytes(requestData);
HttpPost request = new HttpPost("http://" + environment.getServer() + REQUEST_ENDPOINT);
request.setHeader(RPC_STUB_ID_HEADER, REQUEST_STUB_ID);
request.setHeader(RPC_METHOD_HEADER, REQUEST_STUB_METHOD);
// Set TCP connection timeouts.
HttpParams params = new BasicHttpParams();
params.setLongParameter(ConnManagerPNames.TIMEOUT,
timeoutMs + ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS);
params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,
timeoutMs + ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS);
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT,
timeoutMs + ADDITIONAL_HTTP_TIMEOUT_BUFFER_MS);
// Performance tweaks.
params.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY, Boolean.TRUE);
params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK, Boolean.FALSE);
request.setParams(params);
// The request deadline can be overwritten by the environment, read deadline if available.
Double deadline = (Double) (environment.getAttributes().get(API_DEADLINE_KEY));
if (deadline == null) {
request.setHeader(RPC_DEADLINE_HEADER,
Double.toString(TimeUnit.SECONDS.convert(timeoutMs, TimeUnit.MILLISECONDS)));
} else {
request.setHeader(RPC_DEADLINE_HEADER, Double.toString(deadline));
}
// If the incoming request has a dapper trace header: set it on outgoing API calls
// so they are tied to the original request.
Object dapperHeader = environment.getAttributes()
.get(VmApiProxyEnvironment.AttributeMapping.DAPPER_ID.attributeKey);
if (dapperHeader instanceof String) {
request.setHeader(
VmApiProxyEnvironment.AttributeMapping.DAPPER_ID.headerKey, (String) dapperHeader);
}
// If the incoming request has a Cloud trace header: set it on outgoing API calls
// so they are tied to the original request.
// TODO(user): For now, this uses the incoming span id - use the one from the active span.
Object traceHeader = environment.getAttributes()
.get(VmApiProxyEnvironment.AttributeMapping.CLOUD_TRACE_CONTEXT.attributeKey);
if (traceHeader instanceof String) {
request.setHeader(
VmApiProxyEnvironment.AttributeMapping.CLOUD_TRACE_CONTEXT.headerKey,
(String) traceHeader);
}
ByteArrayEntity postPayload = new ByteArrayEntity(remoteRequest.toByteArray(),
ContentType.APPLICATION_OCTET_STREAM);
postPayload.setChunked(false);
request.setEntity(postPayload);
return request;
}
/**
* Convert RemoteApiPb.Response errors to the appropriate exception.
*
* <p>The response must have exactly one of the RpcError and ApplicationError fields set.
*
* @param remoteResponse the Response
* @param packageName the name of the API package.
* @param methodName the name of the method within the API package.
* @param logger the Logger used to create log messages.
* @return ApiProxyException
*/
private static ApiProxyException convertRemoteError(RemoteApiPb.Response remoteResponse,
String packageName, String methodName, Logger logger) {
if (remoteResponse.hasRpcError()) {
return convertApiResponseRpcErrorToException(
remoteResponse.getRpcError(),
packageName,
methodName,
logger);
}
// Otherwise it's an application error
RemoteApiPb.ApplicationError error = remoteResponse.getApplicationError();
return new ApiProxy.ApplicationException(error.getCode(), error.getDetail());
}
/**
* Convert the RemoteApiPb.RpcError to the appropriate exception.
*
* @param rpcError the RemoteApiPb.RpcError.
* @param packageName the name of the API package.
* @param methodName the name of the method within the API package.
* @param logger the Logger used to create log messages.
* @return ApiProxyException
*/
private static ApiProxyException convertApiResponseRpcErrorToException(
RemoteApiPb.RpcError rpcError, String packageName, String methodName, Logger logger) {
int rpcCode = rpcError.getCode();
String errorDetail = rpcError.getDetail();
if (rpcCode > RemoteApiPb.RpcError.ErrorCode.values().length) {
logger.severe("Received unrecognized error code from server: " + rpcError.getCode() +
" details: " + errorDetail);
return new ApiProxy.UnknownException(packageName, methodName);
}
RemoteApiPb.RpcError.ErrorCode errorCode = RemoteApiPb.RpcError.ErrorCode.values()[
rpcError.getCode()];
logger.warning("RPC failed, API=" + packageName + "." + methodName + " : "
+ errorCode + " : " + errorDetail);
// This is very similar to apphosting/utils/runtime/ApiProxyUtils.java#convertApiError,
// which is for APIResponse. TODO(user): retire both in favor of gRPC.
switch (errorCode) {
case CALL_NOT_FOUND:
return new ApiProxy.CallNotFoundException(packageName, methodName);
case PARSE_ERROR:
return new ApiProxy.ArgumentException(packageName, methodName);
case SECURITY_VIOLATION:
logger.severe("Security violation: invalid request id used!");
return new ApiProxy.UnknownException(packageName, methodName);
case CAPABILITY_DISABLED:
return new ApiProxy.CapabilityDisabledException(
errorDetail, packageName, methodName);
case OVER_QUOTA:
return new ApiProxy.OverQuotaException(packageName, methodName);
case REQUEST_TOO_LARGE:
return new ApiProxy.RequestTooLargeException(packageName, methodName);
case RESPONSE_TOO_LARGE:
return new ApiProxy.ResponseTooLargeException(packageName, methodName);
case BAD_REQUEST:
return new ApiProxy.ArgumentException(packageName, methodName);
case CANCELLED:
return new ApiProxy.CancelledException(packageName, methodName);
case FEATURE_DISABLED:
return new ApiProxy.FeatureNotEnabledException(
errorDetail, packageName, methodName);
case DEADLINE_EXCEEDED:
return new ApiProxy.ApiDeadlineExceededException(packageName, methodName);
default:
return new ApiProxy.UnknownException(packageName, methodName);
}
}
private class MakeSyncCall implements Callable<byte[]> {
private final VmApiProxyDelegate delegate;
private final VmApiProxyEnvironment environment;
private final String packageName;
private final String methodName;
private final byte[] requestData;
private final int timeoutMs;
public MakeSyncCall(VmApiProxyDelegate delegate,
VmApiProxyEnvironment environment,
String packageName,
String methodName,
byte[] requestData,
int timeoutMs) {
this.delegate = delegate;
this.environment = environment;
this.packageName = packageName;
this.methodName = methodName;
this.requestData = requestData;
this.timeoutMs = timeoutMs;
}
@Override
public byte[] call() throws Exception {
return delegate.makeApiCall(environment,
packageName,
methodName,
requestData,
timeoutMs,
true);
}
}
@Override
public Future<byte[]> makeAsyncCall(
VmApiProxyEnvironment environment,
String packageName,
String methodName,
byte[] request,
ApiConfig apiConfig) {
int timeoutMs = defaultTimeoutMs;
if (apiConfig != null && apiConfig.getDeadlineInSeconds() != null) {
timeoutMs = (int) (apiConfig.getDeadlineInSeconds() * 1000);
}
environment.aSyncApiCallAdded(VmRuntimeUtils.MAX_USER_API_CALL_WAIT_MS);
return executor.submit(new MakeSyncCall(this, environment, packageName,
methodName, request, timeoutMs));
}
@Override
public void log(VmApiProxyEnvironment environment, LogRecord record) {
if (environment != null) {
environment.addLogRecord(record);
}
}
@Override
public void flushLogs(VmApiProxyEnvironment environment) {
if (environment != null) {
environment.flushLogs();
}
}
@Override
public List<Thread> getRequestThreads(VmApiProxyEnvironment environment) {
Object threadFactory =
environment.getAttributes().get(VmApiProxyEnvironment.REQUEST_THREAD_FACTORY_ATTR);
if (threadFactory != null && threadFactory instanceof VmRequestThreadFactory) {
return ((VmRequestThreadFactory) threadFactory).getRequestThreads();
}
logger.warning("Got a call to getRequestThreads() but no VmRequestThreadFactory is available");
return Lists.newLinkedList();
}
/**
* Simple connection watchdog verifying that our connections are alive. Any stale connections are
* cleared as well.
*/
class IdleConnectionMonitorThread extends Thread {
private final ClientConnectionManager connectionManager;
public IdleConnectionMonitorThread(ClientConnectionManager connectionManager) {
super("IdleApiConnectionMontorThread");
this.connectionManager = connectionManager;
this.setDaemon(false);
}
@Override
public void run() {
try {
while (true) {
// Close expired connections.
connectionManager.closeExpiredConnections();
// Close connections that have been idle longer than 60 sec.
connectionManager.closeIdleConnections(60, TimeUnit.SECONDS);
Thread.sleep(5000);
}
} catch (InterruptedException ex) {
// terminate
}
}
}
}