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 java.io.IOException;
  19. import java.lang.ref.WeakReference;
  20. import java.util.Map;
  21. import java.util.Random;
  22. import java.util.Set;
  23. import java.util.WeakHashMap;
  24. import java.util.concurrent.CopyOnWriteArraySet;
  25. import java.util.logging.Level;
  26. import java.util.logging.Logger;

  27. import org.jivesoftware.smack.XMPPException.StreamErrorException;
  28. import org.jivesoftware.smack.packet.StreamError;
  29. import org.jivesoftware.smack.util.Async;

  30. /**
  31.  * Handles the automatic reconnection process. Every time a connection is dropped without
  32.  * the application explicitly closing it, the manager automatically tries to reconnect to
  33.  * the server.<p>
  34.  *
  35.  * There are two possible reconnection policies:
  36.  *
  37.  * {@link ReconnectionPolicy#RANDOM_INCREASING_DELAY} - The reconnection mechanism will try to reconnect periodically:
  38.  * <ol>
  39.  *  <li>For the first minute it will attempt to connect once every ten seconds.
  40.  *  <li>For the next five minutes it will attempt to connect once a minute.
  41.  *  <li>If that fails it will indefinitely try to connect once every five minutes.
  42.  * </ol>
  43.  *
  44.  * {@link ReconnectionPolicy#FIXED_DELAY} - The reconnection mechanism will try to reconnect after a fixed delay
  45.  * independently from the number of reconnection attempts already performed.
  46.  * <p>
  47.  * Interrupting the reconnection thread will abort the reconnection mechanism.
  48.  * </p>
  49.  *
  50.  * @author Francisco Vives
  51.  * @author Luca Stucchi
  52.  */
  53. public final class ReconnectionManager {
  54.     private static final Logger LOGGER = Logger.getLogger(ReconnectionManager.class.getName());

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

  56.     /**
  57.      * Get a instance of ReconnectionManager for the given connection.
  58.      *
  59.      * @param connection TODO javadoc me please
  60.      * @return a ReconnectionManager for the connection.
  61.      */
  62.     public static synchronized ReconnectionManager getInstanceFor(AbstractXMPPConnection connection) {
  63.         ReconnectionManager reconnectionManager = INSTANCES.get(connection);
  64.         if (reconnectionManager == null) {
  65.             reconnectionManager = new ReconnectionManager(connection);
  66.             INSTANCES.put(connection, reconnectionManager);
  67.         }
  68.         return reconnectionManager;
  69.     }

  70.     static {
  71.         XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
  72.             @Override
  73.             public void connectionCreated(XMPPConnection connection) {
  74.                 if (connection instanceof AbstractXMPPConnection) {
  75.                     ReconnectionManager.getInstanceFor((AbstractXMPPConnection) connection);
  76.                 }
  77.             }
  78.         });
  79.     }

  80.     private static boolean enabledPerDefault = false;

  81.     /**
  82.      * Set if the automatic reconnection mechanism will be enabled per default for new XMPP connections. The default is
  83.      * 'false'.
  84.      *
  85.      * @param enabled TODO javadoc me please
  86.      */
  87.     public static void setEnabledPerDefault(boolean enabled) {
  88.         enabledPerDefault = enabled;
  89.     }

  90.     /**
  91.      * Get the current default reconnection mechanism setting for new XMPP connections.
  92.      *
  93.      * @return true if new connection will come with an enabled reconnection mechanism
  94.      */
  95.     public static boolean getEnabledPerDefault() {
  96.         return enabledPerDefault;
  97.     }

  98.     private final Set<ReconnectionListener> reconnectionListeners = new CopyOnWriteArraySet<>();

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

  103.     private static int defaultFixedDelay = 15;
  104.     private static ReconnectionPolicy defaultReconnectionPolicy = ReconnectionPolicy.RANDOM_INCREASING_DELAY;

  105.     private volatile int fixedDelay = defaultFixedDelay;
  106.     private volatile ReconnectionPolicy reconnectionPolicy = defaultReconnectionPolicy;

  107.     /**
  108.      * Set the default fixed delay in seconds between the reconnection attempts. Also set the
  109.      * default connection policy to {@link ReconnectionPolicy#FIXED_DELAY}
  110.      *
  111.      * @param fixedDelay Delay expressed in seconds
  112.      */
  113.     public static void setDefaultFixedDelay(int fixedDelay) {
  114.         defaultFixedDelay = fixedDelay;
  115.         setDefaultReconnectionPolicy(ReconnectionPolicy.FIXED_DELAY);
  116.     }

  117.     /**
  118.      * Set the default Reconnection Policy to use.
  119.      *
  120.      * @param reconnectionPolicy TODO javadoc me please
  121.      */
  122.     public static void setDefaultReconnectionPolicy(ReconnectionPolicy reconnectionPolicy) {
  123.         defaultReconnectionPolicy = reconnectionPolicy;
  124.     }

  125.     /**
  126.      * Add a new reconnection listener.
  127.      *
  128.      * @param listener the listener to add
  129.      * @return <code>true</code> if the listener was not already added
  130.      * @since 4.2.2
  131.      */
  132.     public boolean addReconnectionListener(ReconnectionListener listener) {
  133.         return reconnectionListeners.add(listener);
  134.     }

  135.     /**
  136.      * Remove a reconnection listener.
  137.      * @param listener the listener to remove
  138.      * @return <code>true</code> if the listener was active and got removed.
  139.      * @since 4.2.2
  140.      */
  141.     public boolean removeReconnectionListener(ReconnectionListener listener) {
  142.         return reconnectionListeners.remove(listener);
  143.     }

  144.     /**
  145.      * Set the fixed delay in seconds between the reconnection attempts Also set the connection
  146.      * policy to {@link ReconnectionPolicy#FIXED_DELAY}.
  147.      *
  148.      * @param fixedDelay Delay expressed in seconds
  149.      */
  150.     public void setFixedDelay(int fixedDelay) {
  151.         this.fixedDelay = fixedDelay;
  152.         setReconnectionPolicy(ReconnectionPolicy.FIXED_DELAY);
  153.     }

  154.     /**
  155.      * Set the Reconnection Policy to use.
  156.      *
  157.      * @param reconnectionPolicy TODO javadoc me please
  158.      */
  159.     public void setReconnectionPolicy(ReconnectionPolicy reconnectionPolicy) {
  160.         this.reconnectionPolicy = reconnectionPolicy;
  161.     }

  162.     /**
  163.      * Flag that indicates if a reconnection should be attempted when abruptly disconnected.
  164.      */
  165.     private boolean automaticReconnectEnabled = false;

  166.     boolean done = false;

  167.     private Thread reconnectionThread;

  168.     private ReconnectionManager(AbstractXMPPConnection connection) {
  169.         weakRefConnection = new WeakReference<>(connection);

  170.         reconnectionRunnable = new Runnable() {

  171.             /**
  172.              * Holds the current number of reconnection attempts
  173.              */
  174.             private int attempts = 0;

  175.             /**
  176.              * Returns the number of seconds until the next reconnection attempt.
  177.              *
  178.              * @return the number of seconds until the next reconnection attempt.
  179.              */
  180.             private int timeDelay() {
  181.                 attempts++;

  182.                 // Delay variable to be assigned
  183.                 int delay;
  184.                 switch (reconnectionPolicy) {
  185.                 case FIXED_DELAY:
  186.                     delay = fixedDelay;
  187.                     break;
  188.                 case RANDOM_INCREASING_DELAY:
  189.                     if (attempts > 13) {
  190.                         delay = randomBase * 6 * 5; // between 2.5 and 7.5 minutes (~5 minutes)
  191.                     }
  192.                     else if (attempts > 7) {
  193.                         delay = randomBase * 6; // between 30 and 90 seconds (~1 minutes)
  194.                     }
  195.                     else {
  196.                         delay = randomBase; // 10 seconds
  197.                     }
  198.                     break;
  199.                 default:
  200.                     throw new AssertionError("Unknown reconnection policy " + reconnectionPolicy);
  201.                 }

  202.                 return delay;
  203.             }

  204.             /**
  205.              * The process will try the reconnection until the connection succeed or the user cancel it
  206.              */
  207.             @Override
  208.             public void run() {
  209.                 final AbstractXMPPConnection connection = weakRefConnection.get();
  210.                 if (connection == null) {
  211.                     return;
  212.                 }

  213.                 // Reset attempts to zero since a new reconnection cycle is started once this runs.
  214.                 attempts = 0;

  215.                 // The process will try to reconnect until the connection is established or
  216.                 // the user cancel the reconnection process AbstractXMPPConnection.disconnect().
  217.                 while (isReconnectionPossible(connection)) {
  218.                     // Find how much time we should wait until the next reconnection
  219.                     int remainingSeconds = timeDelay();
  220.                     // Sleep until we're ready for the next reconnection attempt. Notify
  221.                     // listeners once per second about how much time remains before the next
  222.                     // reconnection attempt.
  223.                     while (remainingSeconds > 0) {
  224.                         if (!isReconnectionPossible(connection)) {
  225.                             return;
  226.                         }
  227.                         try {
  228.                             Thread.sleep(1000);
  229.                             remainingSeconds--;
  230.                             for (ReconnectionListener listener : reconnectionListeners) {
  231.                                 listener.reconnectingIn(remainingSeconds);
  232.                             }
  233.                         }
  234.                         catch (InterruptedException e) {
  235.                             LOGGER.log(Level.FINE, "Reconnection Thread was interrupted, aborting reconnection mechanism", e);
  236.                             // Exit the reconnection thread in case it was interrupted.
  237.                             return;
  238.                         }
  239.                     }

  240.                     for (ReconnectionListener listener : reconnectionListeners) {
  241.                         listener.reconnectingIn(0);
  242.                     }

  243.                     if (!isReconnectionPossible(connection)) {
  244.                         return;
  245.                     }
  246.                     // Makes a reconnection attempt
  247.                     try {
  248.                         try {
  249.                             connection.connect();
  250.                         }
  251.                         catch (SmackException.AlreadyConnectedException e) {
  252.                             LOGGER.log(Level.FINER, "Connection was already connected on reconnection attempt", e);
  253.                         }
  254.                         connection.login();
  255.                     }
  256.                     catch (SmackException.AlreadyLoggedInException e) {
  257.                         // This can happen if another thread concurrently triggers a reconnection
  258.                         // and/or login. Obviously it should not be handled as a reconnection
  259.                         // failure. See also SMACK-725.
  260.                         LOGGER.log(Level.FINER, "Reconnection not required, was already logged in", e);
  261.                     }
  262.                     catch (SmackException | IOException | XMPPException e) {
  263.                         // Fires the failed reconnection notification
  264.                         for (ReconnectionListener listener : reconnectionListeners) {
  265.                             listener.reconnectionFailed(e);
  266.                         }
  267.                         // Failed to reconnect, try again.
  268.                         continue;
  269.                     } catch (InterruptedException e) {
  270.                         LOGGER.log(Level.FINE, "Reconnection Thread was interrupted, aborting reconnection mechanism", e);
  271.                         // Exit the reconnection thread in case it was interrupted.
  272.                         return;
  273.                     }

  274.                     // Successfully reconnected .
  275.                     return;
  276.                 }
  277.             }
  278.         };

  279.         // If the reconnection mechanism is enable per default, enable it for this ReconnectionManager instance
  280.         if (getEnabledPerDefault()) {
  281.             enableAutomaticReconnection();
  282.         }
  283.     }

  284.     /**
  285.      * Enable the automatic reconnection mechanism. Does nothing if already enabled.
  286.      */
  287.     public synchronized void enableAutomaticReconnection() {
  288.         if (automaticReconnectEnabled) {
  289.             return;
  290.         }
  291.         XMPPConnection connection = weakRefConnection.get();
  292.         if (connection == null) {
  293.             throw new IllegalStateException("Connection instance no longer available");
  294.         }
  295.         connection.addConnectionListener(connectionListener);
  296.         automaticReconnectEnabled = true;
  297.     }

  298.     /**
  299.      * Disable the automatic reconnection mechanism. Does nothing if already disabled.
  300.      */
  301.     public synchronized void disableAutomaticReconnection() {
  302.         if (!automaticReconnectEnabled) {
  303.             return;
  304.         }
  305.         XMPPConnection connection = weakRefConnection.get();
  306.         if (connection == null) {
  307.             throw new IllegalStateException("Connection instance no longer available");
  308.         }
  309.         connection.removeConnectionListener(connectionListener);
  310.         automaticReconnectEnabled = false;
  311.     }

  312.     /**
  313.      * Returns if the automatic reconnection mechanism is enabled. You can disable the reconnection mechanism with
  314.      * {@link #disableAutomaticReconnection} and enable the mechanism with {@link #enableAutomaticReconnection()}.
  315.      *
  316.      * @return true, if the reconnection mechanism is enabled.
  317.      */
  318.     public synchronized boolean isAutomaticReconnectEnabled() {
  319.         return automaticReconnectEnabled;
  320.     }

  321.     /**
  322.      * Returns true if the reconnection mechanism is enabled.
  323.      *
  324.      * @return true if automatic reconnection is allowed.
  325.      */
  326.     private boolean isReconnectionPossible(XMPPConnection connection) {
  327.         return !done && !connection.isConnected()
  328.                 && isAutomaticReconnectEnabled();
  329.     }

  330.     /**
  331.      * Starts a reconnection mechanism if it was configured to do that.
  332.      * The algorithm is been executed when the first connection error is detected.
  333.      */
  334.     private synchronized void reconnect() {
  335.         XMPPConnection connection = this.weakRefConnection.get();
  336.         if (connection == null) {
  337.             LOGGER.fine("Connection is null, will not reconnect");
  338.             return;
  339.         }
  340.         // Since there is no thread running, creates a new one to attempt
  341.         // the reconnection.
  342.         // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
  343.         if (reconnectionThread != null && reconnectionThread.isAlive())
  344.             return;

  345.         reconnectionThread = Async.go(reconnectionRunnable,
  346.                         "Smack Reconnection Manager (" + connection.getConnectionCounter() + ')');
  347.     }

  348.     /**
  349.      * Abort a possibly running reconnection mechanism.
  350.      *
  351.      * @since 4.2.2
  352.      */
  353.     public synchronized void abortPossiblyRunningReconnection() {
  354.         if (reconnectionThread == null) {
  355.             return;
  356.         }

  357.         reconnectionThread.interrupt();
  358.         reconnectionThread = null;
  359.     }

  360.     private final ConnectionListener connectionListener = new ConnectionListener() {

  361.         @Override
  362.         public void connectionClosed() {
  363.             done = true;
  364.         }

  365.         @Override
  366.         public void authenticated(XMPPConnection connection, boolean resumed) {
  367.             done = false;
  368.         }

  369.         @Override
  370.         public void connectionClosedOnError(Exception e) {
  371.             done = false;
  372.             if (!isAutomaticReconnectEnabled()) {
  373.                 return;
  374.             }
  375.             if (e instanceof StreamErrorException) {
  376.                 StreamErrorException xmppEx = (StreamErrorException) e;
  377.                 StreamError error = xmppEx.getStreamError();

  378.                 if (StreamError.Condition.conflict == error.getCondition()) {
  379.                     return;
  380.                 }
  381.             }

  382.             reconnect();
  383.         }
  384.     };

  385.     /**
  386.      * Reconnection Policy, where {@link ReconnectionPolicy#RANDOM_INCREASING_DELAY} is the default policy used by smack and {@link ReconnectionPolicy#FIXED_DELAY} implies
  387.      * a fixed amount of time between reconnection attempts.
  388.      */
  389.     public enum ReconnectionPolicy {
  390.         /**
  391.          * Default policy classically used by smack, having an increasing delay related to the
  392.          * overall number of attempts.
  393.          */
  394.         RANDOM_INCREASING_DELAY,

  395.         /**
  396.          * Policy using fixed amount of time between reconnection attempts.
  397.          */
  398.         FIXED_DELAY,
  399.     }
  400. }