App Engine SDK 1.8.4 release.
[gae.git] / java / src / main / com / google / appengine / tools / development / AbstractBackendServers.java
blob27e8736eb7f716a6e3f2510c2a020374ff55ca23
1 // Copyright 2011 Google Inc. All Rights Reserved.
3 package com.google.appengine.tools.development;
5 import com.google.appengine.api.backends.BackendService;
6 import com.google.appengine.api.backends.dev.LocalServerController;
7 import com.google.appengine.tools.development.AbstractContainerService.PortMappingProvider;
8 import com.google.appengine.tools.development.ApplicationConfigurationManager.ModuleConfigurationHandle;
9 import com.google.appengine.tools.development.InstanceStateHolder.InstanceState;
10 import com.google.apphosting.api.ApiProxy;
11 import com.google.apphosting.utils.config.BackendsXml;
12 import com.google.common.collect.ImmutableMap;
13 import com.google.common.collect.Maps;
15 import java.io.File;
16 import java.io.IOException;
17 import java.util.HashMap;
18 import java.util.List;
19 import java.util.Map;
20 import java.util.Map.Entry;
21 import java.util.TreeMap;
22 import java.util.concurrent.Semaphore;
23 import java.util.concurrent.TimeUnit;
24 import java.util.logging.Logger;
26 import javax.servlet.ServletException;
27 import javax.servlet.http.HttpServletRequest;
28 import javax.servlet.http.HttpServletResponse;
30 /**
31 * Controls backend servers configured in appengine-web.xml. Each server is
32 * started on a separate port. All servers run the same code as the main app.
36 public abstract class AbstractBackendServers implements BackendContainer,
37 LocalServerController, PortMappingProvider {
38 public static final String SYSTEM_PROPERTY_STATIC_PORT_NUM_PREFIX =
39 "com.google.appengine.devappserver.";
41 private static final Integer DEFAULT_INSTANCES = 1;
42 private static final String DEFAULT_INSTANCE_CLASS = "B1";
43 private static final Integer DEFAULT_MAX_CONCURRENT_REQUESTS = 10;
45 private static final int MAX_PENDING_QUEUE_LENGTH = 20;
46 private static final int MAX_PENDING_QUEUE_TIME_MS = 10 * 1000;
47 private static final int MAX_START_QUEUE_TIME_MS = 30 * 1000;
49 private String address;
50 private ModuleConfigurationHandle moduleConfigurationHandle;
51 private File externalResourceDir;
52 private Map<String, Object> containerConfigProperties;
53 private Map<AbstractBackendServers.ServerInstanceEntry, ServerWrapper> backendServers =
54 ImmutableMap.copyOf(new HashMap<ServerInstanceEntry, ServerWrapper>());
55 private Map<String, String> portMapping =
56 ImmutableMap.copyOf(new HashMap<String, String>());
57 protected Logger logger = Logger.getLogger(AbstractBackendServers.class.getName());
59 private Map<String, String> serviceProperties = new HashMap<String, String>();
61 private DevAppServer devAppServer;
63 @Override
64 public void init(String address, ModuleConfigurationHandle moduleConfigurationHandle,
65 File externalResourceDirectory, Map<String, Object> containerConfigProperties,
66 DevAppServer devAppServer) {
67 this.moduleConfigurationHandle = moduleConfigurationHandle;
68 this.externalResourceDir = externalResourceDirectory;
69 this.address = address;
70 this.containerConfigProperties = containerConfigProperties;
71 this.devAppServer = devAppServer;
74 @Override
75 public void setServiceProperties(Map<String, String> properties) {
76 this.serviceProperties = properties;
79 @Override
80 public void shutdownAll() throws Exception {
81 for (ServerWrapper server : backendServers.values()) {
82 logger.finer("server shutdown: " + server);
83 server.shutdown();
85 backendServers = ImmutableMap.copyOf(new HashMap<ServerInstanceEntry, ServerWrapper>());
88 @Override
89 public TreeMap<String, BackendStateInfo> getBackendState(String requestHostName) {
90 TreeMap<String, BackendStateInfo> serverInfoMap = new TreeMap<String, BackendStateInfo>();
91 for (ServerWrapper serverWrapper : backendServers.values()) {
92 String name = serverWrapper.serverEntry.getName();
94 String listenAddress;
95 if (requestHostName == null) {
96 listenAddress = portMapping.get(serverWrapper.getDnsPrefix());
97 } else {
98 listenAddress = requestHostName + ":" + serverWrapper.port;
101 BackendStateInfo ssi = serverInfoMap.get(name);
102 if (ssi == null) {
103 ssi = new BackendStateInfo(serverWrapper.serverEntry);
104 serverInfoMap.put(name, ssi);
106 if (serverWrapper.isLoadBalanceServer()) {
107 ssi.setState(serverWrapper.getStateHolder().getDisplayName());
108 ssi.setAddress(listenAddress);
109 } else {
110 ssi.add(new InstanceStateInfo(serverWrapper.serverInstance, listenAddress,
111 serverWrapper.getStateHolder().getDisplayName()));
114 return serverInfoMap;
117 @Override
118 public synchronized void startBackend(String serverToStart) throws IllegalStateException {
119 if (!checkServerExists(serverToStart)) {
120 String message = String.format("Tried to start unknown server %s", serverToStart);
121 logger.warning(message);
122 throw new IllegalStateException(message);
124 for (ServerWrapper server : backendServers.values()) {
125 if (server.getName().equals(serverToStart)) {
126 if (!server.getStateHolder().test(InstanceState.STOPPED)) {
127 continue;
129 if (server.isLoadBalanceServer()) {
130 server.getStateHolder().testAndSet(InstanceState.RUNNING,
131 InstanceState.STOPPED);
132 continue;
134 server.getStateHolder().testAndSet(InstanceState.SLEEPING,
135 InstanceState.STOPPED);
136 server.sendStartRequest();
141 @Override
142 public synchronized void stopBackend(String serverToStop) throws Exception {
143 if (!checkServerExists(serverToStop)) {
144 String message = String.format("Tried to stop unknown server %s", serverToStop);
145 logger.warning(message);
146 throw new IllegalStateException(message);
148 for (ServerWrapper server : backendServers.values()) {
149 if (server.getName().equals(serverToStop)) {
150 if (server.getStateHolder().test(InstanceState.STOPPED)) {
151 continue;
153 if (server.isLoadBalanceServer()) {
154 server.getStateHolder().testAndSet(InstanceState.STOPPED,
155 InstanceState.RUNNING);
156 continue;
158 logger.fine("Stopping server: " + server.getDnsPrefix());
159 server.shutdown();
160 server.createConnection();
161 server.startup(true);
166 public void configureAll(ApiProxyLocal local) throws Exception {
167 BackendsXml backendsXml = moduleConfigurationHandle.getBackendsXml();
168 if (backendsXml == null) {
169 logger.fine("Got null backendsXml config.");
170 return;
172 List<BackendsXml.Entry> servers = backendsXml.getBackends();
173 if (servers.size() == 0) {
174 logger.fine("No backends configured.");
175 return;
178 if (backendServers.size() != 0) {
179 throw new Exception("Tried to start backend servers but some are already running.");
181 logger.finer("Found " + servers.size() + " configured backends.");
183 Map<AbstractBackendServers.ServerInstanceEntry, ServerWrapper> serverMap = Maps.newHashMap();
184 for (BackendsXml.Entry entry : servers) {
185 entry = resolveDefaults(entry);
187 for (int serverInstance = -1; serverInstance < entry.getInstances(); serverInstance++) {
188 int port = checkForStaticPort(entry.getName(), serverInstance);
189 ServerWrapper serverWrapper =
190 new ServerWrapper(ContainerUtils.loadContainer(), entry, serverInstance, port);
191 serverMap.put(new ServerInstanceEntry(entry.getName(), serverInstance), serverWrapper);
194 this.backendServers = ImmutableMap.copyOf(serverMap);
196 String prettyAddress = address;
197 if ("0.0.0.0".equals(address)) {
198 prettyAddress = "127.0.0.1";
201 Map<String, String> portMap = Maps.newHashMap();
202 for (ServerWrapper serverWrapper : backendServers.values()) {
203 logger.finer(
204 "starting server: " + serverWrapper.serverInstance + "." + serverWrapper.getName()
205 + " on " + address + ":" + serverWrapper.port);
206 ApiProxy.Delegate<?> configured = ApiProxy.getDelegate();
207 try {
208 ApiProxy.setDelegate(local);
209 serverWrapper.createConnection();
210 } finally {
211 ApiProxy.setDelegate(configured);
214 portMap.put(serverWrapper.getDnsPrefix(), prettyAddress + ":" + serverWrapper.port);
216 this.portMapping = ImmutableMap.copyOf(portMap);
219 @Override
220 public void startupAll(ApiProxyLocal local) throws Exception {
221 for (ServerWrapper serverWrapper : backendServers.values()) {
222 logger.finer(
223 "starting server: " + serverWrapper.serverInstance + "." + serverWrapper.getName()
224 + " on " + address + ":" + serverWrapper.port);
225 ApiProxy.Delegate<?> configured = ApiProxy.getDelegate();
226 try {
227 ApiProxy.setDelegate(local);
228 serverWrapper.startup(false);
229 } finally {
230 ApiProxy.setDelegate(configured);
234 for (ServerWrapper serverWrapper : backendServers.values()) {
235 if (serverWrapper.isLoadBalanceServer()) {
236 continue;
238 serverWrapper.sendStartRequest();
242 private BackendsXml.Entry resolveDefaults(BackendsXml.Entry entry) {
243 return new BackendsXml.Entry(
244 entry.getName(),
245 entry.getInstances() == null ? DEFAULT_INSTANCES : entry.getInstances(),
246 entry.getInstanceClass() == null ? DEFAULT_INSTANCE_CLASS : entry.getInstanceClass(),
247 entry.getMaxConcurrentRequests() == null ? DEFAULT_MAX_CONCURRENT_REQUESTS :
248 entry.getMaxConcurrentRequests(),
249 entry.getOptions(),
250 entry.getState() == null ? BackendsXml.State.STOP : entry.getState());
254 * Forward a request to a specific server and instance. This will call the
255 * specified instance request dispatcher so the request is handled in the
256 * right server context.
258 void forwardToServer(String requestedServer, int instance, HttpServletRequest hrequest,
259 HttpServletResponse hresponse) throws IOException, ServletException {
260 ServerWrapper server = getServerWrapper(requestedServer, instance);
261 logger.finest("forwarding request to server: " + server);
262 server.getContainer().forwardToServer(hrequest, hresponse);
266 * This method guards access to servers to limit the number of concurrent
267 * requests. Each request running on a server must acquire a serving permit.
268 * If no permits are available a 500 response should be sent.
270 * @param serverName The server for which to acquire a permit.
271 * @param instanceNumber The server instance for which to acquire a permit.
272 * @param allowQueueOnBackends If set to false the method will return
273 * instantly, if set to true (and the specified server allows pending
274 * queues) this method can block for up to 10 s waiting for a serving
275 * permit to become available.
276 * @return true if a permit was acquired, false otherwise
278 public boolean acquireServingPermit(
279 String serverName, int instanceNumber, boolean allowQueueOnBackends) {
280 logger.finest(
281 String.format("trying to get serving permit for server %d.%s", instanceNumber, serverName));
282 try {
283 ServerWrapper server = getServerWrapper(serverName, instanceNumber);
284 int maxQueueTime = 0;
286 synchronized (server.getStateHolder()) {
287 if (!server.getStateHolder().acceptsConnections()) {
288 logger.finest(server + ": got request but server is not in a serving state");
289 return false;
291 if (server.getApproximateQueueLength() > MAX_PENDING_QUEUE_LENGTH) {
292 logger.finest(server + ": server queue is full");
293 return false;
295 if (server.getStateHolder().test(InstanceState.SLEEPING)) {
296 logger.finest(server + ": waking up sleeping server");
297 server.sendStartRequest();
300 if (server.getStateHolder().test(InstanceState.RUNNING_START_REQUEST)) {
301 maxQueueTime = MAX_START_QUEUE_TIME_MS;
302 } else if (allowQueueOnBackends && server.getMaxPendingQueueSize() > 0) {
303 maxQueueTime = MAX_PENDING_QUEUE_TIME_MS;
307 boolean gotPermit = server.acquireServingPermit(maxQueueTime);
308 logger.finest(server + ": tried to get server permit, timeout=" + maxQueueTime + " success="
309 + gotPermit);
310 return gotPermit;
311 } catch (InterruptedException e) {
312 logger.finest(
313 instanceNumber + "." + serverName + ": got interrupted while waiting for serving permit");
314 return false;
319 * Reserves an instance for this request. For workers this method will return
320 * -1 if no free instances are available. For backends this method will assign
321 * this request to the instance with the shortest queue and block until that
322 * instance is ready to serve the request.
324 * @param requestedServer Name of the server the request is to.
325 * @return the instance id of an available server instance, or -1 if no
326 * instance is available.
328 public int getAndReserveFreeInstance(String requestedServer) {
329 logger.finest("trying to get serving permit for server " + requestedServer);
331 ServerWrapper server = getServerWrapper(requestedServer, -1);
332 if (server == null) {
333 return -1;
335 if (!server.getStateHolder().acceptsConnections()) {
336 return -1;
338 int instanceNum = server.getInstances();
339 for (int i = 0; i < instanceNum; i++) {
340 if (acquireServingPermit(requestedServer, i, false)) {
341 return i;
344 if (server.getMaxPendingQueueSize() > 0) {
345 return addToShortestInstanceQueue(requestedServer);
346 } else {
347 logger.finest("no servers free");
348 return -1;
353 * Will add this request to the queue of the instance with the approximate
354 * shortest queue. This method will block for up to 10 seconds until a permit
355 * is received.
357 * @param requestedServer the server name
358 * @return the instance where the serving permit was reserved, or -1 if all
359 * instance queues are full
361 int addToShortestInstanceQueue(String requestedServer) {
362 logger.finest(requestedServer + ": no instances free, trying to find a queue");
363 int shortestQueue = MAX_PENDING_QUEUE_LENGTH;
364 ServerWrapper instanceWithShortestQueue = null;
365 for (ServerWrapper server : backendServers.values()) {
366 if (!server.getStateHolder().acceptsConnections()) {
367 continue;
369 int serverQueue = server.getApproximateQueueLength();
370 if (shortestQueue > serverQueue) {
371 instanceWithShortestQueue = server;
372 shortestQueue = serverQueue;
376 try {
377 if (shortestQueue < MAX_PENDING_QUEUE_LENGTH) {
378 logger.finest("adding request to queue on instance: " + instanceWithShortestQueue);
379 if (instanceWithShortestQueue.acquireServingPermit(MAX_PENDING_QUEUE_TIME_MS)) {
380 logger.finest("ready to serve request on instance: " + instanceWithShortestQueue);
381 return instanceWithShortestQueue.serverInstance;
384 } catch (InterruptedException e) {
385 logger.finer("interupted while queued at server " + instanceWithShortestQueue);
387 return -1;
391 * Method for returning a serving permit after a request has completed.
393 * @param serverName The server name
394 * @param instance The server instance
396 public void returnServingPermit(String serverName, int instance) {
397 ServerWrapper server = getServerWrapper(serverName, instance);
398 server.releaseServingPermit();
402 * Returns the port for the requested instance.
404 public int getPort(String serverName, int instance) {
405 ServerWrapper server = getServerWrapper(serverName, instance);
406 return server.port;
410 * Verifies if a specific server/instance is configured.
412 * @param serverName The server name
413 * @param instance The server instance
414 * @return true if the server/instance is configured, false otherwise.
416 public boolean checkInstanceExists(String serverName, int instance) {
417 return getServerWrapper(serverName, instance) != null;
421 * Verifies if a specific server is configured.
423 * @param serverName The server name
424 * @return true if the server is configured, false otherwise.
426 public boolean checkServerExists(String serverName) {
427 return checkInstanceExists(serverName, -1);
431 * Verifies if a specific server is stopped.
433 * @param serverName The server name
434 * @return true if the server is stopped, false otherwise.
436 public boolean checkServerStopped(String serverName) {
437 return checkInstanceStopped(serverName, -1);
441 * Verifies if a specific server/instance is stopped.
443 * @param serverName The server name
444 * @param instance The server instance
445 * @return true if the server/instance is stopped, false otherwise.
447 public boolean checkInstanceStopped(String serverName, int instance) {
448 return !getServerWrapper(serverName, instance)
449 .getStateHolder().acceptsConnections();
453 * Allows the servers API to get the current mapping from server and instance
454 * to listening ports.
456 * @return The port map
458 @Override
459 public Map<String, String> getPortMapping() {
460 return this.portMapping;
464 * Returns the server instance serving on a specific local port
466 * @param port the local tcp port that received the request
467 * @return the server instance, or -1 if no server instance is running on that
468 * port
470 public int getServerInstanceFromPort(int port) {
471 ServerWrapper server = getServerWrapperFromPort(port);
472 if (server != null) {
473 return server.serverInstance;
474 } else {
475 return -1;
480 * Returns the server serving on a specific local port
482 * @param port the local tcp port that received the request
483 * @return the server name, or null if no server instance is running on that
484 * port
486 public String getServerNameFromPort(int port) {
487 ServerWrapper server = getServerWrapperFromPort(port);
488 if (server != null) {
489 return server.getName();
490 } else {
491 return null;
496 * Convenience method for getting the ServerWrapper running on a specific port
498 private ServerWrapper getServerWrapperFromPort(int port) {
499 for (Entry<ServerInstanceEntry, ServerWrapper> entry : backendServers.entrySet()) {
500 if (entry.getValue().port == port) {
501 return entry.getValue();
504 return null;
508 * Convenience method for getting the ServerWrapper for a specific
509 * server/instance
511 private ServerWrapper getServerWrapper(String serverName, int instanceNumber) {
512 return backendServers.get(new ServerInstanceEntry(serverName, instanceNumber));
516 * Verifies if the specific port is statically configured, if not it will
517 * return 0 which instructs jetty to pick the port
519 * Ports can be statically configured by a system property at
520 * com.google.appengine.server.<server-name>.port for the server and
521 * com.google.appengine.server.<server-name>.<instance-id>.port for individual
522 * instances
524 * @param server the name of the configured server
525 * @param instance the instance number to configure
526 * @return the statically configured port, or 0 if none is configured
528 private int checkForStaticPort(String server, int instance) {
529 StringBuilder key = new StringBuilder();
530 key.append(SYSTEM_PROPERTY_STATIC_PORT_NUM_PREFIX);
531 key.append(server);
532 if (instance >= 0) {
533 key.append("." + instance);
535 key.append(".port");
536 String configuredPort = serviceProperties.get(key.toString());
537 if (configuredPort != null) {
538 return Integer.parseInt(configuredPort);
539 } else {
540 return 0;
545 * Class that allows the key in the server map to be the
546 * (servername,instanceid) tuple. Overrides equals() and hashcode() to
547 * function as a hashtable key
550 static class ServerInstanceEntry {
551 private final int instanceNumber;
552 private final String serverName;
555 * @param serverName
556 * @param instanceNumber
558 public ServerInstanceEntry(String serverName, int instanceNumber) {
559 this.serverName = serverName;
560 this.instanceNumber = instanceNumber;
563 @Override
564 public boolean equals(Object o) {
565 if (!(o instanceof ServerInstanceEntry)) {
566 return false;
569 ServerInstanceEntry that = (ServerInstanceEntry) o;
570 if (this.serverName != null) {
571 if (!this.serverName.equals(that.serverName)) {
572 return false;
574 } else {
575 if (that.serverName != null) {
576 return false;
580 if (this.instanceNumber != that.instanceNumber) {
581 return false;
583 return true;
586 @Override
587 public int hashCode() {
588 int hash = 17;
589 hash = 31 * hash + instanceNumber;
590 if (serverName != null) {
591 hash = 31 * hash + serverName.hashCode();
593 return hash;
596 @Override
597 public String toString() {
598 return instanceNumber + "." + serverName;
603 * Wraps a container service and contains extra information such as the
604 * instanceid of the current container as well as the port number it is
605 * running on.
607 private class ServerWrapper {
609 private final ContainerService container;
610 private final int serverInstance;
611 private int port;
612 private final BackendsXml.Entry serverEntry;
614 private final InstanceStateHolder stateHolder;
615 private final InstanceHelper instanceHelper;
617 private final Semaphore servingQueue = new Semaphore(0, true);
619 public ServerWrapper(ContainerService containerService,
620 BackendsXml.Entry serverEntry,
621 int instance, int port) {
622 this.container = containerService;
623 this.serverEntry = serverEntry;
624 this.serverInstance = instance;
625 this.port = port;
626 stateHolder = new InstanceStateHolder(serverEntry.getName(), instance);
627 instanceHelper = new InstanceHelper(serverEntry.getName(), instance, stateHolder,
628 container);
631 * Shut down the server.
633 * Will trigger any shutdown hooks installed by the
634 * {@link com.google.appengine.api.LifecycleManager}
636 * @throws Exception
638 void shutdown() throws Exception {
639 instanceHelper.shutdown();
642 void createConnection() throws Exception {
643 getStateHolder().testAndSet(InstanceState.INITIALIZING, InstanceState.SHUTDOWN);
645 Map<String, Object> instanceConfigProperties =
646 ImmutableMap.<String, Object>builder()
647 .putAll(containerConfigProperties)
648 .put(BackendService.BACKEND_ID_ENV_ATTRIBUTE, serverEntry.getName())
649 .put(BackendService.INSTANCE_ID_ENV_ATTRIBUTE, serverInstance)
650 .build();
651 getContainer().configure(ContainerUtils.getServerInfo(),
652 address,
653 port,
654 moduleConfigurationHandle,
655 externalResourceDir,
656 instanceConfigProperties,
657 serverInstance,
658 devAppServer);
659 getContainer().createConnection();
661 this.port = getContainer().getPort();
664 void startup(boolean setStateToStopped) throws Exception {
665 getContainer().startup();
666 if (setStateToStopped) {
667 getStateHolder().testAndSet(InstanceState.STOPPED, InstanceState.INITIALIZING);
668 } else {
669 logger.info(
670 "server: " + serverInstance + "." + serverEntry.getName() + " is running on port "
671 + this.port);
672 if (isLoadBalanceServer()) {
673 getStateHolder().testAndSet(InstanceState.RUNNING, InstanceState.INITIALIZING);
674 } else {
675 getStateHolder().testAndSet(InstanceState.SLEEPING, InstanceState.INITIALIZING);
680 void sendStartRequest() {
681 instanceHelper.sendStartRequest(new Runnable() {
683 @Override
684 public void run() {
685 servingQueue.release(serverEntry.getMaxConcurrentRequests());
691 * Acquires a serving permit for this server.
693 * @param maxWaitTimeInMs Max wait time in ms
694 * @return true if a serving permit was acquired within the allowed time,
695 * false if not permit was required.
696 * @throws InterruptedException If the thread was interrupted while waiting.
698 boolean acquireServingPermit(int maxWaitTimeInMs) throws InterruptedException {
699 logger.finest(
700 this + ": accuiring serving permit, available: " + servingQueue.availablePermits());
701 return servingQueue.tryAcquire(maxWaitTimeInMs, TimeUnit.MILLISECONDS);
705 * Returns a serving permit to the pool of available permits
707 void releaseServingPermit() {
708 servingQueue.release();
709 logger.finest(
710 this + ": returned serving permit, available: " + servingQueue.availablePermits());
714 * Returns the approximate number of threads waiting for serving permits.
716 * @return the number of waiting threads.
718 int getApproximateQueueLength() {
719 return servingQueue.getQueueLength();
723 * Returns the number of requests that can be queued on this specific
724 * server. For servers without pending queue no queue (size=0) is allowed.
726 int getMaxPendingQueueSize() {
727 return serverEntry.isFailFast() ? 0 : MAX_PENDING_QUEUE_LENGTH;
731 * The dns prefix for this server, basically the first part of:
732 * <instance>.<server_name>.<app-id>.appspot.com for a specific instance,
733 * and <server_name>.<app-id>.appspot.com for just the server.
735 public String getDnsPrefix() {
736 if (!isLoadBalanceServer()) {
737 return serverInstance + "." + getName();
738 } else {
739 return getName();
743 String getName() {
744 return serverEntry.getName();
747 public int getInstances() {
748 return serverEntry.getInstances();
751 InstanceStateHolder getStateHolder() {
752 return stateHolder;
755 boolean isLoadBalanceServer() {
756 return serverInstance == -1;
759 @Override
760 public String toString() {
761 return serverInstance + "." + serverEntry.getName() + " state=" + stateHolder;
764 public ContainerService getContainer() {
765 return container;