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