Commit 7768e4fb authored by Dave Cridland's avatar Dave Cridland

Merge pull request #493 from guusdk/OF-1028

OF-1028 Stream Management refactoring
parents 8d2f0722 c8269f66
...@@ -190,21 +190,8 @@ public abstract class StanzaHandler { ...@@ -190,21 +190,8 @@ public abstract class StanzaHandler {
// resource binding and session establishment (to client sessions only) // resource binding and session establishment (to client sessions only)
waitingCompressionACK = true; waitingCompressionACK = true;
} }
} else if(isStreamManagementStanza(doc)) { } else if (isStreamManagementStanza(doc)) {
switch(tag) { session.getStreamManager().process( doc, session.getAddress() );
case "enable":
session.enableStreamMangement(doc);
break;
case "r":
session.getStreamManager().sendServerAcknowledgement();
break;
case "a":
session.getStreamManager().processClientAcknowledgement(doc);
break;
default:
process(doc);
break;
}
} }
else { else {
process(doc); process(doc);
......
...@@ -453,28 +453,6 @@ public abstract class LocalSession implements Session { ...@@ -453,28 +453,6 @@ public abstract class LocalSession implements Session {
return "NONE"; return "NONE";
} }
/**
* Enables stream management for session
* @param enable XEP-0198 <enable/> element
*/
public void enableStreamMangement(Element enable) {
// Do nothing if already enabled
if(streamManager.isEnabled()) {
return;
}
streamManager.setNamespace(enable.getNamespace().getStringValue());
// Ensure that resource binding has occurred
if(getAddress().getResource() == null) {
streamManager.sendUnexpectedError();
return;
}
streamManager.setEnabled(true);
}
@Override @Override
public final Locale getLanguage() { public final Locale getLanguage() {
return language; return language;
......
package org.jivesoftware.openfire.streammanagement; package org.jivesoftware.openfire.streammanagement;
import java.util.Date; import java.math.BigInteger;
import java.util.Deque; import java.net.UnknownHostException;
import java.util.LinkedList; import java.util.*;
import org.dom4j.Element; import org.dom4j.Element;
import org.jivesoftware.openfire.Connection; import org.jivesoftware.openfire.Connection;
...@@ -23,13 +23,15 @@ import org.xmpp.packet.PacketError; ...@@ -23,13 +23,15 @@ import org.xmpp.packet.PacketError;
* @author jonnyheavey * @author jonnyheavey
*/ */
public class StreamManager { public class StreamManager {
private static final Logger Log = LoggerFactory.getLogger(StreamManager.class);
private final Logger Log;
public static class UnackedPacket { public static class UnackedPacket {
public final Date timestamp; public final long x;
public final Date timestamp = new Date();
public final Packet packet; public final Packet packet;
public UnackedPacket(Date date, Packet p) { public UnackedPacket(long x, Packet p) {
timestamp = date; this.x = x;
packet = p; packet = p;
} }
} }
...@@ -46,22 +48,10 @@ public class StreamManager { ...@@ -46,22 +48,10 @@ public class StreamManager {
private final Connection connection; private final Connection connection;
/** /**
* Whether Stream Management is enabled for session
* the manager belongs to.
*/
private boolean enabled;
/**
* Namespace to be used in stanzas sent to client (depending on XEP-0198 version used by client) * Namespace to be used in stanzas sent to client (depending on XEP-0198 version used by client)
*/ */
private String namespace; private String namespace;
/**
* Count of how many stanzas/packets
* have been sent from the server to the client (not necessarily processed)
*/
private long serverSentStanzas = 0;
/** /**
* Count of how many stanzas/packets * Count of how many stanzas/packets
* sent from the client that the server has processed * sent from the client that the server has processed
...@@ -73,8 +63,8 @@ public class StreamManager { ...@@ -73,8 +63,8 @@ public class StreamManager {
* sent from the server that the client has processed * sent from the server that the client has processed
*/ */
private long clientProcessedStanzas = 0; private long clientProcessedStanzas = 0;
static private long mask = 0xFFFFFFFF; /* 2**32 - 1; this is used to emulate rollover */ static private long mask = new BigInteger("2").pow(32).longValue() - 1; // This is used to emulate rollover.
/** /**
* Collection of stanzas/packets sent to client that haven't been acknowledged. * Collection of stanzas/packets sent to client that haven't been acknowledged.
...@@ -82,26 +72,88 @@ public class StreamManager { ...@@ -82,26 +72,88 @@ public class StreamManager {
private Deque<UnackedPacket> unacknowledgedServerStanzas = new LinkedList<>(); private Deque<UnackedPacket> unacknowledgedServerStanzas = new LinkedList<>();
public StreamManager(Connection connection) { public StreamManager(Connection connection) {
String address;
try {
address = connection.getHostAddress();
}
catch ( UnknownHostException e )
{
address = null;
}
this.Log = LoggerFactory.getLogger(StreamManager.class + "["+ (address == null ? "(unknown address)" : address) +"]" );
this.connection = connection; this.connection = connection;
} }
/** /**
* Processes a stream management element.
*
* @param element The stream management element to be processed.
* @param onBehalfOf The (full) JID of the entity for which the element is processed.
*/
public void process( Element element, JID onBehalfOf )
{
switch(element.getName()) {
case "enable":
enable( onBehalfOf, element.getNamespace().getStringValue() );
break;
case "r":
sendServerAcknowledgement();
break;
case "a":
processClientAcknowledgement( element);
break;
default:
sendUnexpectedError();
}
}
/**
* Attempts to enable Stream Management for the entity identified by the provided JID.
*
* @param onBehalfOf The address of the entity for which SM is to be enabled.
* @param namespace The namespace that defines what version of SM is to be enabled.
*/
private void enable( JID onBehalfOf, String namespace )
{
// Ensure that resource binding has occurred.
if( onBehalfOf.getResource() == null ) {
sendUnexpectedError();
return;
}
synchronized ( this )
{
// Do nothing if already enabled
if ( isEnabled() )
{
return;
}
this.namespace = namespace;
}
// Send confirmation to the requestee.
connection.deliverRawText( String.format( "<enabled xmlns='%s'/>", namespace ) );
}
/**
* Sends XEP-0198 acknowledgement <a /> to client from server * Sends XEP-0198 acknowledgement <a /> to client from server
*/ */
public void sendServerAcknowledgement() { public void sendServerAcknowledgement() {
if(isEnabled()) { if(isEnabled()) {
String ack = String.format("<a xmlns='%s' h='%s' />", getNamespace(), getServerProcessedStanzas() & mask); String ack = String.format("<a xmlns='%s' h='%s' />", namespace, serverProcessedStanzas & mask);
getConnection().deliverRawText(ack); connection.deliverRawText( ack );
} }
} }
/** /**
* Sends XEP-0198 request <r /> to client from server * Sends XEP-0198 request <r /> to client from server
*/ */
private void sendServerRequest() { private void sendServerRequest() {
if(isEnabled()) { if(isEnabled()) {
String request = String.format("<r xmlns='%s' />", getNamespace()); String request = String.format("<r xmlns='%s' />", namespace);
getConnection().deliverRawText(request); connection.deliverRawText( request );
} }
} }
...@@ -109,59 +161,92 @@ public class StreamManager { ...@@ -109,59 +161,92 @@ public class StreamManager {
* Send an error if a XEP-0198 stanza is received at an unexpected time. * Send an error if a XEP-0198 stanza is received at an unexpected time.
* e.g. before resource-binding has completed. * e.g. before resource-binding has completed.
*/ */
public void sendUnexpectedError() { private void sendUnexpectedError() {
StringBuilder sb = new StringBuilder(340); connection.deliverRawText(
sb.append(String.format("<failed xmlns='%s'>", getNamespace())); String.format( "<failed xmlns='%s'>", namespace )
sb.append(new PacketError(PacketError.Condition.unexpected_request).toXML()); + new PacketError( PacketError.Condition.unexpected_request ).toXML()
sb.append("</failed>"); + "</failed>"
getConnection().deliverRawText(sb.toString()); );
} }
/** /**
* Receive and process acknowledgement packet from client * Receive and process acknowledgement packet from client
* @param ack XEP-0198 acknowledgement <a /> stanza to process * @param ack XEP-0198 acknowledgement <a /> stanza to process
*/ */
public void processClientAcknowledgement(Element ack) { private void processClientAcknowledgement(Element ack) {
if(isEnabled()) { if(isEnabled()) {
synchronized (this) { if (ack.attribute("h") != null) {
if (ack.attribute("h") != null) { final long h = Long.valueOf(ack.attributeValue("h"));
long count = Long.valueOf(ack.attributeValue("h"));
Log.debug( "Received acknowledgement from client: h={}", h );
synchronized (this) {
if ( !unacknowledgedServerStanzas.isEmpty() && h > unacknowledgedServerStanzas.getLast().x ) {
Log.warn( "Client acknowledges stanzas that we didn't sent! Client Ack h: {}, our last stanza: {}", h, unacknowledgedServerStanzas.getLast().x );
}
clientProcessedStanzas = h;
// Remove stanzas from temporary storage as now acknowledged // Remove stanzas from temporary storage as now acknowledged
Deque<UnackedPacket> unacknowledgedStanzas = getUnacknowledgedServerStanzas(); Log.trace( "Before processing client Ack (h={}): {} unacknowledged stanzas.", h, unacknowledgedServerStanzas.size() );
long i = getClientProcessedStanzas();
Log.debug("Ack: h={} mine={} length={}", count, i, unacknowledgedStanzas.size()); // Pop all acknowledged stanzas.
if (count < i) { while( !unacknowledgedServerStanzas.isEmpty() && unacknowledgedServerStanzas.getFirst().x <= h )
/* Consider rollover? */ {
Log.debug("Maybe rollover"); unacknowledgedServerStanzas.removeFirst();
if (i > mask) {
while (count < i) {
Log.debug("Rolling...");
count += mask + 1;
}
}
} }
while (i < count) {
unacknowledgedStanzas.removeFirst(); // Ensure that unacknowledged stanzas are purged after the client rolled over 'h' which occurs at h= (2^32)-1
i++; final int maxUnacked = getMaximumUnacknowledgedStanzas();
Log.debug("In Ack: h={} mine={} length={}", count, i, unacknowledgedStanzas.size()); final boolean clientHadRollOver = h < maxUnacked && !unacknowledgedServerStanzas.isEmpty() && unacknowledgedServerStanzas.getLast().x > mask - maxUnacked;
if ( clientHadRollOver )
{
Log.info( "Client rolled over 'h'. Purging high-numbered unacknowledged stanzas." );
while ( !unacknowledgedServerStanzas.isEmpty() && unacknowledgedServerStanzas.getLast().x > mask - maxUnacked)
{
unacknowledgedServerStanzas.removeLast();
}
} }
setClientProcessedStanzas(count); Log.trace( "After processing client Ack (h={}): {} unacknowledged stanzas.", h, unacknowledgedServerStanzas.size());
} }
} }
} }
} }
/**
* Registers that Openfire sends a stanza to the client (which is expected to be acknowledged later).
* @param packet The stanza that is sent.
*/
public void sentStanza(Packet packet) { public void sentStanza(Packet packet) {
if(isEnabled()) { if(isEnabled()) {
synchronized (this) { final long requestFrequency = JiveGlobals.getLongProperty( "stream.management.requestFrequency", 5 );
incrementServerSentStanzas(); final int size;
// Temporarily store packet until delivery confirmed
getUnacknowledgedServerStanzas().addLast(new StreamManager.UnackedPacket(new Date(), packet.createCopy())); synchronized (this)
Log.debug("Added stanza of type {}, now {} / {}", packet.getClass().getName(), getServerSentStanzas(), getUnacknowledgedServerStanzas().size()); {
// The next ID is one higher than the last stanza that was sent (which might be unacknowledged!)
final long x = 1 + ( unacknowledgedServerStanzas.isEmpty() ? clientProcessedStanzas : unacknowledgedServerStanzas.getLast().x );
unacknowledgedServerStanzas.addLast( new StreamManager.UnackedPacket( x, packet.createCopy() ) );
size = unacknowledgedServerStanzas.size();
Log.trace( "Added stanza of type '{}' to collection of unacknowledged stanzas (x={}). Collection size is now {}.", packet.getElement().getName(), x, size );
// Prevent keeping to many stanzas in memory.
if ( size > getMaximumUnacknowledgedStanzas() )
{
Log.warn( "To many stanzas go unacknowledged for this connection. Clearing queue and disabling functionality." );
namespace = null;
unacknowledgedServerStanzas.clear();
return;
}
} }
if(getServerSentStanzas() % JiveGlobals.getLongProperty("stream.management.requestFrequency", 5) == 0) {
// When we have a sizable amount of unacknowledged stanzas, request acknowledgement.
if ( size % requestFrequency == 0 ) {
Log.debug( "Requesting acknowledgement from peer, as we have {} or more unacknowledged stanzas.", requestFrequency );
sendServerRequest(); sendServerRequest();
} }
} }
...@@ -170,19 +255,16 @@ public class StreamManager { ...@@ -170,19 +255,16 @@ public class StreamManager {
public void onClose(PacketRouter router, JID serverAddress) { public void onClose(PacketRouter router, JID serverAddress) {
// Re-deliver unacknowledged stanzas from broken stream (XEP-0198) // Re-deliver unacknowledged stanzas from broken stream (XEP-0198)
if(isEnabled()) { synchronized (this) {
setEnabled(false); // Avoid concurrent usage. if(isEnabled()) {
synchronized (this) { namespace = null; // disable stream management.
Deque<StreamManager.UnackedPacket> unacknowledgedStanzas = getUnacknowledgedServerStanzas(); for (StreamManager.UnackedPacket unacked : unacknowledgedServerStanzas) {
if (!unacknowledgedStanzas.isEmpty()) { if (unacked.packet instanceof Message) {
for (StreamManager.UnackedPacket unacked : unacknowledgedStanzas) { Message m = (Message) unacked.packet;
if (unacked.packet instanceof Message) { if (m.getExtension("delay", "urn:xmpp:delay") == null) {
Message m = (Message) unacked.packet; Element delayInformation = m.addChildElement("delay", "urn:xmpp:delay");
if (m.getExtension("delay", "urn:xmpp:delay") == null) { delayInformation.addAttribute("stamp", XMPPDateTimeFormat.format(unacked.timestamp));
Element delayInformation = m.addChildElement("delay", "urn:xmpp:delay"); delayInformation.addAttribute("from", serverAddress.toBareJID());
delayInformation.addAttribute("stamp", XMPPDateTimeFormat.format(unacked.timestamp));
delayInformation.addAttribute("from", serverAddress.toBareJID());
}
} }
router.route(unacked.packet); router.route(unacked.packet);
} }
...@@ -192,75 +274,13 @@ public class StreamManager { ...@@ -192,75 +274,13 @@ public class StreamManager {
} }
/**
* Get connection (stream) for the session
* @return
*/
public Connection getConnection() {
return connection;
}
/** /**
* Determines whether Stream Management enabled for session this * Determines whether Stream Management enabled for session this
* manager belongs to. * manager belongs to.
* @return * @return true when stream management is enabled, otherwise false.
*/ */
public boolean isEnabled() { public boolean isEnabled() {
return enabled; return namespace != null;
}
/**
* Sets whether Stream Management enabled for session this
* manager belongs to.
* @param enabled
*/
synchronized public void setEnabled(boolean enabled) {
this.enabled = enabled;
if(enabled) {
String enabledStanza = String.format("<enabled xmlns='%s'/>", getNamespace());
getConnection().deliverRawText(enabledStanza);
}
}
/**
* Retrieve configured XEP-0198 namespace
* @return
*/
public String getNamespace() {
return namespace;
}
/**
* Configure XEP-0198 namespace
* @param namespace
*/
public void setNamespace(String namespace) {
this.namespace = namespace;
}
/**
* Retrieves number of stanzas sent to client by server.
* @return
*/
public long getServerSentStanzas() {
return serverSentStanzas;
}
/**
* Increments the count of stanzas sent to client by server.
*/
public void incrementServerSentStanzas() {
this.serverSentStanzas++;
}
/**
* Retrieve the number of stanzas processed by the server since
* Stream Management was enabled.
* @return
*/
public long getServerProcessedStanzas() {
return serverProcessedStanzas;
} }
/** /**
...@@ -274,30 +294,11 @@ public class StreamManager { ...@@ -274,30 +294,11 @@ public class StreamManager {
} }
/** /**
* Retrieve the number of stanzas processed by the client since * The maximum amount of stanzas we keep, waiting for ack.
* Stream Management was enabled. * @return The maximum number of stanzas.
* @return
*/ */
public long getClientProcessedStanzas() { private int getMaximumUnacknowledgedStanzas()
return clientProcessedStanzas; {
return JiveGlobals.getIntProperty( "stream.management.max-unacked", 10000 );
} }
/**
* Sets the count of stanzas processed by the client since
* Stream Management was enabled.
*/
public void setClientProcessedStanzas(long count) {
if(count >= clientProcessedStanzas) {
clientProcessedStanzas = count;
}
}
/**
* Retrieves all unacknowledged stanzas sent to client from server.
* @return
*/
public Deque<UnackedPacket> getUnacknowledgedServerStanzas() {
return unacknowledgedServerStanzas;
}
} }
...@@ -57,21 +57,8 @@ public class StreamManagementPacketRouter extends SessionPacketRouter { ...@@ -57,21 +57,8 @@ public class StreamManagementPacketRouter extends SessionPacketRouter {
@Override @Override
public void route(Element wrappedElement) throws UnknownStanzaException { public void route(Element wrappedElement) throws UnknownStanzaException {
String tag = wrappedElement.getName();
if (StreamManager.NAMESPACE_V3.equals(wrappedElement.getNamespace().getStringValue())) { if (StreamManager.NAMESPACE_V3.equals(wrappedElement.getNamespace().getStringValue())) {
switch(tag) { session.getStreamManager().process( wrappedElement, session.getAddress() );
case "enable":
session.enableStreamMangement(wrappedElement);
break;
case "r":
session.getStreamManager().sendServerAcknowledgement();
break;
case "a":
session.getStreamManager().processClientAcknowledgement(wrappedElement);
break;
default:
session.getStreamManager().sendUnexpectedError();
}
} else { } else {
super.route(wrappedElement); super.route(wrappedElement);
if (isUnsolicitedAckExpected()) { if (isUnsolicitedAckExpected()) {
......
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