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

com.google.apphosting.runtime.jetty.http.JettyHttpHandler 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.jetty.http;

import static com.google.apphosting.runtime.RequestRunner.WAIT_FOR_USER_RUNNABLE_DEADLINE;

import com.google.appengine.api.ThreadManager;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.base.AppVersionKey;
import com.google.apphosting.base.protos.EmptyMessage;
import com.google.apphosting.base.protos.RuntimePb;
import com.google.apphosting.runtime.ApiProxyImpl;
import com.google.apphosting.runtime.AppEngineConstants;
import com.google.apphosting.runtime.AppVersion;
import com.google.apphosting.runtime.BackgroundRequestCoordinator;
import com.google.apphosting.runtime.LocalRpcContext;
import com.google.apphosting.runtime.RequestManager;
import com.google.apphosting.runtime.RequestRunner;
import com.google.apphosting.runtime.RequestRunner.EagerRunner;
import com.google.apphosting.runtime.ResponseAPIData;
import com.google.apphosting.runtime.ServletEngineAdapter;
import com.google.apphosting.runtime.jetty.AppInfoFactory;
import com.google.common.flogger.GoogleLogger;
import java.io.PrintWriter;
import java.io.StringWriter;
import java.time.Duration;
import java.util.concurrent.TimeoutException;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Request;
import org.eclipse.jetty.server.Response;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.util.Blocker;
import org.eclipse.jetty.util.Callback;

