1.9.5
[gae.git] / java / src / main / com / google / appengine / tools / development / AbstractBackendServers.java
blobe3caef535f4fae08b921c7553e9057f1351069f9
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;
62 private ApiProxyLocal apiProxyLocal;
64 @Override
65 public void init(String address, ModuleConfigurationHandle moduleConfigurationHandle,
66 File externalResourceDirectory, Map<String, Object> containerConfigProperties,
67 DevAppServer devAppServer) {
68 this.moduleConfigurationHandle = moduleConfigurationHandle;
69 this.externalResourceDir = externalResourceDirectory;
70 this.address = address;
71 this.containerConfigProperties = containerConfigProperties;
72 this.devAppServer = devAppServer;
75 @Override
76 public void setServiceProperties(Map<String, String> properties) {
77 this.serviceProperties = properties;
80 @Override
81 public void shutdownAll() throws Exception {
82 for (ServerWrapper server : backendServers.values()) {
83 logger.finer("server shutdown: " + server);
84 server.shutdown();
86 backendServers = ImmutableMap.copyOf(new HashMap<ServerInstanceEntry, ServerWrapper>());
89 @Override
90 public TreeMap<String, BackendStateInfo> getBackendState(String requestHostName) {
91 TreeMap<String, BackendStateInfo> serverInfoMap = new TreeMap<String, BackendStateInfo>();
92 for (ServerWrapper serverWrapper : backendServers.values()) {
93 String name = serverWrapper.serverEntry.getName();
95 String listenAddress;
96 if (requestHostName == null) {
97 listenAddress = portMapping.get(serverWrapper.getDnsPrefix());
98 } else {
99 listenAddress = requestHostName + ":" + serverWrapper.port;
102 BackendStateInfo ssi = serverInfoMap.get(name);
103 if (ssi == null) {
104 ssi = new BackendStateInfo(serverWrapper.serverEntry);
105 serverInfoMap.put(name, ssi);
107 if (serverWrapper.isLoadBalanceServer()) {
108 ssi.setState(serverWrapper.getStateHolder().getDisplayName());
109 ssi.setAddress(listenAddress);
110 } else {
111 ssi.add(new InstanceStateInfo(serverWrapper.serverInstance, listenAddress,
112 serverWrapper.getStateHolder().getDisplayName()));
115 return serverInfoMap;
118 @Override
119 public synchronized void startBackend(String serverToStart) throws IllegalStateException {
120 if (!checkServerExists(serverToStart)) {
121 String message = String.format("Tried to start unknown server %s", serverToStart);
122 logger.warning(message);
123 throw new IllegalStateException(message);
125 for (ServerWrapper server : backendServers.values()) {
126 if (server.getName().equals(serverToStart)) {
127 if (!server.getStateHolder().test(InstanceState.STOPPED)) {
128 continue;
130 if (server.isLoadBalanceServer()) {
131 server.getStateHolder().testAndSet(InstanceState.RUNNING,
132 InstanceState.STOPPED);
133 continue;
135 server.getStateHolder().testAndSet(InstanceState.SLEEPING,
136 InstanceState.STOPPED);
137 server.sendStartRequest();
142 @Override
143 public synchronized void stopBackend(String serverToStop) throws Exception {
144 if (!checkServerExists(serverToStop)) {
145 String message = String.format("Tried to stop unknown server %s", serverToStop);
146 logger.warning(message);
147 throw new IllegalStateException(message);
149 for (ServerWrapper server : backendServers.values()) {
150 if (server.getName().equals(serverToStop)) {
151 if (server.getStateHolder().test(InstanceState.STOPPED)) {
152 continue;
154 if (server.isLoadBalanceServer()) {
155 server.getStateHolder().testAndSet(InstanceState.STOPPED,
156 InstanceState.RUNNING);
157 continue;
159 logger.fine("Stopping server: " + server.getDnsPrefix());
160 server.shutdown();
161 server.createConnection();
162 server.startup(true);
167 public void configureAll(ApiProxyLocal local) throws Exception {
168 this.apiProxyLocal = local;
169 BackendsXml backendsXml = moduleConfigurationHandle.getBackendsXml();
170 if (backendsXml == null) {
171 logger.fine("Got null backendsXml config.");
172 return;
174 List<BackendsXml.Entry> servers = backendsXml.getBackends();
175 if (servers.isEmpty()) {
176 logger.fine("No backends configured.");
177 return;
180 if (!backendServers.isEmpty()) {
181 throw new Exception("Tried to start backend servers but some are already running.");
183 logger.finer("Found " + servers.size() + " configured backends.");
185 Map<AbstractBackendServers.ServerInstanceEntry, ServerWrapper> serverMap = Maps.newHashMap();
186 for (BackendsXml.Entry entry : servers) {
187 entry = resolveDefaults(entry);
189 for (int serverInstance = -1; serverInstance < entry.getInstances(); serverInstance++) {
190 int port = checkForStaticPort(entry.getName(), serverInstance);
191 ServerWrapper serverWrapper =
192 new ServerWrapper(ContainerUtils.loadContainer(), entry, serverInstance, port);
193 serverMap.put(new ServerInstanceEntry(entry.getName(), serverInstance), serverWrapper);
196 this.backendServers = ImmutableMap.copyOf(serverMap);
198 String prettyAddress = address;
199 if ("0.0.0.0".equals(address)) {
200 prettyAddress = "127.0.0.1";
203 Map<String, String> portMap = Maps.newHashMap();
204 for (ServerWrapper serverWrapper : backendServers.values()) {
205 logger.finer(
206 "starting server: " + serverWrapper.serverInstance + "." + serverWrapper.getName()
207 + " on " + address + ":" + serverWrapper.port);
208 ApiProxy.Delegate<?> configured = ApiProxy.getDelegate();
209 try {
210 ApiProxy.setDelegate(local);
211 serverWrapper.createConnection();
212 } finally {
213 ApiProxy.setDelegate(configured);
216 portMap.put(serverWrapper.getDnsPrefix(), prettyAddress + ":" + serverWrapper.port);
218 this.portMapping = ImmutableMap.copyOf(portMap);
221 @Override
222 public void startupAll() throws Exception {
223 for (ServerWrapper serverWrapper : backendServers.values()) {
224 logger.finer(
225 "starting server: " + serverWrapper.serverInstance + "." + serverWrapper.getName()
226 + " on " + address + ":" + serverWrapper.port);
227 ApiProxy.Delegate<?> configured = ApiProxy.getDelegate();
228 try {
229 ApiProxy.setDelegate(apiProxyLocal);
230 serverWrapper.startup(false);
231 } finally {
232 ApiProxy.setDelegate(configured);
236 for (ServerWrapper serverWrapper : backendServers.values()) {
237 if (serverWrapper.isLoadBalanceServer()) {
238 continue;
240 serverWrapper.sendStartRequest();
244 private BackendsXml.Entry resolveDefaults(BackendsXml.Entry entry) {
245 return new BackendsXml.Entry(
246 entry.getName(),
247 entry.getInstances() == null ? DEFAULT_INSTANCES : entry.getInstances(),
248 entry.getInstanceClass() == null ? DEFAULT_INSTANCE_CLASS : entry.getInstanceClass(),
249 entry.getMaxConcurrentRequests() == null ? DEFAULT_MAX_CONCURRENT_REQUESTS :
250 entry.getMaxConcurrentRequests(),
251 entry.getOptions(),
252 entry.getState() == null ? BackendsXml.State.STOP : entry.getState());
256 * Forward a request to a specific server and instance. This will call the
257 * specified instance request dispatcher so the request is handled in the
258 * right server context.
260 void forwardToServer(String requestedServer, int instance, HttpServletRequest hrequest,
261 HttpServletResponse hresponse) throws IOException, ServletException {
262 ServerWrapper server = getServerWrapper(requestedServer, instance);
263 logger.finest("forwarding request to server: " + server);
264 server.getContainer().forwardToServer(hrequest, hresponse);
268 * This method guards access to servers to limit the number of concurrent
269 * requests. Each request running on a server must acquire a serving permit.
270 * If no permits are available a 500 response should be sent.
272 * @param serverName The server for which to acquire a permit.
273 * @param instanceNumber The server instance for which to acquire a permit.
274 * @param allowQueueOnBackends If set to false the method will return
275 * instantly, if set to true (and the specified server allows pending
276 * queues) this method can block for up to 10 s waiting for a serving
277 * permit to become available.
278 * @return true if a permit was acquired, false otherwise
280 public boolean acquireServingPermit(
281 String serverName, int instanceNumber, boolean allowQueueOnBackends) {
282 logger.finest(
283 String.format("trying to get serving permit for server %d.%s", instanceNumber, serverName));
284 try {
285 ServerWrapper server = getServerWrapper(serverName, instanceNumber);
286 int maxQueueTime = 0;
288 synchronized (server.getStateHolder()) {
289 if (!server.getStateHolder().acceptsConnections()) {
290 logger.finest(server + ": got request but server is not in a serving state");
291 return false;
293 if (server.getApproximateQueueLength() > MAX_PENDING_QUEUE_LENGTH) {
294 logger.finest(server + ": server queue is full");
295 return false;
297 if (server.getStateHolder().test(InstanceState.SLEEPING)) {
298 logger.finest(server + ": waking up sleeping server");
299 server.sendStartRequest();
302 if (server.getStateHolder().test(InstanceState.RUNNING_START_REQUEST)) {
303 maxQueueTime = MAX_START_QUEUE_TIME_MS;
304 } else if (allowQueueOnBackends && server.getMaxPendingQueueSize() > 0) {
305 maxQueueTime = MAX_PENDING_QUEUE_TIME_MS;
309 boolean gotPermit = server.acquireServingPermit(maxQueueTime);
310 logger.finest(server + ": tried to get server permit, timeout=" + maxQueueTime + " success="
311 + gotPermit);
312 return gotPermit;
313 } catch (InterruptedException e) {
314 logger.finest(
315 instanceNumber + "." + serverName + ": got interrupted while waiting for serving permit");
316 return false;
321 * Reserves an instance for this request. For workers this method will return
322 * -1 if no free instances are available. For backends this method will assign
323 * this request to the instance with the shortest queue and block until that
324 * instance is ready to serve the request.
326 * @param requestedServer Name of the server the request is to.
327 * @return the instance id of an available server instance, or -1 if no
328 * instance is available.
330 public int getAndReserveFreeInstance(String requestedServer) {
331 logger.finest("trying to get serving permit for server " + requestedServer);
333 ServerWrapper server = getServerWrapper(requestedServer, -1);
334 if (server == null) {
335 return -1;
337 if (!server.getStateHolder().acceptsConnections()) {
338 return -1;
340 int instanceNum = server.getInstances();
341 for (int i = 0; i < instanceNum; i++) {
342 if (acquireServingPermit(requestedServer, i, false)) {
343 return i;
346 if (server.getMaxPendingQueueSize() > 0) {
347 return addToShortestInstanceQueue(requestedServer);
348 } else {
349 logger.finest("no servers free");
350 return -1;
355 * Will add this request to the queue of the instance with the approximate
356 * shortest queue. This method will block for up to 10 seconds until a permit
357 * is received.
359 * @param requestedServer the server name
360 * @return the instance where the serving permit was reserved, or -1 if all
361 * instance queues are full
363 int addToShortestInstanceQueue(String requestedServer) {
364 logger.finest(requestedServer + ": no instances free, trying to find a queue");
365 int shortestQueue = MAX_PENDING_QUEUE_LENGTH;
366 ServerWrapper instanceWithShortestQueue = null;
367 for (ServerWrapper server : backendServers.values()) {
368 if (!server.getStateHolder().acceptsConnections()) {
369 continue;
371 int serverQueue = server.getApproximateQueueLength();
372 if (shortestQueue > serverQueue) {
373 instanceWithShortestQueue = server;
374 shortestQueue = serverQueue;
378 try {
379 if (shortestQueue < MAX_PENDING_QUEUE_LENGTH) {
380 logger.finest("adding request to queue on instance: " + instanceWithShortestQueue);
381 if (instanceWithShortestQueue.acquireServingPermit(MAX_PENDING_QUEUE_TIME_MS)) {
382 logger.finest("ready to serve request on instance: " + instanceWithShortestQueue);
383 return instanceWithShortestQueue.serverInstance;
386 } catch (InterruptedException e) {
387 logger.finer("interupted while queued at server " + instanceWithShortestQueue);
389 return -1;
393 * Method for returning a serving permit after a request has completed.
395 * @param serverName The server name
396 * @param instance The server instance
398 public void returnServingPermit(String serverName, int instance) {
399 ServerWrapper server = getServerWrapper(serverName, instance);
400 server.releaseServingPermit();
404 * Returns the port for the requested instance.
406 public int getPort(String serverName, int instance) {
407 ServerWrapper server = getServerWrapper(serverName, instance);
408 return server.port;
412 * Verifies if a specific server/instance is configured.
414 * @param serverName The server name
415 * @param instance The server instance
416 * @return true if the server/instance is configured, false otherwise.
418 public boolean checkInstanceExists(String serverName, int instance) {
419 return getServerWrapper(serverName, instance) != null;
423 * Verifies if a specific server is configured.
425 * @param serverName The server name
426 * @return true if the server is configured, false otherwise.
428 public boolean checkServerExists(String serverName) {
429 return checkInstanceExists(serverName, -1);
433 * Verifies if a specific server is stopped.
435 * @param serverName The server name
436 * @return true if the server is stopped, false otherwise.
438 public boolean checkServerStopped(String serverName) {
439 return checkInstanceStopped(serverName, -1);
443 * Verifies if a specific server/instance is stopped.
445 * @param serverName The server name
446 * @param instance The server instance
447 * @return true if the server/instance is stopped, false otherwise.
449 public boolean checkInstanceStopped(String serverName, int instance) {
450 return !getServerWrapper(serverName, instance)
451 .getStateHolder().acceptsConnections();
455 * Allows the servers API to get the current mapping from server and instance
456 * to listening ports.
458 * @return The port map
460 @Override
461 public Map<String, String> getPortMapping() {
462 return this.portMapping;
466 * Returns the server instance serving on a specific local port
468 * @param port the local tcp port that received the request
469 * @return the server instance, or -1 if no server instance is running on that
470 * port
472 public int getServerInstanceFromPort(int port) {
473 ServerWrapper server = getServerWrapperFromPort(port);
474 if (server != null) {
475 return server.serverInstance;
476 } else {
477 return -1;
482 * Returns the server serving on a specific local port
484 * @param port the local tcp port that received the request
485 * @return the server name, or null if no server instance is running on that
486 * port
488 public String getServerNameFromPort(int port) {
489 ServerWrapper server = getServerWrapperFromPort(port);
490 if (server != null) {
491 return server.getName();
492 } else {
493 return null;
498 * Convenience method for getting the ServerWrapper running on a specific port
500 private ServerWrapper getServerWrapperFromPort(int port) {
501 for (Entry<ServerInstanceEntry, ServerWrapper> entry : backendServers.entrySet()) {
502 if (entry.getValue().port == port) {
503 return entry.getValue();
506 return null;
510 * Convenience method for getting the ServerWrapper for a specific
511 * server/instance
513 protected ServerWrapper getServerWrapper(String serverName, int instanceNumber) {
514 return backendServers.get(new ServerInstanceEntry(serverName, instanceNumber));
518 * Verifies if the specific port is statically configured, if not it will
519 * return 0 which instructs jetty to pick the port
521 * Ports can be statically configured by a system property at
522 * com.google.appengine.server.<server-name>.port for the server and
523 * com.google.appengine.server.<server-name>.<instance-id>.port for individual
524 * instances
526 * @param server the name of the configured server
527 * @param instance the instance number to configure
528 * @return the statically configured port, or 0 if none is configured
530 private int checkForStaticPort(String server, int instance) {
531 StringBuilder key = new StringBuilder();
532 key.append(SYSTEM_PROPERTY_STATIC_PORT_NUM_PREFIX);
533 key.append(server);
534 if (instance >= 0) {
535 key.append("." + instance);
537 key.append(".port");
538 String configuredPort = serviceProperties.get(key.toString());
539 if (configuredPort != null) {
540 return Integer.parseInt(configuredPort);
541 } else {
542 return 0;
547 * Class that allows the key in the server map to be the
548 * (servername,instanceid) tuple. Overrides equals() and hashcode() to
549 * function as a hashtable key
552 static class ServerInstanceEntry {
553 private final int instanceNumber;
554 private final String serverName;
557 * @param serverName
558 * @param instanceNumber
560 public ServerInstanceEntry(String serverName, int instanceNumber) {
561 this.serverName = serverName;
562 this.instanceNumber = instanceNumber;
565 @Override
566 public boolean equals(Object o) {
567 if (!(o instanceof ServerInstanceEntry)) {
568 return false;
571 ServerInstanceEntry that = (ServerInstanceEntry) o;
572 if (this.serverName != null) {
573 if (!this.serverName.equals(that.serverName)) {
574 return false;
576 } else {
577 if (that.serverName != null) {
578 return false;
582 if (this.instanceNumber != that.instanceNumber) {
583 return false;
585 return true;
588 @Override
589 public int hashCode() {
590 int hash = 17;
591 hash = 31 * hash + instanceNumber;
592 if (serverName != null) {
593 hash = 31 * hash + serverName.hashCode();
595 return hash;
598 @Override
599 public String toString() {
600 return instanceNumber + "." + serverName;
605 * Wraps a container service and contains extra information such as the
606 * instanceid of the current container as well as the port number it is
607 * running on.
609 protected class ServerWrapper {
611 private final ContainerService container;
612 private final int serverInstance;
613 private int port;
614 private final BackendsXml.Entry serverEntry;
616 private final InstanceStateHolder stateHolder;
617 private final InstanceHelper instanceHelper;
619 private final Semaphore servingQueue = new Semaphore(0, true);
621 public ServerWrapper(ContainerService containerService,
622 BackendsXml.Entry serverEntry,
623 int instance, int port) {
624 this.container = containerService;
625 this.serverEntry = serverEntry;
626 this.serverInstance = instance;
627 this.port = port;
628 stateHolder = new InstanceStateHolder(serverEntry.getName(), instance);
629 instanceHelper = new InstanceHelper(serverEntry.getName(), instance, stateHolder,
630 container);
633 * Shut down the server.
635 * Will trigger any shutdown hooks installed by the
636 * {@link com.google.appengine.api.LifecycleManager}
638 * @throws Exception
640 void shutdown() throws Exception {
641 instanceHelper.shutdown();
644 void createConnection() throws Exception {
645 getStateHolder().testAndSet(InstanceState.INITIALIZING, InstanceState.SHUTDOWN);
647 Map<String, Object> instanceConfigProperties =
648 ImmutableMap.<String, Object>builder()
649 .putAll(containerConfigProperties)
650 .put(BackendService.BACKEND_ID_ENV_ATTRIBUTE, serverEntry.getName())
651 .put(BackendService.INSTANCE_ID_ENV_ATTRIBUTE, serverInstance)
652 .build();
653 getContainer().configure(ContainerUtils.getServerInfo(),
654 address,
655 port,
656 moduleConfigurationHandle,
657 externalResourceDir,
658 instanceConfigProperties,
659 serverInstance,
660 devAppServer);
661 getContainer().createConnection();
662 getContainer().setApiProxyDelegate(apiProxyLocal);
664 this.port = getContainer().getPort();
667 void startup(boolean setStateToStopped) throws Exception {
668 getContainer().startup();
669 if (setStateToStopped) {
670 getStateHolder().testAndSet(InstanceState.STOPPED, InstanceState.INITIALIZING);
671 } else {
672 logger.info(
673 "server: " + serverInstance + "." + serverEntry.getName() + " is running on port "
674 + this.port);
675 if (isLoadBalanceServer()) {
676 getStateHolder().testAndSet(InstanceState.RUNNING, InstanceState.INITIALIZING);
677 } else {
678 getStateHolder().testAndSet(InstanceState.SLEEPING, InstanceState.INITIALIZING);
683 void sendStartRequest() {
684 instanceHelper.sendStartRequest(new Runnable() {
686 @Override
687 public void run() {
688 servingQueue.release(serverEntry.getMaxConcurrentRequests());
694 * Acquires a serving permit for this server.
696 * @param maxWaitTimeInMs Max wait time in ms
697 * @return true if a serving permit was acquired within the allowed time,
698 * false if not permit was required.
699 * @throws InterruptedException If the thread was interrupted while waiting.
701 boolean acquireServingPermit(int maxWaitTimeInMs) throws InterruptedException {
702 logger.finest(
703 this + ": accuiring serving permit, available: " + servingQueue.availablePermits());
704 return servingQueue.tryAcquire(maxWaitTimeInMs, TimeUnit.MILLISECONDS);
708 * Returns a serving permit to the pool of available permits
710 void releaseServingPermit() {
711 servingQueue.release();
712 logger.finest(
713 this + ": returned serving permit, available: " + servingQueue.availablePermits());
717 * Returns the approximate number of threads waiting for serving permits.
719 * @return the number of waiting threads.
721 int getApproximateQueueLength() {
722 return servingQueue.getQueueLength();
726 * Returns the number of requests that can be queued on this specific
727 * server. For servers without pending queue no queue (size=0) is allowed.
729 int getMaxPendingQueueSize() {
730 return serverEntry.isFailFast() ? 0 : MAX_PENDING_QUEUE_LENGTH;
734 * The dns prefix for this server, basically the first part of:
735 * <instance>.<server_name>.<app-id>.appspot.com for a specific instance,
736 * and <server_name>.<app-id>.appspot.com for just the server.
738 public String getDnsPrefix() {
739 if (!isLoadBalanceServer()) {
740 return serverInstance + "." + getName();
741 } else {
742 return getName();
746 String getName() {
747 return serverEntry.getName();
750 public int getInstances() {
751 return serverEntry.getInstances();
754 InstanceStateHolder getStateHolder() {
755 return stateHolder;
758 boolean isLoadBalanceServer() {
759 return serverInstance == -1;
762 @Override
763 public String toString() {
764 return serverInstance + "." + serverEntry.getName() + " state=" + stateHolder;
767 public ContainerService getContainer() {
768 return container;