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 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}