package org.cloudname.timber.client; import java.net.InetAddress; import java.net.InetSocketAddress; import java.util.concurrent.ConcurrentHashMap; /** * This class implements a reconnect delay manager. The purpose of * this class is to implement exponential backoff per target address * when doing reconnects. * * The reconnect delay for an address will double on every call until * it reaches a defined maximum. After that it will produce reconnect * delays that are constant in size. If reconnect delay has not been * requested for a defined time period, the delay will reset to the * initial value. * * This class is thread safe. * * @author borud */ public class ReconnectDelayManager { /** * The default initial reconnect delay. On the first reconnect * this is the number of milliseconds we will wait for. This * default can be overridden in the constructor. */ public static final int DEFAULT_RECONNECT_DELAY_INITIAL_MS = 500; /** * The default maximum reconnect delay. As the reconnect time * increases it will never go above this delay. This default can * be overridden in the constructor. */ public static final int DEFAULT_RECONNECT_MAX_DELAY_MS = 30000; /** * The default time before the delay time is reset to the initial * value. */ public static final int DEFAULT_RECONNECT_DELAY_RESET_TIME_MS = 60000; // Settings private final int initialReconnectDelayMs; private final int maxReconnectDelayMs; private final int reconnectDelayResetMs; private final TimeProvider timeProvider; // State private final ConcurrentHashMap<InetAddress, ReconnectItem> addressMap = new ConcurrentHashMap<InetAddress, ReconnectItem>(); // Interface to make time provider pluggable public static interface TimeProvider { public long currentTimeMillis(); } // Default TimeProvider implementation public static TimeProvider DEFAULT_TIMEPROVIDER = new TimeProvider() { @Override public long currentTimeMillis() { return System.currentTimeMillis(); } }; /** * This class manages the reconnect information for a given * InetAddress. Note that this class is not static -- it accesses * the reconnection settings from ReconnectDelayManager. * * This class is thread safe. * * @author borud */ private class ReconnectItem { private long lastReconnectTime = 0; private int lastReconnectDelay = initialReconnectDelayMs; /** * Get the reconnection delay. Whenever you call this method * it updates the internal state of the ReconnectItem. */ public synchronized int getReconnectDelayMs() { long now = timeProvider.currentTimeMillis(); long diff = now - lastReconnectTime; // If the last reconnect was longer than reconnectDelayResetMs // milliseconds ago, we reset the reconnect delay to // initialReconnectDelayMs if (diff >= reconnectDelayResetMs) { lastReconnectTime = now; lastReconnectDelay = initialReconnectDelayMs; return lastReconnectDelay; } // Double the last delay. lastReconnectDelay *= 2; // Cap delay to maxReconnectDelayMs if (lastReconnectDelay > maxReconnectDelayMs) { lastReconnectDelay = maxReconnectDelayMs; } lastReconnectTime = now; return lastReconnectDelay; } } /** * Create a ReconnectDelayManager with default settings. */ public ReconnectDelayManager() { this(DEFAULT_RECONNECT_DELAY_INITIAL_MS, DEFAULT_RECONNECT_MAX_DELAY_MS, DEFAULT_RECONNECT_DELAY_RESET_TIME_MS, DEFAULT_TIMEPROVIDER); } /** * Create a ReconnectDelayManager. * * @param initialReconnectDelayMs initial delay in milliseconds. * @param maxReconnectDelayMs maximum delay in milliseconds. * @param reconnectDelayResetMs number of milliseconds before the * delay is reset to {@code initialReconnectDelayMs} * @param timeProvider the time provider */ public ReconnectDelayManager(final int initialReconnectDelayMs, final int maxReconnectDelayMs, final int reconnectDelayResetMs, final TimeProvider timeProvider) { if (initialReconnectDelayMs < 0) { throw new IllegalArgumentException("Initial Reconnect Delay cannot be negative"); } if (maxReconnectDelayMs < initialReconnectDelayMs) { throw new IllegalArgumentException("Max reconnect Delay cannot be smaller than Initial Reconnect Delay"); } if (reconnectDelayResetMs < maxReconnectDelayMs) { throw new IllegalArgumentException("Reconnect Delay Reset cannot be smaller than Max Reconnect Delay"); } this.initialReconnectDelayMs = initialReconnectDelayMs; this.maxReconnectDelayMs = maxReconnectDelayMs; this.reconnectDelayResetMs = reconnectDelayResetMs; this.timeProvider = timeProvider; } /** * Get the reconnect delay for a given SocketAddress. Note that * this method disregards the port number so that the reconnect * delay is only for the InetAddress portion of the SocketAddress. * * @param socketAddress the SocketAddress for which we want the * reconnect timeout. * @return the time to delay reconnect in milliseconds. * @see #getReconnectDelayForAddress */ public int getReconnectDelayMs(InetSocketAddress socketAddress) { return getReconnectDelayMs(socketAddress.getAddress()); } /** * Get the reconnect delay for a given InetAddress. * * @param address the InetAddress for which we want the reconnect * timeout. * @return the time to delay reconnect in milliseconds. */ public int getReconnectDelayMs(InetAddress address) { ReconnectItem item = addressMap.get(address); if (null == item) { item = new ReconnectItem(); addressMap.putIfAbsent(address, item); } return item.getReconnectDelayMs(); } }