001/**
002 *
003 * Copyright the original author or authors
004 *
005 * Licensed under the Apache License, Version 2.0 (the "License");
006 * you may not use this file except in compliance with the License.
007 * You may obtain a copy of the License at
008 *
009 *     http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.jivesoftware.smack;
018
019import org.jivesoftware.smack.XMPPException.StreamErrorException;
020import org.jivesoftware.smack.packet.StreamError;
021import org.jivesoftware.smack.util.Async;
022
023import java.io.IOException;
024import java.lang.ref.WeakReference;
025import java.util.Map;
026import java.util.Random;
027import java.util.WeakHashMap;
028import java.util.logging.Level;
029import java.util.logging.Logger;
030
031/**
032 * Handles the automatic reconnection process. Every time a connection is dropped without
033 * the application explicitly closing it, the manager automatically tries to reconnect to
034 * the server.<p>
035 *
036 * There are two possible reconnection policies:
037 *
038 * {@link ReconnectionPolicy#RANDOM_INCREASING_DELAY} - The reconnection mechanism will try to reconnect periodically:
039 * <ol>
040 *  <li>For the first minute it will attempt to connect once every ten seconds.
041 *  <li>For the next five minutes it will attempt to connect once a minute.
042 *  <li>If that fails it will indefinitely try to connect once every five minutes.
043 * </ol>
044 *
045 * {@link ReconnectionPolicy#FIXED_DELAY} - The reconnection mechanism will try to reconnect after a fixed delay 
046 * independently from the number of reconnection attempts already performed
047 *
048 * @author Francisco Vives
049 * @author Luca Stucchi
050 */
051public class ReconnectionManager {
052    private static final Logger LOGGER = Logger.getLogger(ReconnectionManager.class.getName());
053
054    private static final Map<AbstractXMPPConnection, ReconnectionManager> INSTANCES = new WeakHashMap<AbstractXMPPConnection, ReconnectionManager>();
055
056    /**
057     * Get a instance of ReconnectionManager for the given connection.
058     * 
059     * @param connection
060     * @return a ReconnectionManager for the connection.
061     */
062    public static synchronized ReconnectionManager getInstanceFor(AbstractXMPPConnection connection) {
063        ReconnectionManager reconnectionManager = INSTANCES.get(connection);
064        if (reconnectionManager == null) {
065            reconnectionManager = new ReconnectionManager(connection);
066            INSTANCES.put(connection, reconnectionManager);
067        }
068        return reconnectionManager;
069    }
070
071    static {
072        XMPPConnectionRegistry.addConnectionCreationListener(new ConnectionCreationListener() {
073            public void connectionCreated(XMPPConnection connection) {
074                if (connection instanceof AbstractXMPPConnection) {
075                    ReconnectionManager.getInstanceFor((AbstractXMPPConnection) connection);
076                }
077            }
078        });
079    }
080
081    private static boolean enabledPerDefault = false;
082
083    /**
084     * Set if the automatic reconnection mechanism will be enabled per default for new XMPP connections. The default is
085     * 'false'.
086     * 
087     * @param enabled
088     */
089    public static void setEnabledPerDefault(boolean enabled) {
090        enabledPerDefault = enabled;
091    }
092
093    /**
094     * Get the current default reconnection mechanism setting for new XMPP connections.
095     *
096     * @return true if new connection will come with an enabled reconnection mechanism
097     */
098    public static boolean getEnabledPerDefault() {
099        return enabledPerDefault;
100    }
101
102    // Holds the connection to the server
103    private final WeakReference<AbstractXMPPConnection> weakRefConnection;
104    private final int randomBase = new Random().nextInt(13) + 2; // between 2 and 15 seconds
105    private final Runnable reconnectionRunnable;
106
107    private static int defaultFixedDelay = 15;
108    private static ReconnectionPolicy defaultReconnectionPolicy = ReconnectionPolicy.RANDOM_INCREASING_DELAY;
109
110    private volatile int fixedDelay = defaultFixedDelay;
111    private volatile ReconnectionPolicy reconnectionPolicy = defaultReconnectionPolicy;
112
113    /**
114     * Set the default fixed delay in seconds between the reconnection attempts. Also set the
115     * default connection policy to {@link ReconnectionPolicy#FIXED_DELAY}
116     * 
117     * @param fixedDelay Delay expressed in seconds
118     */
119    public static void setDefaultFixedDelay(int fixedDelay) {
120        defaultFixedDelay = fixedDelay;
121        setDefaultReconnectionPolicy(ReconnectionPolicy.FIXED_DELAY);
122    }
123
124    /**
125     * Set the default Reconnection Policy to use
126     * 
127     * @param reconnectionPolicy
128     */
129    public static void setDefaultReconnectionPolicy(ReconnectionPolicy reconnectionPolicy) {
130        defaultReconnectionPolicy = reconnectionPolicy;
131    }
132
133    /**
134     * Set the fixed delay in seconds between the reconnection attempts Also set the connection
135     * policy to {@link ReconnectionPolicy#FIXED_DELAY}
136     * 
137     * @param fixedDelay Delay expressed in seconds
138     */
139    public void setFixedDelay(int fixedDelay) {
140        this.fixedDelay = fixedDelay;
141        setReconnectionPolicy(ReconnectionPolicy.FIXED_DELAY);
142    }
143
144    /**
145     * Set the Reconnection Policy to use
146     * 
147     * @param reconnectionPolicy
148     */
149    public void setReconnectionPolicy(ReconnectionPolicy reconnectionPolicy) {
150        this.reconnectionPolicy = reconnectionPolicy;
151    }
152
153    /**
154     * Flag that indicates if a reconnection should be attempted when abruptly disconnected
155     */
156    private boolean automaticReconnectEnabled = false;
157
158    boolean done = false;
159
160    private Thread reconnectionThread;
161
162    private ReconnectionManager(AbstractXMPPConnection connection) {
163        weakRefConnection = new WeakReference<AbstractXMPPConnection>(connection);
164
165        reconnectionRunnable = new Thread() {
166
167            /**
168             * Holds the current number of reconnection attempts
169             */
170            private int attempts = 0;
171
172            /**
173             * Returns the number of seconds until the next reconnection attempt.
174             *
175             * @return the number of seconds until the next reconnection attempt.
176             */
177            private int timeDelay() {
178                attempts++;
179
180                // Delay variable to be assigned
181                int delay;
182                switch (reconnectionPolicy) {
183                case FIXED_DELAY:
184                    delay = fixedDelay;
185                    break;
186                case RANDOM_INCREASING_DELAY:
187                    if (attempts > 13) {
188                        delay = randomBase * 6 * 5; // between 2.5 and 7.5 minutes (~5 minutes)
189                    }
190                    else if (attempts > 7) {
191                        delay = randomBase * 6; // between 30 and 90 seconds (~1 minutes)
192                    }
193                    else {
194                        delay = randomBase; // 10 seconds
195                    }
196                    break;
197                default:
198                    throw new AssertionError("Unknown reconnection policy " + reconnectionPolicy);
199                }
200
201                return delay;
202            }
203
204            /**
205             * The process will try the reconnection until the connection succeed or the user cancel it
206             */
207            public void run() {
208                final AbstractXMPPConnection connection = weakRefConnection.get();
209                if (connection == null) {
210                    return;
211                }
212                // The process will try to reconnect until the connection is established or
213                // the user cancel the reconnection process AbstractXMPPConnection.disconnect().
214                while (isReconnectionPossible(connection)) {
215                    // Find how much time we should wait until the next reconnection
216                    int remainingSeconds = timeDelay();
217                    // Sleep until we're ready for the next reconnection attempt. Notify
218                    // listeners once per second about how much time remains before the next
219                    // reconnection attempt.
220                    while (isReconnectionPossible(connection) && remainingSeconds > 0) {
221                        try {
222                            Thread.sleep(1000);
223                            remainingSeconds--;
224                            for (ConnectionListener listener : connection.connectionListeners) {
225                                listener.reconnectingIn(remainingSeconds);
226                            }
227                        }
228                        catch (InterruptedException e) {
229                            LOGGER.log(Level.FINE, "waiting for reconnection interrupted", e);
230                            break;
231                        }
232                    }
233
234                    for (ConnectionListener listener : connection.connectionListeners) {
235                        listener.reconnectingIn(0);
236                    }
237
238                    // Makes a reconnection attempt
239                    try {
240                        if (isReconnectionPossible(connection)) {
241                            try {
242                                connection.connect();
243                            } catch (SmackException.AlreadyConnectedException e) {
244                                LOGGER.log(Level.FINER, "Connection was already connected on reconnection attempt", e);
245                            }
246                        }
247                        // TODO Starting with Smack 4.2, connect() will no
248                        // longer login automatically. So change this and the
249                        // previous lines to connection.connect().login() in the
250                        // 4.2, or any later, branch.
251                        if (!connection.isAuthenticated()) {
252                            connection.login();
253                        }
254                        // Successfully reconnected.
255                        attempts = 0;
256                    }
257                    catch (SmackException.AlreadyLoggedInException e) {
258                        // This can happen if another thread concurrently triggers a reconnection
259                        // and/or login. Obviously it should not be handled as a reconnection
260                        // failure. See also SMACK-725.
261                        LOGGER.log(Level.FINER, "Reconnection not required, was already logged in", e);
262                    }
263                    catch (SmackException | IOException | XMPPException e) {
264                        // Fires the failed reconnection notification
265                        for (ConnectionListener listener : connection.connectionListeners) {
266                            listener.reconnectionFailed(e);
267                        }
268                    }
269                }
270            }
271        };
272
273        // If the reconnection mechanism is enable per default, enable it for this ReconnectionManager instance
274        if (getEnabledPerDefault()) {
275            enableAutomaticReconnection();
276        }
277    }
278
279    /**
280     * Enable the automatic reconnection mechanism. Does nothing if already enabled.
281     */
282    public synchronized void enableAutomaticReconnection() {
283        if (automaticReconnectEnabled) {
284            return;
285        }
286        XMPPConnection connection = weakRefConnection.get();
287        if (connection == null) {
288            throw new IllegalStateException("Connection instance no longer available");
289        }
290        connection.addConnectionListener(connectionListener);
291        automaticReconnectEnabled = true;
292    }
293
294    /**
295     * Disable the automatic reconnection mechanism. Does nothing if already disabled.
296     */
297    public synchronized void disableAutomaticReconnection() {
298        if (!automaticReconnectEnabled) {
299            return;
300        }
301        XMPPConnection connection = weakRefConnection.get();
302        if (connection == null) {
303            throw new IllegalStateException("Connection instance no longer available");
304        }
305        connection.removeConnectionListener(connectionListener);
306        automaticReconnectEnabled = false;
307    }
308
309    /**
310     * Returns if the automatic reconnection mechanism is enabled. You can disable the reconnection mechanism with
311     * {@link #disableAutomaticReconnection} and enable the mechanism with {@link #enableAutomaticReconnection()}.
312     *
313     * @return true, if the reconnection mechanism is enabled.
314     */
315    public boolean isAutomaticReconnectEnabled() {
316        return automaticReconnectEnabled;
317    }
318
319    /**
320     * Returns true if the reconnection mechanism is enabled.
321     *
322     * @return true if automatic reconnection is allowed.
323     */
324    private boolean isReconnectionPossible(XMPPConnection connection) {
325        return !done && !connection.isConnected()
326                && isAutomaticReconnectEnabled();
327    }
328
329    /**
330     * Starts a reconnection mechanism if it was configured to do that.
331     * The algorithm is been executed when the first connection error is detected.
332     */
333    private synchronized void reconnect() {
334        XMPPConnection connection = this.weakRefConnection.get();
335        if (connection == null) {
336            LOGGER.fine("Connection is null, will not reconnect");
337            return;
338        }
339        // Since there is no thread running, creates a new one to attempt
340        // the reconnection.
341        // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
342        if (reconnectionThread != null && reconnectionThread.isAlive())
343            return;
344
345        reconnectionThread = Async.go(reconnectionRunnable,
346                        "Smack Reconnection Manager (" + connection.getConnectionCounter() + ')');
347    }
348
349    private final ConnectionListener connectionListener = new AbstractConnectionListener() {
350
351        @Override
352        public void connectionClosed() {
353            done = true;
354        }
355
356        @Override
357        public void authenticated(XMPPConnection connection, boolean resumed) {
358            done = false;
359        }
360
361        @Override
362        public void connectionClosedOnError(Exception e) {
363            done = false;
364            if (!isAutomaticReconnectEnabled()) {
365                return;
366            }
367            if (e instanceof StreamErrorException) {
368                StreamErrorException xmppEx = (StreamErrorException) e;
369                StreamError error = xmppEx.getStreamError();
370
371                if (StreamError.Condition.conflict == error.getCondition()) {
372                    return;
373                }
374            }
375
376            reconnect();
377        }
378    };
379
380    /**
381     * Reconnection Policy, where {@link ReconnectionPolicy#RANDOM_INCREASING_DELAY} is the default policy used by smack and {@link ReconnectionPolicy#FIXED_DELAY} implies
382     * a fixed amount of time between reconnection attempts
383     */
384    public enum ReconnectionPolicy {
385        /**
386         * Default policy classically used by smack, having an increasing delay related to the
387         * overall number of attempts
388         */
389        RANDOM_INCREASING_DELAY,
390
391        /**
392         * Policy using fixed amount of time between reconnection attempts
393         */
394        FIXED_DELAY,
395        ;
396    }
397}