/**
 * This class replicates the behaviour of the {@link RequestRunner} for Requests which do not come
 * through RPC. It should be added as a {@link Handler} to the Jetty {@link Server} wrapping the
 * {@code AppEngineWebAppContext}.
 *
 * 

This uses the {@link RequestManager} to start any AppEngine state associated with this request * including the {@link ApiProxy.Environment} which it sets as a request attribute at {@link * AppEngineConstants#ENVIRONMENT_ATTR}. This request attribute is pulled out by {@code * ContextScopeListener}s installed by the {@code AppVersionHandlerFactory} implementations so that * the {@link ApiProxy.Environment} is available all threads which are used to handle the request. */ public class JettyHttpHandler extends Handler.Wrapper { private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); private final boolean passThroughPrivateHeaders; private final AppInfoFactory appInfoFactory; private final AppVersionKey appVersionKey; private final AppVersion appVersion; private final RequestManager requestManager; private final BackgroundRequestCoordinator coordinator; public JettyHttpHandler( ServletEngineAdapter.Config runtimeOptions, AppVersion appVersion, AppVersionKey appVersionKey, AppInfoFactory appInfoFactory) { this.passThroughPrivateHeaders = runtimeOptions.passThroughPrivateHeaders(); this.appInfoFactory = appInfoFactory; this.appVersionKey = appVersionKey; this.appVersion = appVersion; ApiProxyImpl apiProxyImpl = (ApiProxyImpl) ApiProxy.getDelegate(); coordinator = apiProxyImpl.getBackgroundRequestCoordinator(); requestManager = (RequestManager) apiProxyImpl.getRequestThreadManager(); } @Override public boolean handle(Request request, Response response, Callback callback) throws Exception { // This handler cannot be used with anything else which establishes an environment // (e.g. RpcConnection). assert (request.getAttribute(AppEngineConstants.ENVIRONMENT_ATTR) == null); JettyRequestAPIData genericRequest = new JettyRequestAPIData(request, appInfoFactory, passThroughPrivateHeaders); JettyResponseAPIData genericResponse = new JettyResponseAPIData(response); // Read time remaining in request from headers and pass value to LocalRpcContext for use in // reporting remaining time until deadline for API calls (see b/154745969) Duration timeRemaining = genericRequest.getTimeRemaining(); boolean handled; ThreadGroup currentThreadGroup = Thread.currentThread().getThreadGroup(); LocalRpcContext context = new LocalRpcContext<>(EmptyMessage.class, timeRemaining); RequestManager.RequestToken requestToken = requestManager.startRequest( appVersion, context, genericRequest, genericResponse, currentThreadGroup); // Set the environment as a request attribute, so it can be pulled out and set for async // threads. ApiProxy.Environment currentEnvironment = ApiProxy.getCurrentEnvironment(); request.setAttribute(AppEngineConstants.ENVIRONMENT_ATTR, currentEnvironment); try { handled = dispatchRequest(requestToken, genericRequest, genericResponse, callback); if (handled) { callback.succeeded(); } } catch ( @SuppressWarnings("InterruptedExceptionSwallowed") Throwable ex) { // Note we do intentionally swallow InterruptException. // We will report the exception via the rpc. We don't mark this thread as interrupted because // ThreadGroupPool would use that as a signal to remove the thread from the pool; we don't // need that. handled = handleException(ex, requestToken, genericResponse); Response.writeError(request, response, callback, ex); } finally { requestManager.finishRequest(requestToken); } // Do not put this in a final block. If we propagate an // exception the callback will be invoked automatically. genericResponse.finishWithResponse(context); // We don't want threads used for background requests to go back // in the thread pool, because users may have stashed references // to them or may be expecting them to exit. Setting the // interrupt bit causes the pool to drop them. if (genericRequest.getRequestType() == RuntimePb.UPRequest.RequestType.BACKGROUND) { Thread.currentThread().interrupt(); } return handled; } private boolean dispatchRequest( RequestManager.RequestToken requestToken, JettyRequestAPIData request, JettyResponseAPIData response, Callback callback) throws Throwable { switch (request.getRequestType()) { case SHUTDOWN: logger.atInfo().log("Shutting down requests"); requestManager.shutdownRequests(requestToken); return true; case BACKGROUND: dispatchBackgroundRequest(request, response); return true; case OTHER: return dispatchServletRequest(request, response, callback); default: throw new IllegalStateException(request.getRequestType().toString()); } } private boolean dispatchServletRequest( JettyRequestAPIData request, JettyResponseAPIData response, Callback callback) throws Throwable { Request jettyRequest = request.getWrappedRequest(); Response jettyResponse = response.getWrappedResponse(); jettyRequest.setAttribute(AppEngineConstants.APP_VERSION_KEY_REQUEST_ATTR, appVersionKey); // Environment is set in a request attribute which is set/unset for async threads by // a ContextScopeListener created inside the AppVersionHandlerFactory. try (Blocker.Callback cb = Blocker.callback()) { boolean handle = super.handle(jettyRequest, jettyResponse, cb); cb.block(); return handle; } } private void dispatchBackgroundRequest(JettyRequestAPIData request, JettyResponseAPIData response) throws InterruptedException, TimeoutException { String requestId = getBackgroundRequestId(request); // The interface of coordinator.waitForUserRunnable() requires us to provide the app code with a // working thread *in the same exchange* where we get the runnable the user wants to run in the // thread. This prevents us from actually directly feeding that runnable to the thread. To work // around this conundrum, we create an EagerRunner, which lets us start running the thread // without knowing yet what we want to run. // Create an ordinary request thread as a child of this background thread. EagerRunner eagerRunner = new EagerRunner(); Thread thread = ThreadManager.createThreadForCurrentRequest(eagerRunner); // Give this thread to the app code and get its desired runnable in response: Runnable runnable = coordinator.waitForUserRunnable( requestId, thread, WAIT_FOR_USER_RUNNABLE_DEADLINE.toMillis()); // Finally, hand that runnable to the thread so it can actually start working. // This will block until Thread.start() is called by the app code. This is by design: we must // not exit this request handler until the thread has started *and* completed, otherwise the // serving infrastructure will cancel our ability to make API calls. We're effectively "holding // open the door" on the spawned thread's ability to make App Engine API calls. // Now set the context class loader to the UserClassLoader for the application // and pass control to the Runnable the user provided. ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); Thread.currentThread().setContextClassLoader(appVersion.getClassLoader()); try { eagerRunner.supplyRunnable(runnable); } finally { Thread.currentThread().setContextClassLoader(oldClassLoader); } // Wait for the thread to end: thread.join(); } private boolean handleException( Throwable ex, RequestManager.RequestToken requestToken, ResponseAPIData response) { // Unwrap ServletException, either from javax or from jakarta exception: try { java.lang.reflect.Method getRootCause = ex.getClass().getMethod("getRootCause"); Object rootCause = getRootCause.invoke(ex); if (rootCause != null) { ex = (Throwable) rootCause; } } catch (Throwable ignore) { } String msg = "Uncaught exception from servlet"; logger.atWarning().withCause(ex).log("%s", msg); // Don't use ApiProxy here, because we don't know what state the // environment/delegate are in. requestToken.addAppLogMessage(ApiProxy.LogRecord.Level.fatal, formatLogLine(msg, ex)); if (shouldKillCloneAfterException(ex)) { logger.atSevere().log("Detected a dangerous exception, shutting down clone nicely."); response.setTerminateClone(true); } RuntimePb.UPResponse.ERROR error = RuntimePb.UPResponse.ERROR.APP_FAILURE; setFailure(response, error, "Unexpected exception from servlet: " + ex); return true; } /** Create a failure response from the given code and message. */ public static void setFailure( ResponseAPIData response, RuntimePb.UPResponse.ERROR error, String message) { logger.atWarning().log("Runtime failed: %s, %s", error, message); // If the response is already set, use that -- it's probably more // specific (e.g. THREADS_STILL_RUNNING). if (response.getError() == RuntimePb.UPResponse.ERROR.OK_VALUE) { response.error(error.getNumber(), message); } } private String formatLogLine(String message, Throwable ex) { StringWriter stringWriter = new StringWriter(); PrintWriter printWriter = new PrintWriter(stringWriter); printWriter.println(message); ex.printStackTrace(printWriter); return stringWriter.toString(); } public static boolean shouldKillCloneAfterException(Throwable th) { while (th != null) { if (th instanceof OutOfMemoryError) { return true; } try { Throwable[] suppressed = th.getSuppressed(); if (suppressed != null) { for (Throwable s : suppressed) { if (shouldKillCloneAfterException(s)) { return true; } } } } catch (OutOfMemoryError ex) { return true; } // TODO: Consider checking for other subclasses of // VirtualMachineError, but probably not StackOverflowError. th = th.getCause(); } return false; } private String getBackgroundRequestId(JettyRequestAPIData upRequest) { String backgroundRequestId = upRequest.getBackgroundRequestId(); if (backgroundRequestId == null) { throw new IllegalArgumentException("Did not receive a background request identifier."); } return backgroundRequestId; } }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy