ReconnectionManager.java

  1. /**
  2.  *
  3.  * Copyright the original author or authors
  4.  *
  5.  * Licensed under the Apache License, Version 2.0 (the "License");
  6.  * you may not use this file except in compliance with the License.
  7.  * You may obtain a copy of the License at
  8.  *
  9.  *     http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.jivesoftware.smack;

  18. import org.jivesoftware.smack.XMPPException.StreamErrorException;
  19. import org.jivesoftware.smack.packet.StreamError;
  20. import org.jivesoftware.smack.util.Async;

  21. import java.lang.ref.WeakReference;
  22. import java.util.Map;
  23. import java.util.Random;
  24. import java.util.WeakHashMap;
  25. import java.util.logging.Level;
  26. import java.util.logging.Logger;
  27. /**
  28.  * Handles the automatic reconnection process. Every time a connection is dropped without
  29.  * the application explicitly closing it, the manager automatically tries to reconnect to
  30.  * the server.<p>
  31.  *
  32.  * The reconnection mechanism will try to reconnect periodically:
  33.  * <ol>
  34.  *  <li>For the first minute it will attempt to connect once every ten seconds.
  35.  *  <li>For the next five minutes it will attempt to connect once a minute.
  36.  *  <li>If that fails it will indefinitely try to connect once every five minutes.
  37.  * </ol>
  38.  *
  39.  * @author Francisco Vives
  40.  */
  41. public class ReconnectionManager {
  42.     private static final Logger LOGGER = Logger.getLogger(ReconnectionManager.class.getName());

  43.     private static final Map<AbstractXMPPConnection, ReconnectionManager> INSTANCES = new WeakHashMap<AbstractXMPPConnection, ReconnectionManager>();

  44.     /**
  45.      * Get a instance of ReconnectionManager for the given connection.
  46.      *
  47.      * @param connection
  48.      * @return a ReconnectionManager for the connection.
  49.      */
  50.     public static synchronized ReconnectionManager getInstanceFor(AbstractXMPPConnection connection) {
  51.         ReconnectionManager reconnectionManager = INSTANCES.get(connection);
  52.         if (reconnectionManager == null) {
  53.             reconnectionManager = new ReconnectionManager(connection);
  54.             INSTANCES.put(connection, reconnectionManager);
  55.         }
  56.         return reconnectionManager;
  57.     }

  58.     static {
  59.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  60.             public void connectionCreated(XMPPConnection connection) {
  61.                 if (connection instanceof AbstractXMPPConnection) {
  62.                     ReconnectionManager.getInstanceFor((AbstractXMPPConnection) connection);
  63.                 }
  64.             }
  65.         });
  66.     }

  67.     private static boolean enabledPerDefault = false;

  68.     /**
  69.      * Set if the automatic reconnection mechanism will be enabled per default for new XMPP connections. The default is
  70.      * 'false'.
  71.      *
  72.      * @param enabled
  73.      */
  74.     public static void setEnabledPerDefault(boolean enabled) {
  75.         enabledPerDefault = enabled;
  76.     }

  77.     /**
  78.      * Get the current default reconnection mechanism setting for new XMPP connections.
  79.      *
  80.      * @return true if new connection will come with an enabled reconnection mechanism
  81.      */
  82.     public static boolean getEnabledPerDefault() {
  83.         return enabledPerDefault;
  84.     }

  85.     // Holds the connection to the server
  86.     private final WeakReference<AbstractXMPPConnection> weakRefConnection;
  87.     private final int randomBase = new Random().nextInt(13) + 2; // between 2 and 15 seconds
  88.     private final Runnable reconnectionRunnable;

  89.     /**
  90.      * Flag that indicates if a reconnection should be attempted when abruptly disconnected
  91.      */
  92.     private boolean automaticReconnectEnabled = false;

  93.     boolean done = false;

  94.     private Thread reconnectionThread;

  95.     private ReconnectionManager(AbstractXMPPConnection connection) {
  96.         weakRefConnection = new WeakReference<AbstractXMPPConnection>(connection);

  97.         reconnectionRunnable = new Thread() {

  98.             /**
  99.              * Holds the current number of reconnection attempts
  100.              */
  101.             private int attempts = 0;

  102.             /**
  103.              * Returns the number of seconds until the next reconnection attempt.
  104.              *
  105.              * @return the number of seconds until the next reconnection attempt.
  106.              */
  107.             private int timeDelay() {
  108.                 attempts++;
  109.                 if (attempts > 13) {
  110.                     return randomBase * 6 * 5; // between 2.5 and 7.5 minutes (~5 minutes)
  111.                 }
  112.                 if (attempts > 7) {
  113.                     return randomBase * 6; // between 30 and 90 seconds (~1 minutes)
  114.                 }
  115.                 return randomBase; // 10 seconds
  116.             }

  117.             /**
  118.              * The process will try the reconnection until the connection succeed or the user cancel it
  119.              */
  120.             public void run() {
  121.                 final AbstractXMPPConnection connection = weakRefConnection.get();
  122.                 if (connection == null) {
  123.                     return;
  124.                 }
  125.                 // The process will try to reconnect until the connection is established or
  126.                 // the user cancel the reconnection process AbstractXMPPConnection.disconnect().
  127.                 while (isReconnectionPossible(connection)) {
  128.                     // Find how much time we should wait until the next reconnection
  129.                     int remainingSeconds = timeDelay();
  130.                     // Sleep until we're ready for the next reconnection attempt. Notify
  131.                     // listeners once per second about how much time remains before the next
  132.                     // reconnection attempt.
  133.                     while (isReconnectionPossible(connection) && remainingSeconds > 0) {
  134.                         try {
  135.                             Thread.sleep(1000);
  136.                             remainingSeconds--;
  137.                             for (ConnectionListener listener : connection.connectionListeners) {
  138.                                 listener.reconnectingIn(remainingSeconds);
  139.                             }
  140.                         }
  141.                         catch (InterruptedException e) {
  142.                             LOGGER.log(Level.FINE, "waiting for reconnection interrupted", e);
  143.                             break;
  144.                         }
  145.                     }

  146.                     for (ConnectionListener listener : connection.connectionListeners) {
  147.                         listener.reconnectingIn(0);
  148.                     }

  149.                     // Makes a reconnection attempt
  150.                     try {
  151.                         if (isReconnectionPossible(connection)) {
  152.                             connection.connect();
  153.                         }
  154.                     }
  155.                     catch (Exception e) {
  156.                         // Fires the failed reconnection notification
  157.                         for (ConnectionListener listener : connection.connectionListeners) {
  158.                             listener.reconnectionFailed(e);
  159.                         }
  160.                     }
  161.                 }
  162.             }
  163.         };

  164.         // If the reconnection mechanism is enable per default, enable it for this ReconnectionManager instance
  165.         if (getEnabledPerDefault()) {
  166.             enableAutomaticReconnection();
  167.         }
  168.     }

  169.     /**
  170.      * Enable the automatic reconnection mechanism. Does nothing if already enabled.
  171.      */
  172.     public synchronized void enableAutomaticReconnection() {
  173.         if (automaticReconnectEnabled) {
  174.             return;
  175.         }
  176.         XMPPConnection connection = weakRefConnection.get();
  177.         if (connection == null) {
  178.             throw new IllegalStateException("Connection instance no longer available");
  179.         }
  180.         connection.addConnectionListener(connectionListener);
  181.         automaticReconnectEnabled = true;
  182.     }

  183.     /**
  184.      * Disable the automatic reconnection mechanism. Does nothing if already disabled.
  185.      */
  186.     public synchronized void disableAutomaticReconnection() {
  187.         if (!automaticReconnectEnabled) {
  188.             return;
  189.         }
  190.         XMPPConnection connection = weakRefConnection.get();
  191.         if (connection == null) {
  192.             throw new IllegalStateException("Connection instance no longer available");
  193.         }
  194.         connection.removeConnectionListener(connectionListener);
  195.         automaticReconnectEnabled = false;
  196.     }

  197.     /**
  198.      * Returns if the automatic reconnection mechanism is enabled. You can disable the reconnection mechanism with
  199.      * {@link #disableAutomaticReconnection} and enable the mechanism with {@link #enableAutomaticReconnection()}.
  200.      *
  201.      * @return true, if the reconnection mechanism is enabled.
  202.      */
  203.     public boolean isAutomaticReconnectEnabled() {
  204.         return automaticReconnectEnabled;
  205.     }

  206.     /**
  207.      * Returns true if the reconnection mechanism is enabled.
  208.      *
  209.      * @return true if automatic reconnection is allowed.
  210.      */
  211.     private boolean isReconnectionPossible(XMPPConnection connection) {
  212.         return !done && !connection.isConnected()
  213.                 && isAutomaticReconnectEnabled();
  214.     }

  215.     /**
  216.      * Starts a reconnection mechanism if it was configured to do that.
  217.      * The algorithm is been executed when the first connection error is detected.
  218.      */
  219.     private synchronized void reconnect() {
  220.         XMPPConnection connection = this.weakRefConnection.get();
  221.         if (connection == null) {
  222.             LOGGER.fine("Connection is null, will not reconnect");
  223.             return;
  224.         }
  225.         // Since there is no thread running, creates a new one to attempt
  226.         // the reconnection.
  227.         // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
  228.         if (reconnectionThread != null && reconnectionThread.isAlive())
  229.             return;

  230.         reconnectionThread = Async.go(reconnectionRunnable,
  231.                         "Smack Reconnection Manager (" + connection.getConnectionCounter() + ')');
  232.     }

  233.     private final ConnectionListener connectionListener = new AbstractConnectionListener() {

  234.         @Override
  235.         public void connectionClosed() {
  236.             done = true;
  237.         }

  238.         @Override
  239.         public void authenticated(XMPPConnection connection, boolean resumed) {
  240.             done = false;
  241.         }

  242.         @Override
  243.         public void connectionClosedOnError(Exception e) {
  244.             done = false;
  245.             if (!isAutomaticReconnectEnabled()) {
  246.                 return;
  247.             }
  248.             if (e instanceof StreamErrorException) {
  249.                 StreamErrorException xmppEx = (StreamErrorException) e;
  250.                 StreamError error = xmppEx.getStreamError();

  251.                 if (StreamError.Condition.conflict == error.getCondition()) {
  252.                     return;
  253.                 }
  254.             }

  255.             reconnect();
  256.         }
  257.     };
  258. }