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