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