forked from I2P_Developers/i2p.i2p
* I2CP:
- Add experimental bandwidth limiter - Add I2PSession API method to update tunnel and bandwidth configuration on an existing session - Filter more system properties before passing them to the router
This commit is contained in:
@ -12,6 +12,8 @@ package net.i2p.client;
|
|||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Properties;
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
import java.util.concurrent.locks.ReentrantLock;
|
||||||
|
|
||||||
import net.i2p.I2PAppContext;
|
import net.i2p.I2PAppContext;
|
||||||
import net.i2p.data.DataFormatException;
|
import net.i2p.data.DataFormatException;
|
||||||
@ -41,22 +43,51 @@ import net.i2p.util.Log;
|
|||||||
* @author jrandom
|
* @author jrandom
|
||||||
*/
|
*/
|
||||||
class I2CPMessageProducer {
|
class I2CPMessageProducer {
|
||||||
private final static Log _log = new Log(I2CPMessageProducer.class);
|
private final Log _log;
|
||||||
private final I2PAppContext _context;
|
private final I2PAppContext _context;
|
||||||
private int _sendBps;
|
private int _maxBytesPerSecond;
|
||||||
private long _sendPeriodBytes;
|
private volatile int _sendPeriodBytes;
|
||||||
private long _sendPeriodBeginTime;
|
private volatile long _sendPeriodBeginTime;
|
||||||
|
private final ReentrantLock _lock;
|
||||||
|
private static final String PROP_MAX_BW = "i2cp.outboundBytesPerSecond";
|
||||||
|
/** see ConnectionOptions in streaming - MTU + streaming overhead + gzip overhead */
|
||||||
|
private static final int TYP_SIZE = 1730 + 28 + 23;
|
||||||
|
private static final int MIN_RATE = 2 * TYP_SIZE;
|
||||||
|
|
||||||
public I2CPMessageProducer(I2PAppContext context) {
|
public I2CPMessageProducer(I2PAppContext context) {
|
||||||
_context = context;
|
_context = context;
|
||||||
context.statManager().createRateStat("client.sendBpsRaw", "How fast we pump out I2CP data messages", "ClientMessages", new long[] { 60*1000, 5*60*1000, 10*60*1000, 60*60*1000 });
|
_log = context.logManager().getLog(I2CPMessageProducer.class);
|
||||||
|
_lock = new ReentrantLock(true);
|
||||||
|
context.statManager().createRateStat("client.sendThrottled", "Times waited for bandwidth", "ClientMessages", new long[] { 60*1000 });
|
||||||
|
context.statManager().createRateStat("client.sendDropped", "Length of msg dropped waiting for bandwidth", "ClientMessages", new long[] { 60*1000 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the bandwidth setting
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void updateBandwidth(I2PSessionImpl session) {
|
||||||
|
String max = session.getOptions().getProperty(PROP_MAX_BW);
|
||||||
|
if (max != null) {
|
||||||
|
try {
|
||||||
|
int iMax = Integer.parseInt(max);
|
||||||
|
if (iMax > 0)
|
||||||
|
// round up to next higher TYP_SIZE for efficiency, then add some fudge for small messages
|
||||||
|
_maxBytesPerSecond = 256 + Math.max(MIN_RATE, TYP_SIZE * ((iMax + TYP_SIZE - 1) / TYP_SIZE));
|
||||||
|
else
|
||||||
|
_maxBytesPerSecond = 0;
|
||||||
|
} catch (NumberFormatException nfe) {}
|
||||||
|
}
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Setting " + _maxBytesPerSecond + " BPS max");
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send all the messages that a client needs to send to a router to establish
|
* Send all the messages that a client needs to send to a router to establish
|
||||||
* a new session.
|
* a new session.
|
||||||
*/
|
*/
|
||||||
public void connect(I2PSessionImpl session) throws I2PSessionException {
|
public void connect(I2PSessionImpl session) throws I2PSessionException {
|
||||||
|
updateBandwidth(session);
|
||||||
CreateSessionMessage msg = new CreateSessionMessage();
|
CreateSessionMessage msg = new CreateSessionMessage();
|
||||||
SessionConfig cfg = new SessionConfig(session.getMyDestination());
|
SessionConfig cfg = new SessionConfig(session.getMyDestination());
|
||||||
cfg.setOptions(session.getOptions());
|
cfg.setOptions(session.getOptions());
|
||||||
@ -99,6 +130,9 @@ class I2CPMessageProducer {
|
|||||||
*/
|
*/
|
||||||
public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, SessionTag tag,
|
public void sendMessage(I2PSessionImpl session, Destination dest, long nonce, byte[] payload, SessionTag tag,
|
||||||
SessionKey key, Set tags, SessionKey newKey, long expires) throws I2PSessionException {
|
SessionKey key, Set tags, SessionKey newKey, long expires) throws I2PSessionException {
|
||||||
|
if (!updateBps(payload.length, expires))
|
||||||
|
// drop the message... send fail notification?
|
||||||
|
return;
|
||||||
SendMessageMessage msg;
|
SendMessageMessage msg;
|
||||||
if (expires > 0) {
|
if (expires > 0) {
|
||||||
msg = new SendMessageExpiresMessage();
|
msg = new SendMessageExpiresMessage();
|
||||||
@ -111,20 +145,108 @@ class I2CPMessageProducer {
|
|||||||
Payload data = createPayload(dest, payload, tag, key, tags, newKey);
|
Payload data = createPayload(dest, payload, tag, key, tags, newKey);
|
||||||
msg.setPayload(data);
|
msg.setPayload(data);
|
||||||
session.sendMessage(msg);
|
session.sendMessage(msg);
|
||||||
updateBps(payload.length);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateBps(int len) {
|
/**
|
||||||
long now = _context.clock().now();
|
* Super-simple bandwidth throttler.
|
||||||
float period = ((float)now-_sendPeriodBeginTime)/1000f;
|
* We only calculate on a one-second basis, so large messages
|
||||||
if (period >= 1f) {
|
* (compared to the one-second limit) may exceed the limits.
|
||||||
// first term decays on slow transmission
|
* Tuned for streaming, may not work well for large datagrams.
|
||||||
_sendBps = (int)(((float)0.9f * (float)_sendBps) + ((float)0.1f*((float)_sendPeriodBytes)/period));
|
*
|
||||||
_sendPeriodBytes = len;
|
* This does poorly with low rate limits since it doesn't credit
|
||||||
_sendPeriodBeginTime = now;
|
* bandwidth across two periods. So the limit is rounded up,
|
||||||
_context.statManager().addRateData("client.sendBpsRaw", _sendBps, 0);
|
* and the min limit is set to 2x the typ size, above.
|
||||||
} else {
|
*
|
||||||
_sendPeriodBytes += len;
|
* Blocking so this could be very bad for retransmissions,
|
||||||
|
* as it could clog StreamingTimer.
|
||||||
|
* Waits are somewhat "fair" using ReentrantLock.
|
||||||
|
* While out-of-order transmission is acceptable, fairness
|
||||||
|
* reduces the chance of starvation. ReentrantLock does not
|
||||||
|
* guarantee in-order execution due to thread priority issues,
|
||||||
|
* so out-of-order may still occur. But shouldn't happen within
|
||||||
|
* the same thread anyway... Also note that small messages may
|
||||||
|
* go ahead of large ones that are waiting for the next window.
|
||||||
|
* Also, threads waiting a second time go to the back of the line.
|
||||||
|
*
|
||||||
|
* Since this is at the I2CP layer, it includes streaming overhead,
|
||||||
|
* streaming acks and retransmissions,
|
||||||
|
* gzip overhead (or "underhead" for compression),
|
||||||
|
* repliable datagram overhead, etc.
|
||||||
|
* However, it does not, of course, include the substantial overhead
|
||||||
|
* imposed by the router for the leaseset, tags, encryption,
|
||||||
|
* and fixed-size tunnel messages.
|
||||||
|
*
|
||||||
|
* @param expires if > 0, an expiration date
|
||||||
|
* @return true if we should send the message, false to drop it
|
||||||
|
*/
|
||||||
|
private boolean updateBps(int len, long expires) {
|
||||||
|
if (_maxBytesPerSecond <= 0)
|
||||||
|
return true;
|
||||||
|
//synchronized(this) {
|
||||||
|
_lock.lock();
|
||||||
|
try {
|
||||||
|
int waitCount = 0;
|
||||||
|
while (true) {
|
||||||
|
long now = _context.clock().now();
|
||||||
|
if (waitCount > 0 && expires > 0 && expires < now) {
|
||||||
|
// just say no to bufferbloat... drop the message right here
|
||||||
|
_context.statManager().addRateData("client.sendDropped", len, 0);
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.warn("Dropping " + len + " byte msg expired in queue");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long period = now - _sendPeriodBeginTime;
|
||||||
|
if (period >= 2000) {
|
||||||
|
// start new period, always let it through no matter how big
|
||||||
|
_sendPeriodBytes = len;
|
||||||
|
_sendPeriodBeginTime = now;
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("New period after idle, " + len + " bytes");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (period >= 1000) {
|
||||||
|
// start new period
|
||||||
|
// Allow burst within 2 sec, only advance window by 1 sec, and
|
||||||
|
// every other second give credit for unused bytes in previous period
|
||||||
|
if (_sendPeriodBytes > 0 && ((_sendPeriodBeginTime / 1000) & 0x01) == 0)
|
||||||
|
_sendPeriodBytes += len - _maxBytesPerSecond;
|
||||||
|
else
|
||||||
|
_sendPeriodBytes = len;
|
||||||
|
_sendPeriodBeginTime += 1000;
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("New period, " + len + " bytes");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_sendPeriodBytes + len <= _maxBytesPerSecond) {
|
||||||
|
// still bytes available in this period
|
||||||
|
_sendPeriodBytes += len;
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Sending " + len + ", Elapsed " + period + "ms, total " + _sendPeriodBytes + " bytes");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (waitCount >= 2) {
|
||||||
|
// just say no to bufferbloat... drop the message right here
|
||||||
|
_context.statManager().addRateData("client.sendDropped", len, 0);
|
||||||
|
if (_log.shouldLog(Log.WARN))
|
||||||
|
_log.warn("Dropping " + len + " byte msg after waiting " + waitCount + " times");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// wait until next period
|
||||||
|
_context.statManager().addRateData("client.sendThrottled", ++waitCount, 0);
|
||||||
|
if (_log.shouldLog(Log.DEBUG))
|
||||||
|
_log.debug("Throttled " + len + " bytes, wait #" + waitCount + ' ' + (1000 - period) + "ms" /*, new Exception()*/);
|
||||||
|
try {
|
||||||
|
//this.wait(1000 - period);
|
||||||
|
_lock.newCondition().await(1000 - period, TimeUnit.MILLISECONDS);
|
||||||
|
} catch (InterruptedException ie) {}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
_lock.unlock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -9,6 +9,7 @@ package net.i2p.client;
|
|||||||
*
|
*
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import java.util.Properties;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
import net.i2p.data.Destination;
|
import net.i2p.data.Destination;
|
||||||
@ -151,6 +152,13 @@ public interface I2PSession {
|
|||||||
*/
|
*/
|
||||||
public Destination lookupDest(Hash h, long maxWait) throws I2PSessionException;
|
public Destination lookupDest(Hash h, long maxWait) throws I2PSessionException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Does not remove properties previously present but missing from this options parameter.
|
||||||
|
* @param options non-null
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void updateOptions(Properties options);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the current bandwidth limits. Blocking.
|
* Get the current bandwidth limits. Blocking.
|
||||||
*/
|
*/
|
||||||
|
@ -221,20 +221,32 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** save some memory, don't pass along the pointless properties */
|
||||||
private Properties filter(Properties options) {
|
private Properties filter(Properties options) {
|
||||||
Properties rv = new Properties();
|
Properties rv = new Properties();
|
||||||
for (Iterator iter = options.keySet().iterator(); iter.hasNext();) {
|
for (Iterator iter = options.keySet().iterator(); iter.hasNext();) {
|
||||||
String key = (String) iter.next();
|
String key = (String) iter.next();
|
||||||
String val = options.getProperty(key);
|
if (key.startsWith("java.") ||
|
||||||
if (key.startsWith("java") ||
|
key.startsWith("user.") ||
|
||||||
key.startsWith("user") ||
|
key.startsWith("os.") ||
|
||||||
key.startsWith("os") ||
|
key.startsWith("sun.") ||
|
||||||
key.startsWith("sun") ||
|
key.startsWith("file.") ||
|
||||||
key.startsWith("file") ||
|
key.equals("line.separator") ||
|
||||||
key.startsWith("line") ||
|
key.equals("path.separator") ||
|
||||||
key.startsWith("wrapper")) {
|
key.equals("prng.buffers") ||
|
||||||
|
key.equals("router.trustedUpdateKeys") ||
|
||||||
|
key.startsWith("router.update") ||
|
||||||
|
key.startsWith("routerconsole.") ||
|
||||||
|
key.startsWith("time.") ||
|
||||||
|
key.startsWith("stat.") ||
|
||||||
|
key.startsWith("gnu.") || // gnu JVM
|
||||||
|
key.startsWith("net.i2p.router.web.") || // console nonces
|
||||||
|
key.startsWith("wrapper.")) {
|
||||||
if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping property: " + key);
|
if (_log.shouldLog(Log.DEBUG)) _log.debug("Skipping property: " + key);
|
||||||
} else if ((key.length() > 255) || (val.length() > 255)) {
|
continue;
|
||||||
|
}
|
||||||
|
String val = options.getProperty(key);
|
||||||
|
if ((key.length() > 255) || (val.length() > 255)) {
|
||||||
if (_log.shouldLog(Log.WARN))
|
if (_log.shouldLog(Log.WARN))
|
||||||
_log.warn(getPrefix() + "Not passing on property ["
|
_log.warn(getPrefix() + "Not passing on property ["
|
||||||
+ key
|
+ key
|
||||||
@ -247,6 +259,18 @@ abstract class I2PSessionImpl implements I2PSession, I2CPMessageReader.I2CPMessa
|
|||||||
return rv;
|
return rv;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the tunnel and bandwidth settings
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
public void updateOptions(Properties options) {
|
||||||
|
_options.putAll(filter(options));
|
||||||
|
_producer.updateBandwidth(this);
|
||||||
|
try {
|
||||||
|
_producer.updateTunnels(this, 0);
|
||||||
|
} catch (I2PSessionException ise) {}
|
||||||
|
}
|
||||||
|
|
||||||
void setLeaseSet(LeaseSet ls) {
|
void setLeaseSet(LeaseSet ls) {
|
||||||
_leaseSet = ls;
|
_leaseSet = ls;
|
||||||
if (ls != null) {
|
if (ls != null) {
|
||||||
|
@ -98,6 +98,13 @@ class I2PSimpleSession extends I2PSessionImpl2 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ignore, does nothing
|
||||||
|
* @since 0.8.4
|
||||||
|
*/
|
||||||
|
@Override
|
||||||
|
public void updateOptions(Properties options) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Only map message handlers that we will use
|
* Only map message handlers that we will use
|
||||||
*/
|
*/
|
||||||
|
Reference in New Issue
Block a user