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

com.google.appengine.tools.development.InstanceHelper 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.appengine.tools.development.AbstractContainerService.LocalInitializationEnvironment;
import com.google.appengine.tools.development.InstanceStateHolder.InstanceState;
import com.google.apphosting.api.ApiProxy;
import com.google.apphosting.api.ApiProxy.Environment;
import com.google.apphosting.utils.config.AppEngineWebXml;
import com.google.apphosting.utils.config.WebModule;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.google.appengine.repackaged.org.apache.http.HttpResponse;
import com.google.appengine.repackaged.org.apache.http.client.HttpClient;
import com.google.appengine.repackaged.org.apache.http.client.config.RequestConfig;
import com.google.appengine.repackaged.org.apache.http.client.methods.HttpGet;
import com.google.appengine.repackaged.org.apache.http.impl.client.HttpClients;

/**
 * Utility functions to access a server instance.
 */
public class InstanceHelper {
  // This should be kept in sync with
  // com.google.apphosting.utils.jetty.DevAppEngineWebAppContext
  private static final String X_GOOGLE_DEV_APPSERVER_SKIPADMINCHECK =
      "X-Google-DevAppserver-SkipAdminCheck";
  private static final int AH_REQUEST_INFINITE_TIMEOUT = 0;
  private static final int AH_REQUEST_DEFAULT_TIMEOUT = 30 * 1000;

  private static final Logger LOGGER = Logger.getLogger(InstanceHelper.class.getName());

  private final String serverOrBackendName;
  private final int instance;
  private final InstanceStateHolder instanceStateHolder;
  private final ContainerService containerService;

  /**
   * Constructs an {@link InstanceHelper}.
   *
   * @param serverOrBackendName For server instances the server name and for backend instances the
   *     backend name.
   * @param instance The instance number or -1 for load balancing servers and automatic servers.
   * @param instanceStateHolder Holder for the instances state.
   * @param containerService The container service for the instance.
   */
  InstanceHelper(String serverOrBackendName, int instance, InstanceStateHolder instanceStateHolder,
      ContainerService containerService) {
    this.serverOrBackendName = serverOrBackendName;
    this.instance = instance;
    this.instanceStateHolder = instanceStateHolder;
    this.containerService = containerService;
  }

