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}