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}