  /**
   * Triggers an HTTP GET to /_ah/start in a background thread
   *
   * 

This method will keep on trying until it receives a non-error response code from the server. * * @param runOnSuccess {@link Runnable#run} invoked when the startup request succeeds. */ public void sendStartRequest(final Runnable runOnSuccess) { if (LOGGER.isLoggable(Level.FINER)) { LOGGER.log(Level.FINER, "Entering send start request for serverOrBackendName=" + serverOrBackendName + " instance=" + instance, new Exception("Start sendStartRequest")); } if (instance < 0) { throw new IllegalStateException("Attempt to send a start request to server/backend " + serverOrBackendName + " instance " + instance); } InstanceState unchangedState = instanceStateHolder.testAndSetIf(InstanceState.RUNNING_START_REQUEST, InstanceState.SLEEPING); if (unchangedState == null) { // We take this branch if the instance was in the InstanceState.SLEEPING // state and we have updated it. In this case we sent the startup message. // We send the startup request to the servlet in a separate thread to not // block the main thread and to allow multiple servers to run the // /_ah/start operation in parallel. Also, note a server can loop forever // on /_ah/start Thread requestThread = new Thread(new Runnable() { @Override public void run() { sendStartRequest(AH_REQUEST_INFINITE_TIMEOUT, runOnSuccess); } }); requestThread.setDaemon(true); requestThread.setName( "BackendServersStartRequestThread." + instance + "." + serverOrBackendName); requestThread.start(); } else if (unchangedState != InstanceState.RUNNING_START_REQUEST && unchangedState != InstanceState.RUNNING) { // We take this branch if the instance was in an invalid state. // // In the case some other thread is sending or has sent the startup // request we do not send another but do not report an error as this // is an expected situation. InstanceStateHolder.reportInvalidStateChange(serverOrBackendName, instance, unchangedState, InstanceState.RUNNING_START_REQUEST, InstanceState.SLEEPING); } } /** * Triggers an HTTP GET to /_ah/start * * This method will keep on trying until it receives a non-error response * code from the server. * * @param timeoutInMs Timeout in milliseconds, 0 indicates no timeout. * @param runOnSuccess {@link Runnable#run} invoked when the startup request succeeds. */ private void sendStartRequest(int timeoutInMs, Runnable runOnSuccess) { try { // use http as http is used in prod String urlString = String.format("http://%s:%d/_ah/start", containerService.getAddress(), containerService.getPort()); LOGGER.finer("sending start request to: " + urlString); // need to use HttpClient as the URL stream handler is using the // URLFetch API RequestConfig requestConfig = RequestConfig.custom() .setConnectTimeout(timeoutInMs) .setSocketTimeout(timeoutInMs) .setConnectionRequestTimeout(timeoutInMs) .build(); HttpClient httpClient = HttpClients.custom().setDefaultRequestConfig(requestConfig).build(); HttpGet request = new HttpGet(urlString); // set the magic header that tells the dev appserver to skip // authentication - this lets us hit protected urls request.addHeader(X_GOOGLE_DEV_APPSERVER_SKIPADMINCHECK, "true"); try { HttpResponse response = httpClient.execute(request); int returnCode = response.getStatusLine().getStatusCode(); byte[] buffer = new byte[1024]; InputStream in = response.getEntity().getContent(); while (in.read(buffer) != -1) { // Servers looping forever on /_ah/start could // potentially generate a lot of data. Grab it in small batches and // drop to make sure we don't end up loading it into ram. } if ((returnCode >= 200 && returnCode < 300) || returnCode == 404) { LOGGER.fine( String.format("backend server %d.%s request to /_ah/start completed, code=%d", instance, serverOrBackendName, returnCode)); instanceStateHolder.testAndSet(InstanceState.RUNNING, InstanceState.RUNNING_START_REQUEST); runOnSuccess.run(); } else { LOGGER.warning("Start request to /_ah/start on server " + instance + "." + serverOrBackendName + " failed (HTTP status=" + response.getStatusLine() + "). Retrying..."); // in prod the server will retry start requests until a 2xx or 404 // return code is received. Sleep here to rate limit retries. Thread.sleep(1000); sendStartRequest(timeoutInMs, runOnSuccess); } } finally { request.releaseConnection(); } } catch (MalformedURLException e) { LOGGER.severe(String.format( "Unable to send start request to server: %d.%s, " + "MalformedURLException: %s", instance, serverOrBackendName, e.getMessage())); } catch (Exception e) { LOGGER.warning(String.format( "Got exception while performing /_ah/start " + "request on server: %d.%s, %s: %s", instance, serverOrBackendName, e.getClass().getName(), e.getMessage())); } } /** * This method will trigger any shutdown hooks registered with the current * server. * * Some class loader trickery is required to make sure that we get the * {@link com.google.appengine.api.LifecycleManager} responsible for this * server instance. */ private void triggerLifecycleShutdownHookImpl() { // store the current environment Environment prevEnvironment = ApiProxy.getCurrentEnvironment(); try { // get the classloader used by user servlets ClassLoader serverClassLoader = containerService.getAppContext().getClassLoader(); // Get hold of the LifeCycleManager instance Class lifeCycleManagerClass = Class.forName("com.google.appengine.api.LifecycleManager", true, serverClassLoader); Method lifeCycleManagerGetter = lifeCycleManagerClass.getMethod("getInstance"); Object userThreadLifeCycleManager = lifeCycleManagerGetter.invoke(null, new Object[0]); // get the shutdown method with the specified max time (not enforced), Method beginShutdown = lifeCycleManagerClass.getMethod("beginShutdown", long.class); // we need to set the environment for it to be able to get the version AppEngineWebXml appEngineWebXml = containerService.getAppEngineWebXmlConfig(); // For the LocalInitializationEnvironment we need the actual server name. For backends // this will be 'default'. Confusingly a serverOrBackendName is NOT a server name in the case // of a backend but an id from backends.xml which is a major version in prod but not yet in // dev. String moduleName = WebModule.getModuleName(appEngineWebXml); // TODO: Set version to backend name after confirming with ludo@ // for backends call DevAppServerModulesFilter.injectBackendServiceCurrentApiInfo. ApiProxy.setEnvironmentForCurrentThread( new LocalInitializationEnvironment( appEngineWebXml.getAppId(), moduleName, appEngineWebXml.getMajorVersionId(), instance, containerService.getPort())); // do the shutdown, catch any exceptions in the user supplied code. // beginShutdown is blocking and will return when the user supplied // shutdown hook completes try { beginShutdown.invoke(userThreadLifeCycleManager, AH_REQUEST_DEFAULT_TIMEOUT); } catch (Exception e) { LOGGER.warning( String.format("got exception when running shutdown hook on server %d.%s", instance, serverOrBackendName)); // print the stack trace to help the user debug e.printStackTrace(); } } catch (Exception e) { LOGGER.severe( String.format("Exception during reflective call to " + "LifecycleManager.beginShutdown on server %d.%s, got %s: %s", instance, serverOrBackendName, e.getClass().getName(), e.getMessage())); } finally { // restore the environment ApiProxy.setEnvironmentForCurrentThread(prevEnvironment); } } /** * Shut down the server. * *

Will trigger any shutdown hooks installed by the {@link * com.google.appengine.api.LifecycleManager} * * @throws Exception */ public void shutdown() throws Exception { synchronized (instanceStateHolder) { // TODO: This calls user code, can we do this outside the synchronized block. if (instanceStateHolder.test(InstanceState.RUNNING, InstanceState.RUNNING_START_REQUEST)) { triggerLifecycleShutdownHookImpl(); } containerService.shutdown(); instanceStateHolder.set(InstanceState.SHUTDOWN); } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy