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/
*.ipr
.idea
atlassian-ide-plugin.xml
out/
# Ignore Eclipse project files
.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 @@
package org.jivesoftware.openfire.http;
import org.jivesoftware.util.JiveConstants;
import org.eclipse.jetty.continuation.Continuation;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.AsyncContext;
import java.io.IOException;
import java.security.cert.X509Certificate;
/**
* 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.
*
* @author Alexander Wenckus
......@@ -38,18 +37,16 @@ import java.security.cert.X509Certificate;
public class HttpConnection {
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 X509Certificate[] sslCertificates;
private final boolean isSecure;
private String body;
private HttpSession session;
private Continuation continuation;
private boolean isClosed;
private final AsyncContext context;
/**
* Constructs an HTTP Connection.
*
......@@ -57,25 +54,30 @@ public class HttpConnection {
* @param isSecure true if this connection is using HTTPS
* @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.isSecure = isSecure;
this.sslCertificates = sslCertificates;
this.context = context;
}
/**
* The connection should be closed without delivering a stanza to the requestor.
*/
public void close() {
synchronized (this) {
if (isClosed) {
return;
}
}
try {
deliverBody(CONNECTION_CLOSED);
deliverBody(null, true);
}
catch (HttpConnectionClosedException 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 {
*
* @return true if this connection has been closed.
*/
public boolean isClosed() {
public synchronized boolean isClosed() {
return isClosed;
}
......@@ -104,59 +106,25 @@ public class HttpConnection {
* sent an empty body.
*
* @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
* 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.
synchronized (this) {
if (isClosed) {
throw new HttpConnectionClosedException("The http connection is no longer " +
"available to deliver content");
}
else {
isClosed = true;
}
}
if (body == null) {
body = CONNECTION_CLOSED;
}
if (isSuspended()) {
continuation.setAttribute(RESPONSE_BODY, body);
continuation.resume();
session.incrementServerPacketCount();
}
else {
this.body = body;
}
}
/**
* A call that will suspend the request if there is no deliverable currently available.
* 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;
if (body == null) {
body = HttpBindServlet.createEmptyBody(false);
}
return body;
HttpBindServlet.respond(this.getSession(), this.context, body, async);
}
/**
......@@ -195,41 +163,6 @@ public class HttpConnection {
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
public String toString() {
return (session != null ? session.toString() : "[Anonymous]")
......
......@@ -19,8 +19,8 @@
package org.jivesoftware.openfire.http;
import java.io.IOException;
import java.net.InetAddress;
import java.util.List;
import java.util.Map;
import java.util.TimerTask;
import java.util.concurrent.ConcurrentHashMap;
......@@ -33,19 +33,17 @@ import java.util.concurrent.atomic.AtomicInteger;
import org.dom4j.DocumentException;
import org.dom4j.DocumentHelper;
import org.dom4j.Element;
import org.dom4j.QName;
import org.jivesoftware.openfire.SessionManager;
import org.jivesoftware.openfire.StreamID;
import org.jivesoftware.openfire.auth.UnauthorizedException;
import org.jivesoftware.util.JiveConstants;
import org.jivesoftware.util.JiveGlobals;
import org.jivesoftware.util.Log;
import org.jivesoftware.util.TaskEngine;
import org.slf4j.Logger;
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>.
*/
public class HttpSessionManager {
......@@ -78,7 +76,11 @@ public class HttpSessionManager {
JiveGlobals.migrateProperty("xmpp.httpbind.worker.timeout");
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
// with a default size of 16 threads ("xmpp.httpbind.worker.threads"); also
// uses an unbounded task queue and configurable keep-alive (default: 60 secs)
......@@ -144,7 +146,7 @@ public class HttpSessionManager {
/**
* 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 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
......@@ -198,16 +200,20 @@ public class HttpSessionManager {
session.setMajorVersion(Integer.parseInt(versionString[0]));
session.setMinorVersion(Integer.parseInt(versionString[1]));
connection.setSession(session);
try {
connection.deliverBody(createSessionCreationResponse(session));
connection.deliverBody(createSessionCreationResponse(session), true);
}
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) {
Log.error("Error creating document", e);
throw new HttpBindException("Internal server error",
BoshBindingError.internalServerError);
Log.error("Error creating session.", e);
throw new HttpBindException("Internal server error", BoshBindingError.internalServerError);
} catch (IOException e) {
Log.error("Error creating session.", e);
throw new HttpBindException("Internal server error", BoshBindingError.internalServerError);
}
return session;
}
......@@ -293,44 +299,6 @@ public class HttpSessionManager {
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 {
// Create a ClientSession for this user.
StreamID streamID = SessionManager.getInstance().nextStreamID();
......@@ -354,19 +322,7 @@ public class HttpSessionManager {
}
}
private double getDoubleAttribute(String doubleValue, double defaultValue) {
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 {
private static String createSessionCreationResponse(HttpSession session) throws DocumentException {
Element response = DocumentHelper.createElement("body");
response.addNamespace("", "http://jabber.org/protocol/httpbind");
response.addNamespace("stream", "http://etherx.jabber.org/streams");
......@@ -417,23 +373,7 @@ public class HttpSessionManager {
}
}
/**
* A runner that guarantees that the packets per a session will be sent and
* 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);
}
protected void execute(Runnable runnable) {
this.sendPacketPool.execute(runnable);
}
}
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