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
;
62 private ApiProxyLocal apiProxyLocal
;
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
;
76 public void setServiceProperties(Map
<String
, String
> properties
) {
77 this.serviceProperties
= properties
;
81 public void shutdownAll() throws Exception
{
82 for (ServerWrapper server
: backendServers
.values()) {
83 logger
.finer("server shutdown: " + server
);
86 backendServers
= ImmutableMap
.copyOf(new HashMap
<ServerInstanceEntry
, ServerWrapper
>());
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();
96 if (requestHostName
== null) {
97 listenAddress
= portMapping
.get(serverWrapper
.getDnsPrefix());
99 listenAddress
= requestHostName
+ ":" + serverWrapper
.port
;
102 BackendStateInfo ssi
= serverInfoMap
.get(name
);
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
);
111 ssi
.add(new InstanceStateInfo(serverWrapper
.serverInstance
, listenAddress
,
112 serverWrapper
.getStateHolder().getDisplayName()));
115 return serverInfoMap
;
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
)) {
130 if (server
.isLoadBalanceServer()) {
131 server
.getStateHolder().testAndSet(InstanceState
.RUNNING
,
132 InstanceState
.STOPPED
);
135 server
.getStateHolder().testAndSet(InstanceState
.SLEEPING
,
136 InstanceState
.STOPPED
);
137 server
.sendStartRequest();
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
)) {
154 if (server
.isLoadBalanceServer()) {
155 server
.getStateHolder().testAndSet(InstanceState
.STOPPED
,
156 InstanceState
.RUNNING
);
159 logger
.fine("Stopping server: " + server
.getDnsPrefix());
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.");
174 List
<BackendsXml
.Entry
> servers
= backendsXml
.getBackends();
175 if (servers
.isEmpty()) {
176 logger
.fine("No backends configured.");
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()) {
206 "starting server: " + serverWrapper
.serverInstance
+ "." + serverWrapper
.getName()
207 + " on " + address
+ ":" + serverWrapper
.port
);
208 ApiProxy
.Delegate
<?
> configured
= ApiProxy
.getDelegate();
210 ApiProxy
.setDelegate(local
);
211 serverWrapper
.createConnection();
213 ApiProxy
.setDelegate(configured
);
216 portMap
.put(serverWrapper
.getDnsPrefix(), prettyAddress
+ ":" + serverWrapper
.port
);
218 this.portMapping
= ImmutableMap
.copyOf(portMap
);
222 public void startupAll() throws Exception
{
223 for (ServerWrapper serverWrapper
: backendServers
.values()) {
225 "starting server: " + serverWrapper
.serverInstance
+ "." + serverWrapper
.getName()
226 + " on " + address
+ ":" + serverWrapper
.port
);
227 ApiProxy
.Delegate
<?
> configured
= ApiProxy
.getDelegate();
229 ApiProxy
.setDelegate(apiProxyLocal
);
230 serverWrapper
.startup(false);
232 ApiProxy
.setDelegate(configured
);
236 for (ServerWrapper serverWrapper
: backendServers
.values()) {
237 if (serverWrapper
.isLoadBalanceServer()) {
240 serverWrapper
.sendStartRequest();
244 private BackendsXml
.Entry
resolveDefaults(BackendsXml
.Entry entry
) {
245 return new BackendsXml
.Entry(
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(),
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
) {
283 String
.format("trying to get serving permit for server %d.%s", instanceNumber
, serverName
));
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");
293 if (server
.getApproximateQueueLength() > MAX_PENDING_QUEUE_LENGTH
) {
294 logger
.finest(server
+ ": server queue is full");
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="
313 } catch (InterruptedException e
) {
315 instanceNumber
+ "." + serverName
+ ": got interrupted while waiting for serving permit");
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) {
337 if (!server
.getStateHolder().acceptsConnections()) {
340 int instanceNum
= server
.getInstances();
341 for (int i
= 0; i
< instanceNum
; i
++) {
342 if (acquireServingPermit(requestedServer
, i
, false)) {
346 if (server
.getMaxPendingQueueSize() > 0) {
347 return addToShortestInstanceQueue(requestedServer
);
349 logger
.finest("no servers free");
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
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()) {
371 int serverQueue
= server
.getApproximateQueueLength();
372 if (shortestQueue
> serverQueue
) {
373 instanceWithShortestQueue
= server
;
374 shortestQueue
= serverQueue
;
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
);
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
);
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
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
472 public int getServerInstanceFromPort(int port
) {
473 ServerWrapper server
= getServerWrapperFromPort(port
);
474 if (server
!= null) {
475 return server
.serverInstance
;
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
488 public String
getServerNameFromPort(int port
) {
489 ServerWrapper server
= getServerWrapperFromPort(port
);
490 if (server
!= null) {
491 return server
.getName();
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();
510 * Convenience method for getting the ServerWrapper for a specific
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
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
);
535 key
.append("." + instance
);
538 String configuredPort
= serviceProperties
.get(key
.toString());
539 if (configuredPort
!= null) {
540 return Integer
.parseInt(configuredPort
);
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
;
558 * @param instanceNumber
560 public ServerInstanceEntry(String serverName
, int instanceNumber
) {
561 this.serverName
= serverName
;
562 this.instanceNumber
= instanceNumber
;
566 public boolean equals(Object o
) {
567 if (!(o
instanceof ServerInstanceEntry
)) {
571 ServerInstanceEntry that
= (ServerInstanceEntry
) o
;
572 if (this.serverName
!= null) {
573 if (!this.serverName
.equals(that
.serverName
)) {
577 if (that
.serverName
!= null) {
582 if (this.instanceNumber
!= that
.instanceNumber
) {
589 public int hashCode() {
591 hash
= 31 * hash
+ instanceNumber
;
592 if (serverName
!= null) {
593 hash
= 31 * hash
+ serverName
.hashCode();
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
609 protected class ServerWrapper
{
611 private final ContainerService container
;
612 private final int serverInstance
;
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
;
628 stateHolder
= new InstanceStateHolder(serverEntry
.getName(), instance
);
629 instanceHelper
= new InstanceHelper(serverEntry
.getName(), instance
, stateHolder
,
633 * Shut down the server.
635 * Will trigger any shutdown hooks installed by the
636 * {@link com.google.appengine.api.LifecycleManager}
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
)
653 getContainer().configure(ContainerUtils
.getServerInfo(),
656 moduleConfigurationHandle
,
658 instanceConfigProperties
,
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
);
673 "server: " + serverInstance
+ "." + serverEntry
.getName() + " is running on port "
675 if (isLoadBalanceServer()) {
676 getStateHolder().testAndSet(InstanceState
.RUNNING
, InstanceState
.INITIALIZING
);
678 getStateHolder().testAndSet(InstanceState
.SLEEPING
, InstanceState
.INITIALIZING
);
683 void sendStartRequest() {
684 instanceHelper
.sendStartRequest(new Runnable() {
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
{
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();
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();
747 return serverEntry
.getName();
750 public int getInstances() {
751 return serverEntry
.getInstances();
754 InstanceStateHolder
getStateHolder() {
758 boolean isLoadBalanceServer() {
759 return serverInstance
== -1;
763 public String
toString() {
764 return serverInstance
+ "." + serverEntry
.getName() + " state=" + stateHolder
;
767 public ContainerService
getContainer() {