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                    if (attempts > 7) {
190                        delay = randomBase * 6; // between 30 and 90 seconds (~1 minutes)
191                    }
192                    delay = randomBase; // 10 seconds
193                    break;
194                default:
195                    throw new AssertionError("Unknown reconnection policy " + reconnectionPolicy);
196                }
197
198                return delay;
199            }
200
201            /**
202             * The process will try the reconnection until the connection succeed or the user cancel it
203             */
204            public void run() {
205                final AbstractXMPPConnection connection = weakRefConnection.get();
206                if (connection == null) {
207                    return;
208                }
209                // The process will try to reconnect until the connection is established or
210                // the user cancel the reconnection process AbstractXMPPConnection.disconnect().
211                while (isReconnectionPossible(connection)) {
212                    // Find how much time we should wait until the next reconnection
213                    int remainingSeconds = timeDelay();
214                    // Sleep until we're ready for the next reconnection attempt. Notify
215                    // listeners once per second about how much time remains before the next
216                    // reconnection attempt.
217                    while (isReconnectionPossible(connection) && remainingSeconds > 0) {
218                        try {
219                            Thread.sleep(1000);
220                            remainingSeconds--;
221                            for (ConnectionListener listener : connection.connectionListeners) {
222                                listener.reconnectingIn(remainingSeconds);
223                            }
224                        }
225                        catch (InterruptedException e) {
226                            LOGGER.log(Level.FINE, "waiting for reconnection interrupted", e);
227                            break;
228                        }
229                    }
230
231                    for (ConnectionListener listener : connection.connectionListeners) {
232                        listener.reconnectingIn(0);
233                    }
234
235                    // Makes a reconnection attempt
236                    try {
237                        if (isReconnectionPossible(connection)) {
238                            connection.connect();
239                        }
240                    }
241                    catch (Exception e) {
242                        // Fires the failed reconnection notification
243                        for (ConnectionListener listener : connection.connectionListeners) {
244                            listener.reconnectionFailed(e);
245                        }
246                    }
247                }
248            }
249        };
250
251        // If the reconnection mechanism is enable per default, enable it for this ReconnectionManager instance
252        if (getEnabledPerDefault()) {
253            enableAutomaticReconnection();
254        }
255    }
256
257    /**
258     * Enable the automatic reconnection mechanism. Does nothing if already enabled.
259     */
260    public synchronized void enableAutomaticReconnection() {
261        if (automaticReconnectEnabled) {
262            return;
263        }
264        XMPPConnection connection = weakRefConnection.get();
265        if (connection == null) {
266            throw new IllegalStateException("Connection instance no longer available");
267        }
268        connection.addConnectionListener(connectionListener);
269        automaticReconnectEnabled = true;
270    }
271
272    /**
273     * Disable the automatic reconnection mechanism. Does nothing if already disabled.
274     */
275    public synchronized void disableAutomaticReconnection() {
276        if (!automaticReconnectEnabled) {
277            return;
278        }
279        XMPPConnection connection = weakRefConnection.get();
280        if (connection == null) {
281            throw new IllegalStateException("Connection instance no longer available");
282        }
283        connection.removeConnectionListener(connectionListener);
284        automaticReconnectEnabled = false;
285    }
286
287    /**
288     * Returns if the automatic reconnection mechanism is enabled. You can disable the reconnection mechanism with
289     * {@link #disableAutomaticReconnection} and enable the mechanism with {@link #enableAutomaticReconnection()}.
290     *
291     * @return true, if the reconnection mechanism is enabled.
292     */
293    public boolean isAutomaticReconnectEnabled() {
294        return automaticReconnectEnabled;
295    }
296
297    /**
298     * Returns true if the reconnection mechanism is enabled.
299     *
300     * @return true if automatic reconnection is allowed.
301     */
302    private boolean isReconnectionPossible(XMPPConnection connection) {
303        return !done && !connection.isConnected()
304                && isAutomaticReconnectEnabled();
305    }
306
307    /**
308     * Starts a reconnection mechanism if it was configured to do that.
309     * The algorithm is been executed when the first connection error is detected.
310     */
311    private synchronized void reconnect() {
312        XMPPConnection connection = this.weakRefConnection.get();
313        if (connection == null) {
314            LOGGER.fine("Connection is null, will not reconnect");
315            return;
316        }
317        // Since there is no thread running, creates a new one to attempt
318        // the reconnection.
319        // avoid to run duplicated reconnectionThread -- fd: 16/09/2010
320        if (reconnectionThread != null && reconnectionThread.isAlive())
321            return;
322
323        reconnectionThread = Async.go(reconnectionRunnable,
324                        "Smack Reconnection Manager (" + connection.getConnectionCounter() + ')');
325    }
326
327    private final ConnectionListener connectionListener = new AbstractConnectionListener() {
328
329        @Override
330        public void connectionClosed() {
331            done = true;
332        }
333
334        @Override
335        public void authenticated(XMPPConnection connection, boolean resumed) {
336            done = false;
337        }
338
339        @Override
340        public void connectionClosedOnError(Exception e) {
341            done = false;
342            if (!isAutomaticReconnectEnabled()) {
343                return;
344            }
345            if (e instanceof StreamErrorException) {
346                StreamErrorException xmppEx = (StreamErrorException) e;
347                StreamError error = xmppEx.getStreamError();
348
349                if (StreamError.Condition.conflict == error.getCondition()) {
350                    return;
351                }
352            }
353
354            reconnect();
355        }
356    };
357
358    /**
359     * Reconnection Policy, where {@link ReconnectionPolicy#RANDOM_INCREASING_DELAY} is the default policy used by smack and {@link ReconnectionPolicy#FIXED_DELAY} implies
360     * a fixed amount of time between reconnection attempts
361     */
362    public enum ReconnectionPolicy {
363        /**
364         * Default policy classically used by smack, having an increasing delay related to the
365         * overall number of attempts
366         */
367        RANDOM_INCREASING_DELAY,
368
369        /**
370         * Policy using fixed amount of time between reconnection attempts
371         */
372        FIXED_DELAY,
373        ;
374    }
375}