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
;
16 import java
.io
.IOException
;
17 import java
.util
.HashMap
;
18 import java
.util
.List
;
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
;
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
;
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
;
75 public void setServiceProperties(Map
<String
, String
> properties
) {
76 this.serviceProperties
= properties
;
80 public void shutdownAll() throws Exception
{
81 for (ServerWrapper server
: backendServers
.values()) {
82 logger
.finer("server shutdown: " + server
);
85 backendServers
= ImmutableMap
.copyOf(new HashMap
<ServerInstanceEntry
, ServerWrapper
>());
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();
95 if (requestHostName
== null) {
96 listenAddress
= portMapping
.get(serverWrapper
.getDnsPrefix());
98 listenAddress
= requestHostName
+ ":" + serverWrapper
.port
;
101 BackendStateInfo ssi
= serverInfoMap
.get(name
);
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
);
110 ssi
.add(new InstanceStateInfo(serverWrapper
.serverInstance
, listenAddress
,
111 serverWrapper
.getStateHolder().getDisplayName()));
114 return serverInfoMap
;
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
)) {
129 if (server
.isLoadBalanceServer()) {
130 server
.getStateHolder().testAndSet(InstanceState
.RUNNING
,
131 InstanceState
.STOPPED
);
134 server
.getStateHolder().testAndSet(InstanceState
.SLEEPING
,
135 InstanceState
.STOPPED
);
136 server
.sendStartRequest();
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
)) {
153 if (server
.isLoadBalanceServer()) {
154 server
.getStateHolder().testAndSet(InstanceState
.STOPPED
,
155 InstanceState
.RUNNING
);
158 logger
.fine("Stopping server: " + server
.getDnsPrefix());
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.");
172 List
<BackendsXml
.Entry
> servers
= backendsXml
.getBackends();
173 if (servers
.size() == 0) {
174 logger
.fine("No backends configured.");
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()) {
204 "starting server: " + serverWrapper
.serverInstance
+ "." + serverWrapper
.getName()
205 + " on " + address
+ ":" + serverWrapper
.port
);
206 ApiProxy
.Delegate
<?
> configured
= ApiProxy
.getDelegate();
208 ApiProxy
.setDelegate(local
);
209 serverWrapper
.createConnection();
211 ApiProxy
.setDelegate(configured
);
214 portMap
.put(serverWrapper
.getDnsPrefix(), prettyAddress
+ ":" + serverWrapper
.port
);
216 this.portMapping
= ImmutableMap
.copyOf(portMap
);
220 public void startupAll(ApiProxyLocal local
) throws Exception
{
221 for (ServerWrapper serverWrapper
: backendServers
.values()) {
223 "starting server: " + serverWrapper
.serverInstance
+ "." + serverWrapper
.getName()
224 + " on " + address
+ ":" + serverWrapper
.port
);
225 ApiProxy
.Delegate
<?
> configured
= ApiProxy
.getDelegate();
227 ApiProxy
.setDelegate(local
);
228 serverWrapper
.startup(false);
230 ApiProxy
.setDelegate(configured
);
234 for (ServerWrapper serverWrapper
: backendServers
.values()) {
235 if (serverWrapper
.isLoadBalanceServer()) {
238 serverWrapper
.sendStartRequest();
242 private BackendsXml
.Entry
resolveDefaults(BackendsXml
.Entry entry
) {
243 return new BackendsXml
.Entry(
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(),
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
) {
281 String
.format("trying to get serving permit for server %d.%s", instanceNumber
, serverName
));
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");
291 if (server
.getApproximateQueueLength() > MAX_PENDING_QUEUE_LENGTH
) {
292 logger
.finest(server
+ ": server queue is full");
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="
311 } catch (InterruptedException e
) {
313 instanceNumber
+ "." + serverName
+ ": got interrupted while waiting for serving permit");
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) {
335 if (!server
.getStateHolder().acceptsConnections()) {
338 int instanceNum
= server
.getInstances();
339 for (int i
= 0; i
< instanceNum
; i
++) {
340 if (acquireServingPermit(requestedServer
, i
, false)) {
344 if (server
.getMaxPendingQueueSize() > 0) {
345 return addToShortestInstanceQueue(requestedServer
);
347 logger
.finest("no servers free");
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
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()) {
369 int serverQueue
= server
.getApproximateQueueLength();
370 if (shortestQueue
> serverQueue
) {
371 instanceWithShortestQueue
= server
;
372 shortestQueue
= serverQueue
;
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
);
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
);
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
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
470 public int getServerInstanceFromPort(int port
) {
471 ServerWrapper server
= getServerWrapperFromPort(port
);
472 if (server
!= null) {
473 return server
.serverInstance
;
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
486 public String
getServerNameFromPort(int port
) {
487 ServerWrapper server
= getServerWrapperFromPort(port
);
488 if (server
!= null) {
489 return server
.getName();
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();
508 * Convenience method for getting the ServerWrapper for a specific
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
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
);
533 key
.append("." + instance
);
536 String configuredPort
= serviceProperties
.get(key
.toString());
537 if (configuredPort
!= null) {
538 return Integer
.parseInt(configuredPort
);
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
;
556 * @param instanceNumber
558 public ServerInstanceEntry(String serverName
, int instanceNumber
) {
559 this.serverName
= serverName
;
560 this.instanceNumber
= instanceNumber
;
564 public boolean equals(Object o
) {
565 if (!(o
instanceof ServerInstanceEntry
)) {
569 ServerInstanceEntry that
= (ServerInstanceEntry
) o
;
570 if (this.serverName
!= null) {
571 if (!this.serverName
.equals(that
.serverName
)) {
575 if (that
.serverName
!= null) {
580 if (this.instanceNumber
!= that
.instanceNumber
) {
587 public int hashCode() {
589 hash
= 31 * hash
+ instanceNumber
;
590 if (serverName
!= null) {
591 hash
= 31 * hash
+ serverName
.hashCode();
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
607 private class ServerWrapper
{
609 private final ContainerService container
;
610 private final int serverInstance
;
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
;
626 stateHolder
= new InstanceStateHolder(serverEntry
.getName(), instance
);
627 instanceHelper
= new InstanceHelper(serverEntry
.getName(), instance
, stateHolder
,
631 * Shut down the server.
633 * Will trigger any shutdown hooks installed by the
634 * {@link com.google.appengine.api.LifecycleManager}
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
)
651 getContainer().configure(ContainerUtils
.getServerInfo(),
654 moduleConfigurationHandle
,
656 instanceConfigProperties
,
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
);
670 "server: " + serverInstance
+ "." + serverEntry
.getName() + " is running on port "
672 if (isLoadBalanceServer()) {
673 getStateHolder().testAndSet(InstanceState
.RUNNING
, InstanceState
.INITIALIZING
);
675 getStateHolder().testAndSet(InstanceState
.SLEEPING
, InstanceState
.INITIALIZING
);
680 void sendStartRequest() {
681 instanceHelper
.sendStartRequest(new Runnable() {
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
{
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();
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();
744 return serverEntry
.getName();
747 public int getInstances() {
748 return serverEntry
.getInstances();
751 InstanceStateHolder
getStateHolder() {
755 boolean isLoadBalanceServer() {
756 return serverInstance
== -1;
760 public String
toString() {
761 return serverInstance
+ "." + serverEntry
.getName() + " state=" + stateHolder
;
764 public ContainerService
getContainer() {