package net.i2p.router.transport; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.locks.ReentrantReadWriteLock; import net.i2p.I2PAppContext; import net.i2p.data.DataHelper; import net.i2p.router.transport.FIFOBandwidthLimiter.Request; import net.i2p.util.Log; /** * Thread that runs several times a second to "give" bandwidth to * FIFOBandwidthLimiter. * Instantiated by FIFOBandwidthLimiter. * * As of 0.8.12, this also contains a counter for outbound participating bandwidth. * This was a good place for it since we needed a thread for it. * * Public only for the properties and defaults. */ public class FIFOBandwidthRefiller implements Runnable { private final Log _log; private final I2PAppContext _context; private final FIFOBandwidthLimiter _limiter; /** how many KBps do we want to allow? */ private int _inboundKBytesPerSecond; /** how many KBps do we want to allow? */ private int _outboundKBytesPerSecond; /** how many KBps do we want to allow during burst? */ private int _inboundBurstKBytesPerSecond; /** how many KBps do we want to allow during burst? */ private int _outboundBurstKBytesPerSecond; /** when did we last replenish the queue? */ private long _lastRefillTime; /** when did we last check the config for updates? */ private long _lastCheckConfigTime; /** how frequently do we check the config for updates? */ private long _configCheckPeriodMs = 60*1000; private volatile boolean _isRunning; public static final String PROP_INBOUND_BANDWIDTH = "i2np.bandwidth.inboundKBytesPerSecond"; public static final String PROP_OUTBOUND_BANDWIDTH = "i2np.bandwidth.outboundKBytesPerSecond"; public static final String PROP_INBOUND_BURST_BANDWIDTH = "i2np.bandwidth.inboundBurstKBytesPerSecond"; public static final String PROP_OUTBOUND_BURST_BANDWIDTH = "i2np.bandwidth.outboundBurstKBytesPerSecond"; public static final String PROP_INBOUND_BANDWIDTH_PEAK = "i2np.bandwidth.inboundBurstKBytes"; public static final String PROP_OUTBOUND_BANDWIDTH_PEAK = "i2np.bandwidth.outboundBurstKBytes"; //public static final String PROP_REPLENISH_FREQUENCY = "i2np.bandwidth.replenishFrequencyMs"; // no longer allow unlimited bandwidth - the user must specify a value, else use defaults below (KBps) public static final int DEFAULT_INBOUND_BANDWIDTH = 300; /** * Caution, do not make DEFAULT_OUTBOUND_BANDWIDTH * DEFAULT_SHARE_PCT > 32 * without thinking about the implications (default connection limits, for example) * of moving the default bandwidth class from L to M, or maybe * adjusting bandwidth class boundaries. */ public static final int DEFAULT_OUTBOUND_BANDWIDTH = 60; public static final int DEFAULT_INBOUND_BURST_BANDWIDTH = 300; public static final int DEFAULT_OUTBOUND_BURST_BANDWIDTH = 60; public static final int DEFAULT_BURST_SECONDS = 60; /** For now, until there is some tuning and safe throttling, we set the floor at this inbound (KBps) */ public static final int MIN_INBOUND_BANDWIDTH = 5; /** For now, until there is some tuning and safe throttling, we set the floor at this outbound (KBps) */ public static final int MIN_OUTBOUND_BANDWIDTH = 5; /** For now, until there is some tuning and safe throttling, we set the floor at this during burst (KBps) */ public static final int MIN_INBOUND_BANDWIDTH_PEAK = 5; /** For now, until there is some tuning and safe throttling, we set the floor at this during burst (KBps) */ public static final int MIN_OUTBOUND_BANDWIDTH_PEAK = 5; /** * Max for reasonable Bloom filter false positive rate. * Do not increase without adding a new Bloom filter size! * See util/DecayingBloomFilter and tunnel/BloomFilterIVValidator. */ public static final int MAX_OUTBOUND_BANDWIDTH = 16384; /** * how often we replenish the queues. * the bandwidth limiter will get an update this often (ms) */ private static final long REPLENISH_FREQUENCY = 40; FIFOBandwidthRefiller(I2PAppContext context, FIFOBandwidthLimiter limiter) { _limiter = limiter; _context = context; _log = context.logManager().getLog(FIFOBandwidthRefiller.class); reinitialize(); _isRunning = true; } /** @since 0.8.8 */ synchronized void shutdown() { _isRunning = false; } public void run() { // bootstrap 'em with nothing _lastRefillTime = _limiter.now(); List<FIFOBandwidthLimiter.Request> buffer = new ArrayList<Request>(2); while (_isRunning) { long now = _limiter.now(); if (now >= _lastCheckConfigTime + _configCheckPeriodMs) { checkConfig(); now = _limiter.now(); _lastCheckConfigTime = now; } updateParticipating(now); boolean updated = updateQueues(buffer, now); if (updated) { _lastRefillTime = now; } try { Thread.sleep(REPLENISH_FREQUENCY); } catch (InterruptedException ie) {} } } synchronized void reinitialize() { _lastRefillTime = _limiter.now(); checkConfig(); _lastCheckConfigTime = _lastRefillTime; } private boolean updateQueues(List<FIFOBandwidthLimiter.Request> buffer, long now) { long numMs = (now - _lastRefillTime); if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating bandwidth after " + numMs + " (status: " + _limiter.getStatus().toString() + " rate in=" + _inboundKBytesPerSecond + ", out=" + _outboundKBytesPerSecond +")"); // clock skew if (numMs >= REPLENISH_FREQUENCY * 50 || numMs <= 0) numMs = REPLENISH_FREQUENCY; if (numMs >= REPLENISH_FREQUENCY) { long inboundToAdd = (1024*_inboundKBytesPerSecond * numMs)/1000; long outboundToAdd = (1024*_outboundKBytesPerSecond * numMs)/1000; if (inboundToAdd < 0) inboundToAdd = 0; if (outboundToAdd < 0) outboundToAdd = 0; /**** Always limited for now if (_inboundKBytesPerSecond <= 0) { _limiter.setInboundUnlimited(true); inboundToAdd = 0; } else { _limiter.setInboundUnlimited(false); } if (_outboundKBytesPerSecond <= 0) { _limiter.setOutboundUnlimited(true); outboundToAdd = 0; } else { _limiter.setOutboundUnlimited(false); } ****/ long maxBurstIn = ((_inboundBurstKBytesPerSecond-_inboundKBytesPerSecond)*1024*numMs)/1000; long maxBurstOut = ((_outboundBurstKBytesPerSecond-_outboundKBytesPerSecond)*1024*numMs)/1000; _limiter.refillBandwidthQueues(buffer, inboundToAdd, outboundToAdd, maxBurstIn, maxBurstOut); //if (_log.shouldLog(Log.DEBUG)) { // _log.debug("Adding " + inboundToAdd + " bytes to inboundAvailable"); // _log.debug("Adding " + outboundToAdd + " bytes to outboundAvailable"); //} return true; } else { if (_log.shouldLog(Log.DEBUG)) _log.debug("Refresh delay too fast (" + numMs + ")"); return false; } } private void checkConfig() { updateInboundRate(); updateOutboundRate(); updateInboundBurstRate(); updateOutboundBurstRate(); updateInboundPeak(); updateOutboundPeak(); // We are always limited for now //_limiter.setInboundUnlimited(_inboundKBytesPerSecond <= 0); //_limiter.setOutboundUnlimited(_outboundKBytesPerSecond <= 0); } private void updateInboundRate() { int in = _context.getProperty(PROP_INBOUND_BANDWIDTH, DEFAULT_INBOUND_BANDWIDTH); if (in != _inboundKBytesPerSecond) { // bandwidth was specified *and* changed if ( (in <= 0) || (in > MIN_INBOUND_BANDWIDTH) ) _inboundKBytesPerSecond = in; else _inboundKBytesPerSecond = MIN_INBOUND_BANDWIDTH; if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating inbound rate to " + _inboundKBytesPerSecond); } if (_inboundKBytesPerSecond <= 0) _inboundKBytesPerSecond = DEFAULT_INBOUND_BANDWIDTH; } private void updateOutboundRate() { int out = _context.getProperty(PROP_OUTBOUND_BANDWIDTH, DEFAULT_OUTBOUND_BANDWIDTH); if (out != _outboundKBytesPerSecond) { // bandwidth was specified *and* changed if (out >= MAX_OUTBOUND_BANDWIDTH) _outboundKBytesPerSecond = MAX_OUTBOUND_BANDWIDTH; else if ( (out <= 0) || (out >= MIN_OUTBOUND_BANDWIDTH) ) _outboundKBytesPerSecond = out; else _outboundKBytesPerSecond = MIN_OUTBOUND_BANDWIDTH; if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating outbound rate to " + _outboundKBytesPerSecond); } if (_outboundKBytesPerSecond <= 0) _outboundKBytesPerSecond = DEFAULT_OUTBOUND_BANDWIDTH; } private void updateInboundBurstRate() { int in = _context.getProperty(PROP_INBOUND_BURST_BANDWIDTH, DEFAULT_INBOUND_BURST_BANDWIDTH); if (in != _inboundBurstKBytesPerSecond) { // bandwidth was specified *and* changed if ( (in <= 0) || (in >= _inboundKBytesPerSecond) ) _inboundBurstKBytesPerSecond = in; else _inboundBurstKBytesPerSecond = _inboundKBytesPerSecond; if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating inbound burst rate to " + _inboundBurstKBytesPerSecond); } if (_inboundBurstKBytesPerSecond <= 0) _inboundBurstKBytesPerSecond = DEFAULT_INBOUND_BURST_BANDWIDTH; _limiter.setInboundBurstKBps(_inboundBurstKBytesPerSecond); } private void updateOutboundBurstRate() { int out = _context.getProperty(PROP_OUTBOUND_BURST_BANDWIDTH, DEFAULT_OUTBOUND_BURST_BANDWIDTH); if (out != _outboundBurstKBytesPerSecond) { // bandwidth was specified *and* changed if ( (out <= 0) || (out >= _outboundKBytesPerSecond) ) _outboundBurstKBytesPerSecond = out; else _outboundBurstKBytesPerSecond = _outboundKBytesPerSecond; if (_log.shouldLog(Log.DEBUG)) _log.debug("Updating outbound burst rate to " + _outboundBurstKBytesPerSecond); } if (_outboundBurstKBytesPerSecond <= 0) _outboundBurstKBytesPerSecond = DEFAULT_OUTBOUND_BURST_BANDWIDTH; _limiter.setOutboundBurstKBps(_outboundBurstKBytesPerSecond); } private void updateInboundPeak() { int in = _context.getProperty(PROP_INBOUND_BANDWIDTH_PEAK, DEFAULT_BURST_SECONDS * _inboundBurstKBytesPerSecond); if (in != _limiter.getInboundBurstBytes()) { // peak bw was specified *and* changed if (in >= MIN_INBOUND_BANDWIDTH_PEAK) { if (in < _inboundBurstKBytesPerSecond) _limiter.setInboundBurstBytes(_inboundBurstKBytesPerSecond * 1024); else _limiter.setInboundBurstBytes(in * 1024); } else { if (MIN_INBOUND_BANDWIDTH_PEAK < _inboundBurstKBytesPerSecond) _limiter.setInboundBurstBytes(_inboundBurstKBytesPerSecond * 1024); else _limiter.setInboundBurstBytes(MIN_INBOUND_BANDWIDTH_PEAK * 1024); } } } private void updateOutboundPeak() { int in = _context.getProperty(PROP_OUTBOUND_BANDWIDTH_PEAK, DEFAULT_BURST_SECONDS * _outboundBurstKBytesPerSecond); if (in != _limiter.getOutboundBurstBytes()) { // peak bw was specified *and* changed if (in >= MIN_OUTBOUND_BANDWIDTH_PEAK) { if (in < _outboundBurstKBytesPerSecond) _limiter.setOutboundBurstBytes(_outboundBurstKBytesPerSecond * 1024); else _limiter.setOutboundBurstBytes(in * 1024); } else { if (MIN_OUTBOUND_BANDWIDTH_PEAK < _outboundBurstKBytesPerSecond) _limiter.setOutboundBurstBytes(_outboundBurstKBytesPerSecond * 1024); else _limiter.setOutboundBurstBytes(MIN_OUTBOUND_BANDWIDTH_PEAK * 1024); } } } int getOutboundKBytesPerSecond() { return _outboundKBytesPerSecond; } int getInboundKBytesPerSecond() { return _inboundKBytesPerSecond; } int getOutboundBurstKBytesPerSecond() { return _outboundBurstKBytesPerSecond; } int getInboundBurstKBytesPerSecond() { return _inboundBurstKBytesPerSecond; } /** * Participating counter stuff below here * TOTAL_TIME needs to be high enough to get a burst without dropping * @since 0.8.12 */ private static final int TOTAL_TIME = 4000; private static final int PERIODS = TOTAL_TIME / (int) REPLENISH_FREQUENCY; /** count in current replenish period */ private final AtomicInteger _currentParticipating = new AtomicInteger(); private long _lastPartUpdateTime; private int _lastTotal; /** the actual length of last total period as coalesced (nominally TOTAL_TIME) */ private long _lastTotalTime; private int _lastIndex; /** buffer of count per replenish period, last is at _lastIndex, older at higher indexes (wraps) */ private final int[] _counts = new int[PERIODS]; /** the actual length of the period (nominally REPLENISH_FREQUENCY) */ private final long[] _times = new long[PERIODS]; private final ReentrantReadWriteLock _updateLock = new ReentrantReadWriteLock(false); /** * We sent a message. * * @param size bytes * @since 0.8.12 */ void incrementParticipatingMessageBytes(int size) { _currentParticipating.addAndGet(size); } /** * Out bandwidth. Actual bandwidth, not smoothed, not bucketed. * * @return Bps in recent period (a few seconds) * @since 0.8.12 */ int getCurrentParticipatingBandwidth() { _updateLock.readLock().lock(); try { return locked_getCurrentParticipatingBandwidth(); } finally { _updateLock.readLock().unlock(); } } private int locked_getCurrentParticipatingBandwidth() { int current = _currentParticipating.get(); long totalTime = (_limiter.now() - _lastPartUpdateTime) + _lastTotalTime; if (totalTime <= 0) return 0; // 1000 for ms->seconds in denominator long bw = 1000l * (current + _lastTotal) / totalTime; if (bw > Integer.MAX_VALUE) return 0; return (int) bw; } /** * Run once every replenish period * * @since 0.8.12 */ private void updateParticipating(long now) { _updateLock.writeLock().lock(); try { locked_updateParticipating(now); } finally { _updateLock.writeLock().unlock(); } } private void locked_updateParticipating(long now) { long elapsed = now - _lastPartUpdateTime; if (elapsed <= 0) { // glitch in the matrix _lastPartUpdateTime = now; return; } _lastPartUpdateTime = now; if (--_lastIndex < 0) _lastIndex = PERIODS - 1; _counts[_lastIndex] = _currentParticipating.getAndSet(0); _times[_lastIndex] = elapsed; _lastTotal = 0; _lastTotalTime = 0; // add up total counts and times for (int i = 0; i < PERIODS; i++) { int idx = (_lastIndex + i) % PERIODS; _lastTotal += _counts[idx]; _lastTotalTime += _times[idx]; if (_lastTotalTime >= TOTAL_TIME) break; } if (_lastIndex == 0 && _lastTotalTime > 0) { long bw = 1000l * _lastTotal / _lastTotalTime; _context.statManager().addRateData("tunnel.participatingBandwidthOut", bw); if (_lastTotal > 0 && _log.shouldLog(Log.INFO)) _log.info(DataHelper.formatSize(_lastTotal) + " bytes out part. tunnels in last " + _lastTotalTime + " ms: " + DataHelper.formatSize(bw) + " Bps"); } } }