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;
} }
} }
...@@ -45,23 +47,11 @@ public class StreamManager { ...@@ -45,23 +47,11 @@ 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
...@@ -74,7 +64,7 @@ public class StreamManager { ...@@ -74,7 +64,7 @@ public class StreamManager {
*/ */
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,16 +72,78 @@ public class StreamManager { ...@@ -82,16 +72,78 @@ 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 );
} }
} }
...@@ -100,8 +152,8 @@ public class StreamManager { ...@@ -100,8 +152,8 @@ public class StreamManager {
*/ */
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) {
long count = Long.valueOf(ack.attributeValue("h")); final long h = Long.valueOf(ack.attributeValue("h"));
// Remove stanzas from temporary storage as now acknowledged
Deque<UnackedPacket> unacknowledgedStanzas = getUnacknowledgedServerStanzas(); Log.debug( "Received acknowledgement from client: h={}", h );
long i = getClientProcessedStanzas(); synchronized (this) {
Log.debug("Ack: h={} mine={} length={}", count, i, unacknowledgedStanzas.size());
if (count < i) { if ( !unacknowledgedServerStanzas.isEmpty() && h > unacknowledgedServerStanzas.getLast().x ) {
/* Consider rollover? */ Log.warn( "Client acknowledges stanzas that we didn't sent! Client Ack h: {}, our last stanza: {}", h, unacknowledgedServerStanzas.getLast().x );
Log.debug("Maybe rollover");
if (i > mask) {
while (count < i) {
Log.debug("Rolling...");
count += mask + 1;
} }
clientProcessedStanzas = h;
// Remove stanzas from temporary storage as now acknowledged
Log.trace( "Before processing client Ack (h={}): {} unacknowledged stanzas.", h, unacknowledgedServerStanzas.size() );
// Pop all acknowledged stanzas.
while( !unacknowledgedServerStanzas.isEmpty() && unacknowledgedServerStanzas.getFirst().x <= h )
{
unacknowledgedServerStanzas.removeFirst();
} }
// Ensure that unacknowledged stanzas are purged after the client rolled over 'h' which occurs at h= (2^32)-1
final int maxUnacked = getMaximumUnacknowledgedStanzas();
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();
} }
while (i < count) {
unacknowledgedStanzas.removeFirst();
i++;
Log.debug("In Ack: h={} mine={} length={}", count, i, unacknowledgedStanzas.size());
} }
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,12 +255,10 @@ public class StreamManager { ...@@ -170,12 +255,10 @@ 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()) {
setEnabled(false); // Avoid concurrent usage.
synchronized (this) { synchronized (this) {
Deque<StreamManager.UnackedPacket> unacknowledgedStanzas = getUnacknowledgedServerStanzas(); if(isEnabled()) {
if (!unacknowledgedStanzas.isEmpty()) { namespace = null; // disable stream management.
for (StreamManager.UnackedPacket unacked : unacknowledgedStanzas) { for (StreamManager.UnackedPacket unacked : unacknowledgedServerStanzas) {
if (unacked.packet instanceof Message) { if (unacked.packet instanceof Message) {
Message m = (Message) unacked.packet; Message m = (Message) unacked.packet;
if (m.getExtension("delay", "urn:xmpp:delay") == null) { if (m.getExtension("delay", "urn:xmpp:delay") == null) {
...@@ -183,7 +266,6 @@ public class StreamManager { ...@@ -183,7 +266,6 @@ public class StreamManager {
delayInformation.addAttribute("stamp", XMPPDateTimeFormat.format(unacked.timestamp)); delayInformation.addAttribute("stamp", XMPPDateTimeFormat.format(unacked.timestamp));
delayInformation.addAttribute("from", serverAddress.toBareJID()); 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() {
return clientProcessedStanzas;
}
/**
* Sets the count of stanzas processed by the client since
* Stream Management was enabled.
*/ */
public void setClientProcessedStanzas(long count) { private int getMaximumUnacknowledgedStanzas()
if(count >= clientProcessedStanzas) { {
clientProcessedStanzas = count; return JiveGlobals.getIntProperty( "stream.management.max-unacked", 10000 );
} }
}
/**
* 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