Commit c6dd1ae4 authored by daryl herzmann's avatar daryl herzmann

Merge pull request #195 from guusdk/OF-885

OF-885: Use non-blocking, async API for BOSH servlet
parents 8bc8c539 5a0a2e6b
...@@ -6,6 +6,7 @@ work/ ...@@ -6,6 +6,7 @@ work/
*.ipr *.ipr
.idea .idea
atlassian-ide-plugin.xml atlassian-ide-plugin.xml
out/
# Ignore Eclipse project files # Ignore Eclipse project files
.settings .settings
......
/**
* $RCSfile$
* $Revision: $
* $Date: $
*
* Copyright (C) 2005-2008 Jive Software. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.jivesoftware.openfire.http;
/**
* An exception which indicates that the maximum waiting time for a client response has been
* surpassed and an empty response should be returned to the requesting client.
*
* @author Alexander Wenckus
*/
class HttpBindTimeoutException extends Exception {
public HttpBindTimeoutException(String message) {
super(message);
}
public HttpBindTimeoutException() {
super();
}
}
...@@ -20,17 +20,16 @@ ...@@ -20,17 +20,16 @@
package org.jivesoftware.openfire.http; package org.jivesoftware.openfire.http;
import org.jivesoftware.util.JiveConstants;
import org.eclipse.jetty.continuation.Continuation;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
import javax.servlet.AsyncContext;
import java.io.IOException;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
/** /**
* Represents one HTTP connection with a client using the HTTP Binding service. The client will wait * Represents one HTTP connection with a client using the HTTP Binding service. The client will wait
* on {@link #getResponse()} until the server forwards a message to it or the wait time on the * on a response until the server forwards a message to it or the wait time on the
* session timeout. * session timeout.
* *
* @author Alexander Wenckus * @author Alexander Wenckus
...@@ -38,18 +37,16 @@ import java.security.cert.X509Certificate; ...@@ -38,18 +37,16 @@ import java.security.cert.X509Certificate;
public class HttpConnection { public class HttpConnection {
private static final Logger Log = LoggerFactory.getLogger(HttpConnection.class); private static final Logger Log = LoggerFactory.getLogger(HttpConnection.class);
private static final String RESPONSE_BODY = "response-body";
private static final String CONNECTION_CLOSED = "connection closed";
private final long requestId; private final long requestId;
private final X509Certificate[] sslCertificates; private final X509Certificate[] sslCertificates;
private final boolean isSecure; private final boolean isSecure;
private String body;
private HttpSession session; private HttpSession session;
private Continuation continuation;
private boolean isClosed; private boolean isClosed;
private final AsyncContext context;
/** /**
* Constructs an HTTP Connection. * Constructs an HTTP Connection.
* *
...@@ -57,25 +54,30 @@ public class HttpConnection { ...@@ -57,25 +54,30 @@ public class HttpConnection {
* @param isSecure true if this connection is using HTTPS * @param isSecure true if this connection is using HTTPS
* @param sslCertificates list of certificates presented by the client. * @param sslCertificates list of certificates presented by the client.
*/ */
public HttpConnection(long requestId, boolean isSecure, X509Certificate[] sslCertificates) { public HttpConnection(long requestId, boolean isSecure, X509Certificate[] sslCertificates, AsyncContext context) {
this.requestId = requestId; this.requestId = requestId;
this.isSecure = isSecure; this.isSecure = isSecure;
this.sslCertificates = sslCertificates; this.sslCertificates = sslCertificates;
this.context = context;
} }
/** /**
* The connection should be closed without delivering a stanza to the requestor. * The connection should be closed without delivering a stanza to the requestor.
*/ */
public void close() { public void close() {
if (isClosed) { synchronized (this) {
return; if (isClosed) {
return;
}
} }
try { try {
deliverBody(CONNECTION_CLOSED); deliverBody(null, true);
} }
catch (HttpConnectionClosedException e) { catch (HttpConnectionClosedException e) {
Log.warn("Unexpected exception occurred while trying to close an HttpException.", e); Log.warn("Unexpected exception occurred while trying to close an HttpException.", e);
} catch (IOException e) {
Log.warn("Unexpected exception occurred while trying to close an HttpException.", e);
} }
} }
...@@ -85,7 +87,7 @@ public class HttpConnection { ...@@ -85,7 +87,7 @@ public class HttpConnection {
* *
* @return true if this connection has been closed. * @return true if this connection has been closed.
*/ */
public boolean isClosed() { public synchronized boolean isClosed() {
return isClosed; return isClosed;
} }
...@@ -104,59 +106,25 @@ public class HttpConnection { ...@@ -104,59 +106,25 @@ public class HttpConnection {
* sent an empty body. * sent an empty body.
* *
* @param body the XMPP content to be forwarded to the client inside of a body tag. * @param body the XMPP content to be forwarded to the client inside of a body tag.
* @param async when false, this method blocks until the data has been delivered to the client.
* *
* @throws HttpConnectionClosedException when this connection to the client has already received * @throws HttpConnectionClosedException when this connection to the client has already received
* a deliverable to forward to the client * a deliverable to forward to the client
*/ */
public void deliverBody(String body) throws HttpConnectionClosedException { public void deliverBody(String body, boolean async) throws HttpConnectionClosedException, IOException {
// We only want to use this function once so we will close it when the body is delivered. // We only want to use this function once so we will close it when the body is delivered.
synchronized (this) { synchronized (this) {
if (isClosed) { if (isClosed) {
throw new HttpConnectionClosedException("The http connection is no longer " + throw new HttpConnectionClosedException("The http connection is no longer " +
"available to deliver content"); "available to deliver content");
} }
else { isClosed = true;
isClosed = true;
}
}
if (body == null) {
body = CONNECTION_CLOSED;
}
if (isSuspended()) {
continuation.setAttribute(RESPONSE_BODY, body);
continuation.resume();
session.incrementServerPacketCount();
}
else {
this.body = body;
} }
}
/** if (body == null) {
* A call that will suspend the request if there is no deliverable currently available. body = HttpBindServlet.createEmptyBody(false);
* Once the response becomes available, it is returned.
*
* @return the deliverable to send to the client
* @throws HttpBindTimeoutException to indicate that the maximum wait time requested by the
* client has been surpassed and an empty response should be returned.
*/
public String getResponse() throws HttpBindTimeoutException {
if (body == null && continuation != null) {
try {
body = waitForResponse();
}
catch (HttpBindTimeoutException e) {
this.isClosed = true;
throw e;
}
}
else if (body == null) {
throw new IllegalStateException("Continuation not set, cannot wait for deliverable.");
}
else if(CONNECTION_CLOSED.equals(body)) {
return null;
} }
return body; HttpBindServlet.respond(this.getSession(), this.context, body, async);
} }
/** /**
...@@ -195,41 +163,6 @@ public class HttpConnection { ...@@ -195,41 +163,6 @@ public class HttpConnection {
return sslCertificates; return sslCertificates;
} }
void setContinuation(Continuation continuation) {
this.continuation = continuation;
}
public boolean isSuspended() {
return continuation != null && continuation.isSuspended();
}
public boolean isExpired() {
return continuation != null && continuation.isExpired();
}
private String waitForResponse() throws HttpBindTimeoutException {
// we enter this method when we have no messages pending delivery
// when we resume a suspended continuation, or when we time out
if (continuation.isInitial()) {
continuation.setTimeout(session.getWait() * JiveConstants.SECOND);
continuation.suspend();
continuation.undispatch();
} else if (continuation.isResumed()) {
// This will occur when the hold attribute of a session has been exceeded.
String deliverable = (String) continuation.getAttribute(RESPONSE_BODY);
if (deliverable == null) {
throw new HttpBindTimeoutException();
}
else if(CONNECTION_CLOSED.equals(deliverable)) {
return null;
}
return deliverable;
}
throw new HttpBindTimeoutException("Request " + requestId + " exceeded response time from " +
"server of " + session.getWait() + " seconds.");
}
@Override @Override
public String toString() { public String toString() {
return (session != null ? session.toString() : "[Anonymous]") return (session != null ? session.toString() : "[Anonymous]")
......
...@@ -19,8 +19,8 @@ ...@@ -19,8 +19,8 @@
package org.jivesoftware.openfire.http; package org.jivesoftware.openfire.http;
import java.io.IOException;
import java.net.InetAddress; import java.net.InetAddress;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
...@@ -33,19 +33,17 @@ import java.util.concurrent.atomic.AtomicInteger; ...@@ -33,19 +33,17 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.dom4j.DocumentException; import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper; import org.dom4j.DocumentHelper;
import org.dom4j.Element; import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.SessionManager; import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID; import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.auth.UnauthorizedException; import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.util.JiveConstants; import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.JiveGlobals; import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.TaskEngine; import org.jivesoftware.util.TaskEngine;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import org.slf4j.LoggerFactory;
/** /**
* Manages sessions for all users connecting to Openfire using the HTTP binding protocal, * Manages sessions for all users connecting to Openfire using the HTTP binding protocol,
* <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>. * <a href="http://www.xmpp.org/extensions/xep-0124.html">XEP-0124</a>.
*/ */
public class HttpSessionManager { public class HttpSessionManager {
...@@ -78,7 +76,11 @@ public class HttpSessionManager { ...@@ -78,7 +76,11 @@ public class HttpSessionManager {
JiveGlobals.migrateProperty("xmpp.httpbind.worker.timeout"); JiveGlobals.migrateProperty("xmpp.httpbind.worker.timeout");
this.sessionManager = SessionManager.getInstance(); this.sessionManager = SessionManager.getInstance();
init();
}
public void init() {
Log.warn("HttpSessionManager.init() recreate sendPacketPool");
// Configure a pooled executor to handle async routing for incoming packets // Configure a pooled executor to handle async routing for incoming packets
// with a default size of 16 threads ("xmpp.httpbind.worker.threads"); also // with a default size of 16 threads ("xmpp.httpbind.worker.threads"); also
// uses an unbounded task queue and configurable keep-alive (default: 60 secs) // uses an unbounded task queue and configurable keep-alive (default: 60 secs)
...@@ -144,7 +146,7 @@ public class HttpSessionManager { ...@@ -144,7 +146,7 @@ public class HttpSessionManager {
/** /**
* Creates an HTTP binding session which will allow a user to exchange packets with Openfire. * Creates an HTTP binding session which will allow a user to exchange packets with Openfire.
* *
* @param address the internet address that was used to bind to Wildfie. * @param address the internet address that was used to bind to Openfire.
* @param rootNode the body element that was sent containing the request for a new session. * @param rootNode the body element that was sent containing the request for a new session.
* @param connection the HTTP connection object which abstracts the individual connections to * @param connection the HTTP connection object which abstracts the individual connections to
* Openfire over the HTTP binding protocol. The initial session creation response is returned to * Openfire over the HTTP binding protocol. The initial session creation response is returned to
...@@ -197,17 +199,21 @@ public class HttpSessionManager { ...@@ -197,17 +199,21 @@ public class HttpSessionManager {
String [] versionString = version.split("\\."); String [] versionString = version.split("\\.");
session.setMajorVersion(Integer.parseInt(versionString[0])); session.setMajorVersion(Integer.parseInt(versionString[0]));
session.setMinorVersion(Integer.parseInt(versionString[1])); session.setMinorVersion(Integer.parseInt(versionString[1]));
connection.setSession(session);
try { try {
connection.deliverBody(createSessionCreationResponse(session)); connection.deliverBody(createSessionCreationResponse(session), true);
} }
catch (HttpConnectionClosedException e) { catch (HttpConnectionClosedException e) {
/* This won't happen here. */ Log.error("Error creating session.", e);
throw new HttpBindException("Internal server error", BoshBindingError.internalServerError);
} }
catch (DocumentException e) { catch (DocumentException e) {
Log.error("Error creating document", e); Log.error("Error creating session.", e);
throw new HttpBindException("Internal server error", throw new HttpBindException("Internal server error", BoshBindingError.internalServerError);
BoshBindingError.internalServerError); } catch (IOException e) {
Log.error("Error creating session.", e);
throw new HttpBindException("Internal server error", BoshBindingError.internalServerError);
} }
return session; return session;
} }
...@@ -293,44 +299,6 @@ public class HttpSessionManager { ...@@ -293,44 +299,6 @@ public class HttpSessionManager {
return JiveGlobals.getIntProperty("xmpp.httpbind.client.idle.polling", 60); return JiveGlobals.getIntProperty("xmpp.httpbind.client.idle.polling", 60);
} }
/**
* Forwards a client request, which is related to a session, to the server. A connection is
* created and queued up in the provided session. When a connection reaches the top of a queue
* any pending packets bound for the client will be forwarded to the client through the
* connection.
*
* @param rid the unique, sequential, requestID sent from the client.
* @param session the HTTP session of the client that made the request.
* @param isSecure true if the request was made over a secure channel, HTTPS, and false if it
* was not.
* @param rootNode the XML body of the request.
* @return the created HTTP connection.
*
* @throws HttpBindException for several reasons: if the encoding inside of an auth packet is
* not recognized by the server, or if the packet type is not recognized.
* @throws HttpConnectionClosedException if the session is no longer available.
*/
public HttpConnection forwardRequest(long rid, HttpSession session, boolean isSecure,
Element rootNode) throws HttpBindException,
HttpConnectionClosedException
{
//noinspection unchecked
List<Element> elements = rootNode.elements();
boolean isPoll = (elements.size() == 0);
if ("terminate".equals(rootNode.attributeValue("type")))
isPoll = false;
else if ("true".equals(rootNode.attributeValue(new QName("restart", rootNode.getNamespaceForPrefix("xmpp")))))
isPoll = false;
else if (rootNode.attributeValue("pause") != null)
isPoll = false;
HttpConnection connection = session.createConnection(rid, elements, isSecure, isPoll);
if (elements.size() > 0) {
// creates the runnable to forward the packets
new HttpPacketSender(session).init();
}
return connection;
}
private HttpSession createSession(long rid, InetAddress address, HttpConnection connection) throws UnauthorizedException { private HttpSession createSession(long rid, InetAddress address, HttpConnection connection) throws UnauthorizedException {
// Create a ClientSession for this user. // Create a ClientSession for this user.
StreamID streamID = SessionManager.getInstance().nextStreamID(); StreamID streamID = SessionManager.getInstance().nextStreamID();
...@@ -354,19 +322,7 @@ public class HttpSessionManager { ...@@ -354,19 +322,7 @@ public class HttpSessionManager {
} }
} }
private double getDoubleAttribute(String doubleValue, double defaultValue) { private static String createSessionCreationResponse(HttpSession session) throws DocumentException {
if (doubleValue == null || "".equals(doubleValue.trim())) {
return defaultValue;
}
try {
return Double.parseDouble(doubleValue);
}
catch (Exception ex) {
return defaultValue;
}
}
private String createSessionCreationResponse(HttpSession session) throws DocumentException {
Element response = DocumentHelper.createElement("body"); Element response = DocumentHelper.createElement("body");
response.addNamespace("", "http://jabber.org/protocol/httpbind"); response.addNamespace("", "http://jabber.org/protocol/httpbind");
response.addNamespace("stream", "http://etherx.jabber.org/streams"); response.addNamespace("stream", "http://etherx.jabber.org/streams");
...@@ -417,23 +373,7 @@ public class HttpSessionManager { ...@@ -417,23 +373,7 @@ public class HttpSessionManager {
} }
} }
/** protected void execute(Runnable runnable) {
* A runner that guarantees that the packets per a session will be sent and this.sendPacketPool.execute(runnable);
* processed in the order in which they were received.
*/
private class HttpPacketSender implements Runnable {
private HttpSession session;
HttpPacketSender(HttpSession session) {
this.session = session;
}
public void run() {
session.sendPendingPackets();
}
private void init() {
sendPacketPool.execute(this);
}
} }
} }
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment