/************************************************************************* * * ADOBE CONFIDENTIAL * __________________ * * [2002] - [2007] Adobe Systems Incorporated * All Rights Reserved. * * NOTICE: All information contained herein is, and remains * the property of Adobe Systems Incorporated and its suppliers, * if any. The intellectual and technical concepts contained * herein are proprietary to Adobe Systems Incorporated * and its suppliers and may be covered by U.S. and Foreign Patents, * patents in process, and are protected by trade secret or copyright law. * Dissemination of this information or reproduction of this material * is strictly forbidden unless prior written permission is obtained * from Adobe Systems Incorporated. **************************************************************************/ /** Regardless of the above rights notice, this file was * obtained under a distribution of the 3.2.0 BlazeDS * source which was labeled as available under the * e GNU Lesser General Public License Version 3 , * as published by the Free Software Foundation. * * This file has been modified and distributed under those * terms and parts of this file are copyright webtide LLC 2009 */ package org.mortbay.jetty.asyncblazeds; import flex.messaging.FlexContext; import flex.messaging.FlexSession; import flex.messaging.client.AsyncPollHandler; import flex.messaging.client.FlexClient; import flex.messaging.client.FlushResult; import flex.messaging.client.PollFlushResult; import flex.messaging.client.PollWaitListener; import flex.messaging.client.UserAgentSettings; import flex.messaging.config.ConfigMap; import flex.messaging.config.ConfigurationConstants; import flex.messaging.endpoints.BaseHTTPEndpoint; import flex.messaging.io.MessageIOConstants; import flex.messaging.io.amf.ActionContext; import flex.messaging.log.Log; import flex.messaging.log.HTTPRequestLog; import flex.messaging.messages.CommandMessage; import flex.messaging.util.SettingsReplaceUtil; import flex.messaging.util.UserAgentManager; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.concurrent.ConcurrentHashMap; import org.eclipse.jetty.continuation.Continuation; import org.eclipse.jetty.continuation.ContinuationSupport; import org.eclipse.jetty.continuation.ContinuationThrowable; /** * Base for HTTP-based endpoints that support regular polling and long polling. * * If there are messages waiting when the client polls, the request returns immediately * with the messages. Otherwise, the client will hold the poll. If the endpoint is configured to wait * indefinitely until notified of messages, the client will do a blocking long poll, placing the request * thread into a wait state; if configured to wait for a positive value of milliseconds, * request processing is suspended and the request goes into a threadless wait. The request returns when * messages are available to deliver, or when the configurable wait interval is reached. * * The threadless wait is achieved using jetty-7 portable continuations, which work asynchronous on jetty-6 * and any servlet-3.0 container. */ public abstract class BaseAsyncHTTPEndpoint extends BaseHTTPEndpoint implements PollWaitListener { // -------------------------------------------------------------------------- // // Private Static Constants // // -------------------------------------------------------------------------- private static final String POLLING_ENABLED = "polling-enabled"; private static final String POLLING_INTERVAL_MILLIS = "polling-interval-millis"; private static final String POLLING_INTERVAL_SECONDS = "polling-interval-seconds"; // Deprecated // configuration // option. private static final String MAX_WAITING_POLL_REQUESTS = "max-waiting-poll-requests"; private static final String WAIT_INTERVAL_MILLIS = "wait-interval-millis"; private static final String CLIENT_WAIT_INTERVAL_MILLIS = "client-wait-interval-millis"; // Force clients that exceed the long-poll limit to wait at least this long // between poll requests. // This matches the default polling interval defined in the client // PollingChannel. private static final int DEFAULT_WAIT_FOR_EXCESS_POLL_WAIT_CLIENTS = 3000; // User Agent based settings manager private UserAgentManager userAgentManager = new UserAgentManager(); // Access request private static ThreadLocal<HttpServletRequest> request = new ThreadLocal<HttpServletRequest>(); // -------------------------------------------------------------------------- // // Constructor // // -------------------------------------------------------------------------- /** * Constructs an unmanaged <code>BaseAsyncHTTPEndpoint</code>. */ public BaseAsyncHTTPEndpoint() { this(false); } /** * Constructs a <code>BaseAsyncHTTPEndpoint</code> with the indicated * management. * * @param enableManagement * <code>true</code> if the <code>BaseAsyncHTTPEndpoint</code> * is manageable; otherwise <code>false</code>. */ public BaseAsyncHTTPEndpoint(boolean enableManagement) { super(enableManagement); } /** * Handle AMF/AMFX encoded messages sent over HTTP, with suspend/resume of the request, * using jetty-7 portable continuations. * * @param req The original servlet request. * @param res The active servlet response. */ public void service(HttpServletRequest req, HttpServletResponse res) { Thread thread=Thread.currentThread(); final Continuation continuation = ContinuationSupport.getContinuation(req); ActionContext context= (ActionContext)req.getAttribute("ActionContext"); try { // Setup serialization and type marshalling contexts request.set(req); setThreadLocals(); if (context==null) { validateRequestProtocol(req); context = new ActionContext(); req.setAttribute("ActionContext",context); // Pass endpoint's mpi settings to the context so that it knows what level of // performance metrics should be gathered during serialization/deserialization context.setRecordMessageSizes(isRecordMessageSizes()); context.setRecordMessageTimes(isRecordMessageTimes()); } // Send invocation through filter chain, which ends at the MessageBroker filterChain.invoke(context); if (!continuation.isSuspended()) { // After serialization completes, increment endpoint byte counters, // if the endpoint is managed if (isManaged()) { controller.addToBytesDeserialized(context.getDeserializedBytes()); controller.addToBytesSerialized(context.getSerializedBytes()); } if (context.getStatus() != MessageIOConstants.STATUS_NOTAMF) { if (addNoCacheHeaders) addNoCacheHeaders(req, res); ByteArrayOutputStream outBuffer = context.getResponseOutput(); res.setContentType(getResponseContentType()); res.setContentLength(outBuffer.size()); outBuffer.writeTo(res.getOutputStream()); res.flushBuffer(); } else { // Not an AMF request, probably viewed in a browser if (redirectURL != null) { try { //Check for redirect URL context-root token redirectURL = SettingsReplaceUtil.replaceContextPath(redirectURL, req.getContextPath()); res.sendRedirect(redirectURL); } catch (IllegalStateException alreadyFlushed) { // ignore } } } } } catch (ContinuationThrowable ct) { // This causes blazeds4 to issue a HTTP 500 error due to an added try/catch Throwable added to MessageBrokerServlet->service //throw ct; } catch (IOException ioe) { // This happens when client closes the connection, log it at info level log.info(ioe.getMessage()); // Store exception information for latter logging req.setAttribute(HTTPRequestLog.HTTP_ERROR_INFO, ioe.toString()); } catch (Throwable t) { log.error(t.getMessage(), t); // Store exception information for latter logging req.setAttribute(HTTPRequestLog.HTTP_ERROR_INFO, t.toString()); } finally { request.set(null); clearThreadLocals(); } } // -------------------------------------------------------------------------- // // Initialize, validate, start, and stop methods. // // -------------------------------------------------------------------------- /** * Initializes the <code>Endpoint</code> with the properties. If subclasses * override, they must call <code>super.initialize()</code>. * * @param id * Id of the <code>Endpoint</code>. * @param properties * Properties for the <code>Endpoint</code>. */ public void initialize(String id, ConfigMap properties) { super.initialize(id,properties); if (properties == null || properties.size() == 0) return; // General poll props. pollingEnabled = properties.getPropertyAsBoolean(POLLING_ENABLED,false); pollingIntervalMillis = properties.getPropertyAsLong(POLLING_INTERVAL_MILLIS,-1); long pollingIntervalSeconds = properties.getPropertyAsLong(POLLING_INTERVAL_SECONDS,-1); // Deprecated if (pollingIntervalSeconds > -1) pollingIntervalMillis = pollingIntervalSeconds * 1000; // Piggybacking props. piggybackingEnabled = properties.getPropertyAsBoolean(ConfigurationConstants.PIGGYBACKING_ENABLED_ELEMENT,false); // HTTP poll wait props. maxWaitingPollRequests = properties.getPropertyAsInt(MAX_WAITING_POLL_REQUESTS,0); waitInterval = properties.getPropertyAsLong(WAIT_INTERVAL_MILLIS,0); clientWaitInterval = properties.getPropertyAsInt(CLIENT_WAIT_INTERVAL_MILLIS,0); // User Agent props. UserAgentManager.setupUserAgentManager(properties,userAgentManager); // Set initial state for the canWait flag based on whether we allow // waits or not. if (maxWaitingPollRequests > 0 && (waitInterval == -1 || waitInterval > 0)) { waitEnabled = true; canWait = true; } } // -------------------------------------------------------------------------- // // Variables // // -------------------------------------------------------------------------- /** * This flag is volatile to allow for consistent reads across thread without * needing to pay the cost for a synchronized lock for each read. */ private volatile boolean canWait; /** * Used to synchronize sets and gets to the number of waiting clients. */ protected final Object lock = new Object(); /** * Set when properties are handled; used as a shortcut for logging to * determine whether this instance attempts to put request threads in a wait * state or not. */ private boolean waitEnabled; /** * A count of the number of request threads that are currently in the wait * state (including those on their way into or out of it). */ protected int waitingPollRequestsCount; /** * A Map(notification Object for a waited request thread, Boolean.TRUE). */ private ConcurrentHashMap currentWaitedRequests; // -------------------------------------------------------------------------- // // Properties // // -------------------------------------------------------------------------- // ---------------------------------- // clientWaitInterval // ---------------------------------- protected int clientWaitInterval = 0; /** * Returns the number of milliseconds the client will wait after receiving a * response for a poll with server wait before it issues its next poll * request. A value of zero or less causes the client to use its default * polling interval (based on the channel's polling-interval-millis * configuration) and this value is ignored. A value greater than zero will * cause the client to wait for the specified interval before issuing its * next poll request with a value of 1 triggering an immediate poll from the * client as soon as a waited poll response is received. */ public int getClientWaitInterval() { return clientWaitInterval; } /** * Sets the number of milliseconds a client will wait after receiving a * response for a poll with server wait before it issues its next poll * request. A value of zero or less causes the client to use its default * polling interval (based on the channel's polling-interval-millis * configuration) and this value is ignored. A value greater than zero will * cause the client to wait for the specified interval before issuing its * next poll request with a value of 1 triggering an immediate poll from the * client as soon as a waited poll response is received. This property does * not effect polling clients that poll the server without a server wait. * * @param value The number of milliseconds a client will wait before issuing its * next poll when the server is configured to wait. */ public void setClientWaitInterval(int value) { clientWaitInterval = value; } // ---------------------------------- // maxWaitingPollRequests // ---------------------------------- protected int maxWaitingPollRequests = 0; /** * Returns the maximum number of server poll response threads that will be * waiting for messages to arrive for clients. */ public int getMaxWaitingPollRequests() { return maxWaitingPollRequests; } /** * Sets the maximum number of server poll response threads that will be * waiting for messages to arrive for clients. If you set wait-interval to -1, * note that the request threads will block and you will need to use a lower * limit; if you set wait-interval to use a positive integer, the requests will be * suspended using threadless waits, and it is safe to set a higher limit. * * @param maxWaitingPollRequests * The maximum number of server poll response threads that will * be waiting for messages to arrive for the client. */ public void setMaxWaitingPollRequests(int maxWaitingPollRequests) { this.maxWaitingPollRequests = maxWaitingPollRequests; if (maxWaitingPollRequests > 0 && (waitInterval == -1 || waitInterval > 0)) { waitEnabled = true; canWait = (waitingPollRequestsCount < maxWaitingPollRequests); } } // ---------------------------------- // pollingEnabled // ---------------------------------- /** * @exclude This is a property used on the client. */ protected boolean piggybackingEnabled; // ---------------------------------- // pollingEnabled // ---------------------------------- /** * @exclude This is a property used on the client. */ protected boolean pollingEnabled; // ---------------------------------- // pollingIntervalMillis // ---------------------------------- /** * @exclude This is a property used on the client. */ protected long pollingIntervalMillis; // ---------------------------------- // waitInterval // ---------------------------------- protected long waitInterval = 0; /** * Returns the number of milliseconds the server poll response thread will * be waiting for messages to arrive for the client. */ public long getWaitInterval() { return waitInterval; } /** * Sets the number of milliseconds the server poll response thread will be * waiting for messages to arrive for the client. * * @param waitInterval * The number of milliseconds the server poll response thread * will be waiting for messages to arrive for the client. A value * of -1 means wait until notified, and will hold the request thread. * A value > 0 puts the request into a threadless wait. */ public void setWaitInterval(long waitInterval) { this.waitInterval = waitInterval; if (maxWaitingPollRequests > 0 && (waitInterval == -1 || waitInterval > 0)) { waitEnabled = true; canWait = (waitingPollRequestsCount < maxWaitingPollRequests); } } // -------------------------------------------------------------------------- // // Public Methods // // -------------------------------------------------------------------------- /** * @exclude Returns a <code>ConfigMap</code> of endpoint properties that the * client needs. This includes properties from * <code>super.describeEndpoint</code> and additional * <code>BaseHTTPEndpoint</code> specific properties under * "properties" key. */ public ConfigMap describeEndpoint() { ConfigMap endpointConfig = super.describeEndpoint(); if (loginAfterDisconnect) { ConfigMap loginAfterDisconnect = new ConfigMap(); // Adding as a value rather than attribute to the parent loginAfterDisconnect.addProperty(EMPTY_STRING, TRUE_STRING); ConfigMap properties = endpointConfig.getPropertyAsMap(PROPERTIES_ELEMENT, null); if (properties == null) { properties = new ConfigMap(); endpointConfig.addProperty(PROPERTIES_ELEMENT, properties); } properties.addProperty(ConfigurationConstants.LOGIN_AFTER_DISCONNECT_ELEMENT, loginAfterDisconnect); if (pollingEnabled) { ConfigMap pollingEnabled = new ConfigMap(); // Adding as a value rather than attribute to the parent pollingEnabled.addProperty("","true"); properties.addProperty(POLLING_ENABLED,pollingEnabled); } if (pollingIntervalMillis > -1) { ConfigMap pollingInterval = new ConfigMap(); // Adding as a value rather than attribute to the parent pollingInterval.addProperty("",String.valueOf(pollingIntervalMillis)); properties.addProperty(POLLING_INTERVAL_MILLIS,pollingInterval); } if (piggybackingEnabled) { ConfigMap piggybackingEnabled = new ConfigMap(); // Adding as a value rather than attribute to the parent piggybackingEnabled.addProperty("",String.valueOf(piggybackingEnabled)); properties.addProperty(ConfigurationConstants.PIGGYBACKING_ENABLED_ELEMENT,piggybackingEnabled); } } return endpointConfig; } /** * Sets up monitoring of waited poll requests so they can be notified and * exit when the endpoint stops. * * @see flex.messaging.endpoints.AbstractEndpoint#start() */ public void start() { if (isStarted()) return; super.start(); currentWaitedRequests = new ConcurrentHashMap(); } /** * Ensures that no poll requests in a wait state are left un-notified when * the endpoint stops. * * @see flex.messaging.endpoints.AbstractEndpoint#stop() */ public void stop() { if (!isStarted()) return; // Notify any currently waiting polls. for (Object notifier : currentWaitedRequests.keySet()) { synchronized (notifier) { notifier.notifyAll(); // Break any current waits. } } currentWaitedRequests = null; super.stop(); } /** * @see flex.messaging.client.PollWaitListener#waitStart(Object) */ public void waitStart(Object notifier) { currentWaitedRequests.put(notifier,Boolean.TRUE); } /** * @see flex.messaging.client.PollWaitListener#waitEnd(Object) */ public void waitEnd(Object notifier) { if (currentWaitedRequests != null) currentWaitedRequests.remove(notifier); } // -------------------------------------------------------------------------- // // Protected Methods // // -------------------------------------------------------------------------- /** * Overrides the base poll handling to support optionally putting Http * request handling threads into a wait state until messages are available * to be delivered in the poll response or a timeout is reached. The number * of threads that may be put in a wait state is bounded by * <code>max-waiting-poll-requests</code> and waits will only be attempted * if the canWait flag that is based on the * <code>max-waiting-poll-requests</code> and the specified * <code>wait-interval</code> is true. * * @param flexClient * The FlexClient that issued the poll request. * @param pollCommand * The poll command from the client. * @return The flush info used to build the poll response. */ protected FlushResult handleFlexClientPoll(FlexClient flexClient, CommandMessage pollCommand) { FlushResult flushResult = null; if (canWait && !pollCommand.headerExists(CommandMessage.SUPPRESS_POLL_WAIT_HEADER)) { FlexSession session = FlexContext.getFlexSession(); // If canWait is true it means we currently have less than the max // number of allowed waiting threads. // We need to protect writes/reads to the wait count with the // endpoint's lock. // Also, we have to be careful to handle the case where two threads // get to this point when only // one wait spot remains; one thread will win and the other needs to // revert to a non-waitable poll. boolean thisThreadCanWait; synchronized (lock) { ++waitingPollRequestsCount; if (waitingPollRequestsCount == maxWaitingPollRequests) { thisThreadCanWait = true; // This thread got the last wait // spot. canWait = false; } else if (waitingPollRequestsCount > maxWaitingPollRequests) { thisThreadCanWait = false; // This thread was beaten out for // the last spot. --waitingPollRequestsCount; // Decrement the count because // we're not going to try a poll // with wait. canWait = false; // All the wait spots are currently // occupied so prevent further attempts for // now. } else { // We haven't hit the limit yet, allow this thread to wait. thisThreadCanWait = true; } } // Check the max waiting connections per session count if (thisThreadCanWait) { String userAgentValue = FlexContext.getHttpRequest().getHeader(UserAgentManager.USER_AGENT_HEADER_NAME); UserAgentSettings agentSettings = userAgentManager.match(userAgentValue); synchronized (session) { if (agentSettings != null) { session.maxConnectionsPerSession = agentSettings.getMaxPersistentConnectionsPerSession(); } ++session.streamingConnectionsCount; if (session.streamingConnectionsCount <= session.maxConnectionsPerSession) { thisThreadCanWait = true; // We haven't hit the limit // yet, allow the wait. } else // (session.streamingConnectionsCount > // session.maxConnectionsPerSession) { thisThreadCanWait = false; // no more from this client --session.streamingConnectionsCount; } } if (!thisThreadCanWait) { // Decrement the waiting poll count, since this poll isn't // going to wait. synchronized (lock) { --waitingPollRequestsCount; if (waitingPollRequestsCount < maxWaitingPollRequests) canWait = true; } if (Log.isDebug()) { log.debug("Max long-polling requests per session limit (" + session.maxConnectionsPerSession + ") has been reached, this poll won't wait."); } } } if (thisThreadCanWait) { if (Log.isDebug()) log.debug("Number of waiting threads for endpoint with id '" + getId() + "' is " + waitingPollRequestsCount + "."); try { // Do we have async results? final HttpServletRequest req = request.get(); flushResult = (FlushResult)req.getAttribute("AsyncFlushResults"); if (flushResult == null) { // Nothing available. Try non waiting poll. flushResult = flexClient.poll(getId()); if (flushResult == null) { // Nothing available. Have we suspended before? final Continuation continuation = ContinuationSupport.getContinuation(req); if (waitInterval <= 0) { flushResult = flexClient.pollWithWait(getId(),FlexContext.getFlexSession(),this,waitInterval); } else if (continuation.isInitial()) { continuation.setTimeout(waitInterval * 2); continuation.suspend(); flexClient.pollAsync(getId(),new AsyncPollHandler() { public void asyncPollComplete(FlushResult flushResult) { req.setAttribute("AsyncFlushResults",flushResult); continuation.resume(); } },waitInterval); continuation.undispatch(); } else { flushResult = flexClient.pollWithWait(getId(),FlexContext.getFlexSession(),this,1); } } } if (flushResult != null) { // Prevent busy-polling due to multiple clients sharing // a session and swapping each other out too quickly. if ((flushResult instanceof PollFlushResult) && ((PollFlushResult)flushResult).isAvoidBusyPolling() && (flushResult.getNextFlushWaitTimeMillis() < DEFAULT_WAIT_FOR_EXCESS_POLL_WAIT_CLIENTS)) { // Force the client polling interval to match the // default defined in the client PollingChannel. flushResult.setNextFlushWaitTimeMillis(DEFAULT_WAIT_FOR_EXCESS_POLL_WAIT_CLIENTS); } else if ((clientWaitInterval > 0) && (flushResult.getNextFlushWaitTimeMillis() == 0)) { // If the FlushResult doesn't specify it's own flush // wait time, use the configured clientWaitInterval // if defined. flushResult.setNextFlushWaitTimeMillis(clientWaitInterval); } } } finally { // We're done waiting so decrement the count of waiting // threads and update the canWait flag if necessary synchronized (lock) { --waitingPollRequestsCount; if (waitingPollRequestsCount < maxWaitingPollRequests) canWait = true; } synchronized (session) { --session.streamingConnectionsCount; } if (Log.isDebug()) log.debug("Number of waiting threads for endpoint with id '" + getId() + "' is " + waitingPollRequestsCount + "."); } } } else if (Log.isDebug() && waitEnabled) { if (pollCommand.headerExists(CommandMessage.SUPPRESS_POLL_WAIT_HEADER)) log.debug("Suppressing poll wait for this request because it is part of a batch of messages to process."); else log.debug("Max waiting poll requests limit '" + maxWaitingPollRequests + "' has been reached for endpoint '" + getId() + "'. FlexClient with id '" + flexClient.getId() + "' will poll with no wait."); } // If we weren't able to do a poll with wait above for any reason just // run the base poll handling logic. if (flushResult == null) { flushResult = super.handleFlexClientPoll(flexClient,pollCommand); // If this is an excess poll request that we couldn't wait on, make // sure the client doesn't poll the endpoint too aggressively. // In this case, force a client wait to match the default polling // interval defined in the client PollingChannel. if (waitEnabled && (pollingIntervalMillis < DEFAULT_WAIT_FOR_EXCESS_POLL_WAIT_CLIENTS)) { if (flushResult == null) { flushResult = new FlushResult(); } flushResult.setNextFlushWaitTimeMillis(DEFAULT_WAIT_FOR_EXCESS_POLL_WAIT_CLIENTS); } } return flushResult; } }