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}