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 TODO javadoc me please
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 TODO javadoc me please
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 TODO javadoc me please
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 TODO javadoc me please
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            @Override
237            public void run() {
238                final AbstractXMPPConnection connection = weakRefConnection.get();
239                if (connection == null) {
240                    return;
241                }
242
243                // Reset attempts to zero since a new reconnection cycle is started once this runs.
244                attempts = 0;
245
246                // The process will try to reconnect until the connection is established or
247                // the user cancel the reconnection process AbstractXMPPConnection.disconnect().
248                while (isReconnectionPossible(connection)) {
249                    // Find how much time we should wait until the next reconnection
250                    int remainingSeconds = timeDelay();
251                    // Sleep until we're ready for the next reconnection attempt. Notify
252                    // listeners once per second about how much time remains before the next
253                    // reconnection attempt.
254                    while (remainingSeconds > 0) {
255                        if (!isReconnectionPossible(connection)) {
256                            return;
257                        }
258                        try {
259                            Thread.sleep(1000);
260                            remainingSeconds--;
261                            for (ReconnectionListener listener : reconnectionListeners) {
262                                listener.reconnectingIn(remainingSeconds);
263                            }
264                        }
265                        catch (InterruptedException e) {
266                            LOGGER.log(Level.FINE, "Reconnection Thread was interrupted, aborting reconnection mechanism", e);
267                            // Exit the reconnection thread in case it was interrupted.
268                            return;
269                        }
270                    }
271
272                    for (ReconnectionListener listener : reconnectionListeners) {
273                        listener.reconnectingIn(0);
274                    }
275
276                    if (!isReconnectionPossible(connection)) {
277                        return;
278                    }
279                    // Makes a reconnection attempt
280                    try {
281                        try {
282                            connection.connect();
283                        }
284                        catch (SmackException.AlreadyConnectedException e) {
285                            LOGGER.log(Level.FINER, "Connection was already connected on reconnection attempt", e);
286                        }
287                        connection.login();
288                    }
289                    catch (SmackException.AlreadyLoggedInException e) {
290                        // This can happen if another thread concurrently triggers a reconnection
291                        // and/or login. Obviously it should not be handled as a reconnection
292                        // failure. See also SMACK-725.
293                        LOGGER.log(Level.FINER, "Reconnection not required, was already logged in", e);
294                    }
295                    catch (SmackException | IOException | XMPPException e) {
296                        // Fires the failed reconnection notification
297                        for (ReconnectionListener listener : reconnectionListeners) {
298                            listener.reconnectionFailed(e);
299                        }
300                        // Failed to reconnect, try again.
301                        continue;
302                    } catch (InterruptedException e) {
303                        LOGGER.log(Level.FINE, "Reconnection Thread was interrupted, aborting reconnection mechanism", e);
304                        // Exit the reconnection thread in case it was interrupted.
305                        return;
306                    }
307
308                    // Successfully reconnected .
309                    return;
310                }
311            }
312        };
313
314        // If the reconnection mechanism is enable per default, enable it for this ReconnectionManager instance
315        if (getEnabledPerDefault()) {
316            enableAutomaticReconnection();
317        }
318    }
319
320    /**
321     * Enable the automatic reconnection mechanism. Does nothing if already enabled.
322     */
323    public synchronized void enableAutomaticReconnection() {
324        if (automaticReconnectEnabled) {
325            return;
326        }
327        XMPPConnection connection = weakRefConnection.get();
328        if (connection == null) {
329            throw new IllegalStateException("Connection instance no longer available");
330        }
331        connection.addConnectionListener(connectionListener);
332        automaticReconnectEnabled = true;
333    }
334
335    /**
336     * Disable the automatic reconnection mechanism. Does nothing if already disabled.
337     */
338    public synchronized void disableAutomaticReconnection() {
339        if (!automaticReconnectEnabled) {
340            return;
341        }
342        XMPPConnection connection = weakRefConnection.get();
343        if (connection == null) {
344            throw new IllegalStateException("Connection instance no longer available");
345        }
346        connection.removeConnectionListener(connectionListener);
347        automaticReconnectEnabled = false;
348    }
349
350    /**
351     * Returns if the automatic reconnection mechanism is enabled. You can disable the reconnection mechanism with
352     * {@link #disableAutomaticReconnection} and enable the mechanism with {@link #enableAutomaticReconnection()}.
353     *
354     * @return true, if the reconnection mechanism is enabled.
355     */
356    public synchronized boolean isAutomaticReconnectEnabled() {
357        return automaticReconnectEnabled;
358    }
359
360    /**
361     * Returns true if the reconnection mechanism is enabled.
362     *
363     * @return true if automatic reconnection is allowed.
364     */
365    private boolean isReconnectionPossible(XMPPConnection connection) {
366        return !done && !connection.isConnected()
367                && isAutomaticReconnectEnabled();
368    }
369
370    /**
371     * Starts a reconnection mechanism if it was configured to do that.
372     * The algorithm is been executed when the first connection error is detected.
373     */
374    private synchronized void reconnect() {
375        XMPPConnection connection = this.weakRefConnection.get();
376        if (connection == null) {
377            LOGGER.fine("Connection is null, will not reconnect");
378            return;
379        }
380        // Since there is no thread running, creates a new one to attempt
381        // the reconnection.
382        // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
383        if (reconnectionThread != null && reconnectionThread.isAlive())
384            return;
385
386        reconnectionThread = Async.go(reconnectionRunnable,
387                        "Smack Reconnection Manager (" + connection.getConnectionCounter() + ')');
388    }
389
390    /**
391     * Abort a possibly running reconnection mechanism.
392     *
393     * @since 4.2.2
394     */
395    public synchronized void abortPossiblyRunningReconnection() {
396        if (reconnectionThread == null) {
397            return;
398        }
399
400        reconnectionThread.interrupt();
401        reconnectionThread = null;
402    }
403
404    private final ConnectionListener connectionListener = new ConnectionListener() {
405
406        @Override
407        public void connectionClosed() {
408            done = true;
409        }
410
411        @Override
412        public void authenticated(XMPPConnection connection, boolean resumed) {
413            done = false;
414        }
415
416        @Override
417        public void connectionClosedOnError(Exception e) {
418            done = false;
419            if (!isAutomaticReconnectEnabled()) {
420                return;
421            }
422            if (e instanceof StreamErrorException) {
423                StreamErrorException xmppEx = (StreamErrorException) e;
424                StreamError error = xmppEx.getStreamError();
425
426                if (StreamError.Condition.conflict == error.getCondition()) {
427                    return;
428                }
429            }
430
431            reconnect();
432        }
433    };
434
435    /**
436     * Reconnection Policy, where {@link ReconnectionPolicy#RANDOM_INCREASING_DELAY} is the default policy used by smack and {@link ReconnectionPolicy#FIXED_DELAY} implies
437     * a fixed amount of time between reconnection attempts.
438     */
439    public enum ReconnectionPolicy {
440        /**
441         * Default policy classically used by smack, having an increasing delay related to the
442         * overall number of attempts.
443         */
444        RANDOM_INCREASING_DELAY,
445
446        /**
447         * Policy using fixed amount of time between reconnection attempts.
448         */
449        FIXED_DELAY,
450    }
451}