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