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                            connection.connect();
242                        }
243                        // TODO Starting with Smack 4.2, connect() will no
244                        // longer login automatically. So change this and the
245                        // previous lines to connection.connect().login() in the
246                        // 4.2, or any later, branch.
247                        if (!connection.isAuthenticated()) {
248                            connection.login();
249                        }
250                        // Successfully reconnected.
251                        attempts = 0;
252                    }
253                    catch (SmackException | IOException | XMPPException e) {
254                        // Fires the failed reconnection notification
255                        for (ConnectionListener listener : connection.connectionListeners) {
256                            listener.reconnectionFailed(e);
257                        }
258                    }
259                }
260            }
261        };
262
263        // If the reconnection mechanism is enable per default, enable it for this ReconnectionManager instance
264        if (getEnabledPerDefault()) {
265            enableAutomaticReconnection();
266        }
267    }
268
269    /**
270     * Enable the automatic reconnection mechanism. Does nothing if already enabled.
271     */
272    public synchronized void enableAutomaticReconnection() {
273        if (automaticReconnectEnabled) {
274            return;
275        }
276        XMPPConnection connection = weakRefConnection.get();
277        if (connection == null) {
278            throw new IllegalStateException("Connection instance no longer available");
279        }
280        connection.addConnectionListener(connectionListener);
281        automaticReconnectEnabled = true;
282    }
283
284    /**
285     * Disable the automatic reconnection mechanism. Does nothing if already disabled.
286     */
287    public synchronized void disableAutomaticReconnection() {
288        if (!automaticReconnectEnabled) {
289            return;
290        }
291        XMPPConnection connection = weakRefConnection.get();
292        if (connection == null) {
293            throw new IllegalStateException("Connection instance no longer available");
294        }
295        connection.removeConnectionListener(connectionListener);
296        automaticReconnectEnabled = false;
297    }
298
299    /**
300     * Returns if the automatic reconnection mechanism is enabled. You can disable the reconnection mechanism with
301     * {@link #disableAutomaticReconnection} and enable the mechanism with {@link #enableAutomaticReconnection()}.
302     *
303     * @return true, if the reconnection mechanism is enabled.
304     */
305    public boolean isAutomaticReconnectEnabled() {
306        return automaticReconnectEnabled;
307    }
308
309    /**
310     * Returns true if the reconnection mechanism is enabled.
311     *
312     * @return true if automatic reconnection is allowed.
313     */
314    private boolean isReconnectionPossible(XMPPConnection connection) {
315        return !done && !connection.isConnected()
316                && isAutomaticReconnectEnabled();
317    }
318
319    /**
320     * Starts a reconnection mechanism if it was configured to do that.
321     * The algorithm is been executed when the first connection error is detected.
322     */
323    private synchronized void reconnect() {
324        XMPPConnection connection = this.weakRefConnection.get();
325        if (connection == null) {
326            LOGGER.fine("Connection is null, will not reconnect");
327            return;
328        }
329        // Since there is no thread running, creates a new one to attempt
330        // the reconnection.
331        // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
332        if (reconnectionThread != null && reconnectionThread.isAlive())
333            return;
334
335        reconnectionThread = Async.go(reconnectionRunnable,
336                        "Smack Reconnection Manager (" + connection.getConnectionCounter() + ')');
337    }
338
339    private final ConnectionListener connectionListener = new AbstractConnectionListener() {
340
341        @Override
342        public void connectionClosed() {
343            done = true;
344        }
345
346        @Override
347        public void authenticated(XMPPConnection connection, boolean resumed) {
348            done = false;
349        }
350
351        @Override
352        public void connectionClosedOnError(Exception e) {
353            done = false;
354            if (!isAutomaticReconnectEnabled()) {
355                return;
356            }
357            if (e instanceof StreamErrorException) {
358                StreamErrorException xmppEx = (StreamErrorException) e;
359                StreamError error = xmppEx.getStreamError();
360
361                if (StreamError.Condition.conflict == error.getCondition()) {
362                    return;
363                }
364            }
365
366            reconnect();
367        }
368    };
369
370    /**
371     * Reconnection Policy, where {@link ReconnectionPolicy#RANDOM_INCREASING_DELAY} is the default policy used by smack and {@link ReconnectionPolicy#FIXED_DELAY} implies
372     * a fixed amount of time between reconnection attempts
373     */
374    public enum ReconnectionPolicy {
375        /**
376         * Default policy classically used by smack, having an increasing delay related to the
377         * overall number of attempts
378         */
379        RANDOM_INCREASING_DELAY,
380
381        /**
382         * Policy using fixed amount of time between reconnection attempts
383         */
384        FIXED_DELAY,
385        ;
386    }
387}