diff --git a/source/org/jivesoftware/smack/ReconnectionManager.java b/source/org/jivesoftware/smack/ReconnectionManager.java new file mode 100644 index 000000000..258989113 --- /dev/null +++ b/source/org/jivesoftware/smack/ReconnectionManager.java @@ -0,0 +1,234 @@ +package org.jivesoftware.smack; + +import java.util.ArrayList; +import java.util.List; + +/** + * Handles the reconnection process. Every time a connection is broken, it automatically + * tries to reconnect it. The reconnection is been executed when the first connection + * error is detected.

+ * + * The reconnection mechanism will try to reconnect periodically in this way: + *

    + *
  1. First it will try 6 times every 10 seconds. + *
  2. Then it will try 10 times every 1 minute. + *
  3. Finally it will try indefinitely every 5 minutes. + *
+ * + * @author Francisco Vives + */ +public class ReconnectionManager implements ConnectionListener { + + // Holds the time elapsed between each reconnection attempt + private int secondBetweenReconnection = 5 * 60; // 5 minutes + + // Holds the thread that produces a periodical reconnection. + private Thread reconnectionThread; + + // Holds the connection to the server + private XMPPConnection connection; + + // Holds the state of the reconnection + boolean done = false; + + static { + // Create a new PrivacyListManager on every established connection. In the init() + // method of PrivacyListManager, we'll add a listener that will delete the + // instance when the connection is closed. + XMPPConnection.addConnectionListener(new ConnectionEstablishedListener() { + public void connectionEstablished(XMPPConnection connection) { + connection.addConnectionListener(new ReconnectionManager(connection)); + } + }); + } + + private ReconnectionManager(XMPPConnection connection) { + this.connection = connection; + } + + + /** + * Returns if the reconnection mechanism is allowed to use. + */ + private boolean isReconnectionAllowed() { + return !done && !connection.isConnected() + && connection.getConfiguration().isReconnectionAllowed() + && connection.packetReader != null; + } + + /** + * Returns the time elapsed between each reconnection attempt. + * By default it will try to reconnect every 5 minutes. + * It is used when the client has lost the server connection and the XMPPConnection + * automatically tries to reconnect. + * + * @return Returns the number of seconds between reconnection. + */ + private int getSecondBetweenReconnection() { + return secondBetweenReconnection; + } + + /** + * Sets the time elapsed between each reconnection attempt. + * It is used when the client has lost the server connection and the XMPPConnection + * automatically tries to reconnect. + * + * @param secondBetweenReconnection The number of seconds between reconnection. + */ + protected void setSecondBetweenReconnection( + int secondBetweenReconnection) { + this.secondBetweenReconnection = secondBetweenReconnection; + } + + /** + * Starts a reconnection mechanism if it was configured to do that. + * The algorithm is been executed when the first connection error is detected. + *

+ * The reconnection mechanism will try to reconnect periodically in this way: + *

    + *
  1. First it will try 6 times every 10 seconds. + *
  2. Then it will try 10 times every 1 minute. + *
  3. Finally it will try indefinitely every 5 minutes. + *
+ */ + protected void reconnect() { + if (this.isReconnectionAllowed() && reconnectionThread == null) { + // Since there is no thread running, creates a new one to attempt + // the reconnection. + reconnectionThread = new Thread() { + /** + * Holds the number of reconnection attempts + */ + private int attempts = 0; + private int firstReconnectionPeriod = 7; // 6 attempts + private int secondReconnectionPeriod = 10 + firstReconnectionPeriod; // 16 attempts + private int firstReconnectionTime = 10; // 10 seconds + private int secondReconnectionTime = 1 * 60; // 1 minute + private int lastReconnectionTime = + getSecondBetweenReconnection(); // user defined in seconds + private int remainingSeconds = 0; // The seconds remaining to a reconnection + private int notificationPeriod = 1000; // 1 second + + /** + * Answer the time it should wait until the next reconnection + * attempt + */ + private int timeDelay() { + if (attempts > secondReconnectionPeriod) { + return lastReconnectionTime; // 5 minutes + } + if (attempts > firstReconnectionPeriod) { + return secondReconnectionTime; // 1 minute + } + return firstReconnectionTime; // 10 seconds + } + + /** + * The process will try the reconnection until the connection succeed or the user + * cancell it + */ + public void run() { + // The process will try to reconnect until the connection is established or + // the user cancel the reconnection process {@link XMPPConnection#disconnect()} + while (ReconnectionManager.this.isReconnectionAllowed()) { + // Indicate how much time will wait until next reconnection + remainingSeconds = timeDelay(); + // Notifies the remaining time until the next reconnection attempt + // every 1 second. + while (ReconnectionManager.this.isReconnectionAllowed() && + remainingSeconds > 0) { + try { + Thread.sleep(notificationPeriod); + remainingSeconds = remainingSeconds - 1; + ReconnectionManager.this + .notifyAttemptToReconnectIn(remainingSeconds); + } + catch (InterruptedException e1) { + e1.printStackTrace(); + // Notify the reconnection has failed + ReconnectionManager.this.notifyReconnectionFailed(e1); + } + } + // Waiting time have finished + + // Makes the reconnection attempt + try { + if (ReconnectionManager.this.isReconnectionAllowed()) { + // Attempts to reconnect. + connection.connect(); + } + } + catch (XMPPException e) { + // Fires the failed reconnection notification + ReconnectionManager.this.notifyReconnectionFailed(e); + } + } + } + }; + reconnectionThread.setName("Smack Reconnection Manager"); + reconnectionThread.setDaemon(true); + reconnectionThread.start(); + } + } + + /** + * Fires listeners when a reconnection attempt has failed. + */ + protected void notifyReconnectionFailed(Exception exception) { + List listenersCopy; + if (isReconnectionAllowed()) { + synchronized (connection.packetReader.connectionListeners) { + // Makes a copy since it's possible that a listener will be removed from the list + listenersCopy = new ArrayList( + connection.packetReader.connectionListeners); + for (ConnectionListener listener : listenersCopy) { + listener.reconnectionFailed(exception); + } + } + } + } + + /** + * Fires listeners when The XMPPConnection will retry a reconnection. Expressed in seconds. + */ + protected void notifyAttemptToReconnectIn(int seconds) { + List listenersCopy; + if (isReconnectionAllowed()) { + synchronized (connection.packetReader.connectionListeners) { + // Makes a copy since it's possible that a listener will be removed from the list + listenersCopy = new ArrayList( + connection.packetReader.connectionListeners); + for (ConnectionListener listener : listenersCopy) { + listener.reconnectingIn(seconds); + } + } + } + } + + public void connectionClosed() { + done = true; + } + + public void connectionClosedOnError(Exception e) { + done = false; + if (this.isReconnectionAllowed()) { + this.reconnect(); + } + } + + public void reconnectingIn(int seconds) { + // ignore + } + + public void reconnectionFailed(Exception e) { + // ignore + } + + /** + * The connection has successfull gotten connected. + */ + public void reconectionSuccessful() { + // ignore + } + +} diff --git a/test/org/jivesoftware/smack/ReconnectionTest.java b/test/org/jivesoftware/smack/ReconnectionTest.java new file mode 100644 index 000000000..b2341cfe8 --- /dev/null +++ b/test/org/jivesoftware/smack/ReconnectionTest.java @@ -0,0 +1,188 @@ +package org.jivesoftware.smack; + +import org.jivesoftware.smack.test.SmackTestCase; + +/** + * Tests the connection and reconnection mechanism + * + * @author Francisco Vives + */ + +public class ReconnectionTest extends SmackTestCase { + + public ReconnectionTest(String arg0) { + super(arg0); + } + + /** + * Tests an automatic reconnection. + * Simulates a connection error and then waits until gets reconnected. + */ + + public void testAutomaticReconnection() throws Exception { + XMPPConnection connection = getConnection(0); + XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); + connection.addConnectionListener(listener); + + // Simulates an error in the connection + connection.packetReader.notifyConnectionError(new Exception("Simulated Error")); + Thread.sleep(12000); + // After 10 seconds, the reconnection manager must reestablishes the connection + assertEquals("The ConnectionListener.connectionStablished() notification was not fired", + true, listener.reconnected); + assertEquals("The ConnectionListener.reconnectingIn() notification was not fired", 10, + listener.attemptsNotifications); + assertEquals("The ReconnectionManager algorithm has reconnected without waiting until 0", 0, + listener.remainingSeconds); + + // Executes some server interaction testing the connection + executeSomeServerInteraction(connection); + } + + /** + * Tests a manual reconnection. + * Simulates a connection error, disables the reconnection mechanism and then reconnects. + */ + public void testManualReconnectionWithCancelation() throws Exception { + XMPPConnection connection = getConnection(0); + XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); + connection.addConnectionListener(listener); + + // Produces a connection error + connection.packetReader.notifyConnectionError(new Exception("Simulated Error")); + assertEquals( + "An error occurs but the ConnectionListener.connectionClosedOnError(e) was not notified", + true, listener.connectionClosedOnError); + Thread.sleep(1000); + // Cancels the automatic reconnection + connection.getConfiguration().setReconnectionAllowed(false); + // Waits for a reconnection that must not happened. + Thread.sleep(10500); + // Cancels the automatic reconnection + assertEquals("The connection was stablished but it was not allowed to", false, + listener.reconnected); + + // Makes a manual reconnection from an error terminated connection without reconnection + connection.connect(); + + // Executes some server interaction testing the connection + executeSomeServerInteraction(connection); + } + + /** + * Tests a manual reconnection after a login. + * Closes the connection and then reconnects. + */ + public void testCloseAndManualReconnection() throws Exception { + XMPPConnection connection = getConnection(0); + String username = connection.getConfiguration().getUsername(); + String password = connection.getConfiguration().getPassword(); + XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); + connection.addConnectionListener(listener); + + // Produces a normal disconnection + connection.disconnect(); + assertEquals("ConnectionListener.connectionClosed() was not notified", + true, listener.connectionClosed); + // Waits 10 seconds waiting for a reconnection that must not happened. + Thread.sleep(12200); + assertEquals("The connection was stablished but it was not allowed to", false, + listener.reconnected); + + // Makes a manual reconnection + connection.connect(); + connection.login(username, password); + + // Executes some server interaction testing the connection + executeSomeServerInteraction(connection); + } + + /** + * Tests a reconnection in a anonymously logged connection. + * Closes the connection and then reconnects. + */ + public void testAnonymousReconnection() throws Exception { + XMPPConnection connection = createConnection(); + connection.connect(); + XMPPConnectionTestListener listener = new XMPPConnectionTestListener(); + connection.addConnectionListener(listener); + + // Makes the anounymous login + connection.loginAnonymously(); + + // Produces a normal disconnection + connection.disconnect(); + assertEquals("ConnectionListener.connectionClosed() was not notified", + true, listener.connectionClosed); + // Makes a manual reconnection + connection.connect(); + connection.loginAnonymously(); + assertEquals("Failed the manual connection", true, connection.isAnonymous()); + } + + private XMPPConnection createConnection() throws Exception { + XMPPConnection connection; + // Create the configuration + ConnectionConfiguration config = new ConnectionConfiguration(getHost(), getPort()); + config.setTLSEnabled(true); + config.setCompressionEnabled(Boolean.getBoolean("test.compressionEnabled")); + config.setSASLAuthenticationEnabled(true); + connection = new XMPPConnection(config, getSocketFactory()); + + return connection; + } + + /** + * Execute some server interaction in order to test that the regenerated connection works fine. + */ + private void executeSomeServerInteraction(XMPPConnection connection) throws XMPPException { + PrivacyListManager privacyManager = PrivacyListManager.getInstanceFor(connection); + privacyManager.getPrivacyLists(); + } + + protected int getMaxConnections() { + return 1; + } + + private class XMPPConnectionTestListener implements ConnectionListener { + + // Variables to support listener notifications verification + private boolean connectionClosed = false; + private boolean connectionClosedOnError = false; + private boolean reconnected = false; + private boolean reconnectionFailed = false; + private int remainingSeconds = 0; + private int attemptsNotifications = 0; + private boolean reconnectionCanceled = false; + + /** + * Methods to test the listener. + */ + public void connectionClosed() { + connectionClosed = true; + } + + public void connectionClosedOnError(Exception e) { + connectionClosedOnError = true; + } + + public void reconnectionCanceled() { + reconnectionCanceled = true; + } + + public void reconnectingIn(int seconds) { + attemptsNotifications = attemptsNotifications + 1; + remainingSeconds = seconds; + + } + + public void reconectionSuccessful() { + reconnected = true; + } + + public void reconnectionFailed(Exception error) { + reconnectionFailed = true; + } + } + +} \ No newline at end of file