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.WeakHashMap;
024import java.util.logging.Level;
025import java.util.logging.Logger;
026
027import org.jivesoftware.smack.XMPPException.StreamErrorException;
028import org.jivesoftware.smack.packet.StreamError;
029import org.jivesoftware.smack.util.Async;
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 final 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            @Override
074            public void connectionCreated(XMPPConnection connection) {
075                if (connection instanceof AbstractXMPPConnection) {
076                    ReconnectionManager.getInstanceFor((AbstractXMPPConnection) connection);
077                }
078            }
079        });
080    }
081
082    private static boolean enabledPerDefault = false;
083
084    /**
085     * Set if the automatic reconnection mechanism will be enabled per default for new XMPP connections. The default is
086     * 'false'.
087     * 
088     * @param enabled
089     */
090    public static void setEnabledPerDefault(boolean enabled) {
091        enabledPerDefault = enabled;
092    }
093
094    /**
095     * Get the current default reconnection mechanism setting for new XMPP connections.
096     *
097     * @return true if new connection will come with an enabled reconnection mechanism
098     */
099    public static boolean getEnabledPerDefault() {
100        return enabledPerDefault;
101    }
102
103    // Holds the connection to the server
104    private final WeakReference<AbstractXMPPConnection> weakRefConnection;
105    private final int randomBase = new Random().nextInt(13) + 2; // between 2 and 15 seconds
106    private final Runnable reconnectionRunnable;
107
108    private static int defaultFixedDelay = 15;
109    private static ReconnectionPolicy defaultReconnectionPolicy = ReconnectionPolicy.RANDOM_INCREASING_DELAY;
110
111    private volatile int fixedDelay = defaultFixedDelay;
112    private volatile ReconnectionPolicy reconnectionPolicy = defaultReconnectionPolicy;
113
114    /**
115     * Set the default fixed delay in seconds between the reconnection attempts. Also set the
116     * default connection policy to {@link ReconnectionPolicy#FIXED_DELAY}
117     * 
118     * @param fixedDelay Delay expressed in seconds
119     */
120    public static void setDefaultFixedDelay(int fixedDelay) {
121        defaultFixedDelay = fixedDelay;
122        setDefaultReconnectionPolicy(ReconnectionPolicy.FIXED_DELAY);
123    }
124
125    /**
126     * Set the default Reconnection Policy to use.
127     * 
128     * @param reconnectionPolicy
129     */
130    public static void setDefaultReconnectionPolicy(ReconnectionPolicy reconnectionPolicy) {
131        defaultReconnectionPolicy = reconnectionPolicy;
132    }
133
134    /**
135     * Set the fixed delay in seconds between the reconnection attempts Also set the connection
136     * policy to {@link ReconnectionPolicy#FIXED_DELAY}.
137     * 
138     * @param fixedDelay Delay expressed in seconds
139     */
140    public void setFixedDelay(int fixedDelay) {
141        this.fixedDelay = fixedDelay;
142        setReconnectionPolicy(ReconnectionPolicy.FIXED_DELAY);
143    }
144
145    /**
146     * Set the Reconnection Policy to use.
147     * 
148     * @param reconnectionPolicy
149     */
150    public void setReconnectionPolicy(ReconnectionPolicy reconnectionPolicy) {
151        this.reconnectionPolicy = reconnectionPolicy;
152    }
153
154    /**
155     * Flag that indicates if a reconnection should be attempted when abruptly disconnected.
156     */
157    private boolean automaticReconnectEnabled = false;
158
159    boolean done = false;
160
161    private Thread reconnectionThread;
162
163    private ReconnectionManager(AbstractXMPPConnection connection) {
164        weakRefConnection = new WeakReference<AbstractXMPPConnection>(connection);
165
166        reconnectionRunnable = new Thread() {
167
168            /**
169             * Holds the current number of reconnection attempts
170             */
171            private int attempts = 0;
172
173            /**
174             * Returns the number of seconds until the next reconnection attempt.
175             *
176             * @return the number of seconds until the next reconnection attempt.
177             */
178            private int timeDelay() {
179                attempts++;
180
181                // Delay variable to be assigned
182                int delay;
183                switch (reconnectionPolicy) {
184                case FIXED_DELAY:
185                    delay = fixedDelay;
186                    break;
187                case RANDOM_INCREASING_DELAY:
188                    if (attempts > 13) {
189                        delay = randomBase * 6 * 5; // between 2.5 and 7.5 minutes (~5 minutes)
190                    }
191                    else if (attempts > 7) {
192                        delay = randomBase * 6; // between 30 and 90 seconds (~1 minutes)
193                    }
194                    else {
195                        delay = randomBase; // 10 seconds
196                    }
197                    break;
198                default:
199                    throw new AssertionError("Unknown reconnection policy " + reconnectionPolicy);
200                }
201
202                return delay;
203            }
204
205            /**
206             * The process will try the reconnection until the connection succeed or the user cancel it
207             */
208            @Override
209            public void run() {
210                final AbstractXMPPConnection connection = weakRefConnection.get();
211                if (connection == null) {
212                    return;
213                }
214                // The process will try to reconnect until the connection is established or
215                // the user cancel the reconnection process AbstractXMPPConnection.disconnect().
216                while (isReconnectionPossible(connection)) {
217                    // Find how much time we should wait until the next reconnection
218                    int remainingSeconds = timeDelay();
219                    // Sleep until we're ready for the next reconnection attempt. Notify
220                    // listeners once per second about how much time remains before the next
221                    // reconnection attempt.
222                    while (isReconnectionPossible(connection) && remainingSeconds > 0) {
223                        try {
224                            Thread.sleep(1000);
225                            remainingSeconds--;
226                            for (ConnectionListener listener : connection.connectionListeners) {
227                                listener.reconnectingIn(remainingSeconds);
228                            }
229                        }
230                        catch (InterruptedException e) {
231                            LOGGER.log(Level.FINE, "waiting for reconnection interrupted", e);
232                            break;
233                        }
234                    }
235
236                    for (ConnectionListener listener : connection.connectionListeners) {
237                        listener.reconnectingIn(0);
238                    }
239
240                    // Makes a reconnection attempt
241                    try {
242                        if (isReconnectionPossible(connection)) {
243                            try {
244                                connection.connect();
245                            } catch (SmackException.AlreadyConnectedException e) {
246                                LOGGER.log(Level.FINER, "Connection was already connected on reconnection attempt", e);
247                            }
248                        }
249                        // TODO Starting with Smack 4.2, connect() will no
250                        // longer login automatically. So change this and the
251                        // previous lines to connection.connect().login() in the
252                        // 4.2, or any later, branch.
253                        if (!connection.isAuthenticated()) {
254                            connection.login();
255                        }
256                        // Successfully reconnected.
257                        attempts = 0;
258                    }
259                    catch (SmackException.AlreadyLoggedInException e) {
260                        // This can happen if another thread concurrently triggers a reconnection
261                        // and/or login. Obviously it should not be handled as a reconnection
262                        // failure. See also SMACK-725.
263                        LOGGER.log(Level.FINER, "Reconnection not required, was already logged in", e);
264                    }
265                    catch (SmackException | IOException | XMPPException | InterruptedException e) {
266                        // Fires the failed reconnection notification
267                        for (ConnectionListener listener : connection.connectionListeners) {
268                            listener.reconnectionFailed(e);
269                        }
270                    }
271                }
272            }
273        };
274
275        // If the reconnection mechanism is enable per default, enable it for this ReconnectionManager instance
276        if (getEnabledPerDefault()) {
277            enableAutomaticReconnection();
278        }
279    }
280
281    /**
282     * Enable the automatic reconnection mechanism. Does nothing if already enabled.
283     */
284    public synchronized void enableAutomaticReconnection() {
285        if (automaticReconnectEnabled) {
286            return;
287        }
288        XMPPConnection connection = weakRefConnection.get();
289        if (connection == null) {
290            throw new IllegalStateException("Connection instance no longer available");
291        }
292        connection.addConnectionListener(connectionListener);
293        automaticReconnectEnabled = true;
294    }
295
296    /**
297     * Disable the automatic reconnection mechanism. Does nothing if already disabled.
298     */
299    public synchronized void disableAutomaticReconnection() {
300        if (!automaticReconnectEnabled) {
301            return;
302        }
303        XMPPConnection connection = weakRefConnection.get();
304        if (connection == null) {
305            throw new IllegalStateException("Connection instance no longer available");
306        }
307        connection.removeConnectionListener(connectionListener);
308        automaticReconnectEnabled = false;
309    }
310
311    /**
312     * Returns if the automatic reconnection mechanism is enabled. You can disable the reconnection mechanism with
313     * {@link #disableAutomaticReconnection} and enable the mechanism with {@link #enableAutomaticReconnection()}.
314     *
315     * @return true, if the reconnection mechanism is enabled.
316     */
317    public boolean isAutomaticReconnectEnabled() {
318        return automaticReconnectEnabled;
319    }
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    /**
332     * Starts a reconnection mechanism if it was configured to do that.
333     * The algorithm is been executed when the first connection error is detected.
334     */
335    private synchronized void reconnect() {
336        XMPPConnection connection = this.weakRefConnection.get();
337        if (connection == null) {
338            LOGGER.fine("Connection is null, will not reconnect");
339            return;
340        }
341        // Since there is no thread running, creates a new one to attempt
342        // the reconnection.
343        // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
344        if (reconnectionThread != null && reconnectionThread.isAlive())
345            return;
346
347        reconnectionThread = Async.go(reconnectionRunnable,
348                        "Smack Reconnection Manager (" + connection.getConnectionCounter() + ')');
349    }
350
351    private final ConnectionListener connectionListener = new AbstractConnectionListener() {
352
353        @Override
354        public void connectionClosed() {
355            done = true;
356        }
357
358        @Override
359        public void authenticated(XMPPConnection connection, boolean resumed) {
360            done = false;
361        }
362
363        @Override
364        public void connectionClosedOnError(Exception e) {
365            done = false;
366            if (!isAutomaticReconnectEnabled()) {
367                return;
368            }
369            if (e instanceof StreamErrorException) {
370                StreamErrorException xmppEx = (StreamErrorException) e;
371                StreamError error = xmppEx.getStreamError();
372
373                if (StreamError.Condition.conflict == error.getCondition()) {
374                    return;
375                }
376            }
377
378            reconnect();
379        }
380    };
381
382    /**
383     * Reconnection Policy, where {@link ReconnectionPolicy#RANDOM_INCREASING_DELAY} is the default policy used by smack and {@link ReconnectionPolicy#FIXED_DELAY} implies
384     * a fixed amount of time between reconnection attempts.
385     */
386    public enum ReconnectionPolicy {
387        /**
388         * Default policy classically used by smack, having an increasing delay related to the
389         * overall number of attempts.
390         */
391        RANDOM_INCREASING_DELAY,
392
393        /**
394         * Policy using fixed amount of time between reconnection attempts.
395         */
396        FIXED_DELAY,
397        ;
398    }
399}