package net.i2p.router.peermanager; import net.i2p.data.router.RouterInfo; import net.i2p.router.RouterContext; import net.i2p.router.networkdb.kademlia.FloodfillNetworkDatabaseFacade; import net.i2p.stat.Rate; import net.i2p.stat.RateAverages; import net.i2p.stat.RateStat; /** * Estimate how many of our tunnels the peer can join per hour. */ class CapacityCalculator { public static final String PROP_COUNTRY_BONUS = "profileOrganizer.sameCountryBonus"; /** used to adjust each period so that we keep trying to expand the peer's capacity */ static final long GROWTH_FACTOR = 5; /** the calculator estimates over a 1 hour period */ private static long ESTIMATE_PERIOD = 60*60*1000; // total of all possible bonuses should be less than 4, since // crappy peers start at 1 and the base is 5. private static final double BONUS_NEW = 0.85; private static final double BONUS_ESTABLISHED = 0.65; private static final double BONUS_SAME_COUNTRY = 0; private static final double BONUS_XOR = .25; private static final double PENALTY_UNREACHABLE = 2; // we make this a bonus for non-ff, not a penalty for ff, so we // don't drive the ffs below the default private static final double BONUS_NON_FLOODFILL = 1.0; public static double calc(PeerProfile profile) { double capacity; if (tooOld(profile)) { capacity = 1; } else { RateStat acceptStat = profile.getTunnelCreateResponseTime(); RateStat rejectStat = profile.getTunnelHistory().getRejectionRate(); RateStat failedStat = profile.getTunnelHistory().getFailedRate(); double capacity10m = estimateCapacity(acceptStat, rejectStat, failedStat, 10*60*1000); // if we actively know they're bad, who cares if they used to be good? if (capacity10m <= 0) { capacity = 0; } else { double capacity30m = estimateCapacity(acceptStat, rejectStat, failedStat, 30*60*1000); double capacity60m = estimateCapacity(acceptStat, rejectStat, failedStat, 60*60*1000); double capacity1d = estimateCapacity(acceptStat, rejectStat, failedStat, 24*60*60*1000); capacity = capacity10m * periodWeight(10*60*1000) + capacity30m * periodWeight(30*60*1000) + capacity60m * periodWeight(60*60*1000) + capacity1d * periodWeight(24*60*60*1000); } } // now take into account non-rejection tunnel rejections (which haven't // incremented the rejection counter, since they were only temporary) RouterContext context = profile.getContext(); long now = context.clock().now(); if (profile.getTunnelHistory().getLastRejectedTransient() > now - 5*60*1000) capacity = 1; else if (profile.getTunnelHistory().getLastRejectedProbabalistic() > now - 5*60*1000) capacity -= context.random().nextInt(5); // boost new profiles if (now - profile.getFirstHeardAbout() < 45*60*1000) capacity += BONUS_NEW; // boost connected peers if (profile.isEstablished()) capacity += BONUS_ESTABLISHED; // boost same country if (profile.isSameCountry()) { double bonus = BONUS_SAME_COUNTRY; String b = context.getProperty(PROP_COUNTRY_BONUS); if (b != null) { try { bonus = Double.parseDouble(b); } catch (NumberFormatException nfe) {} } capacity += bonus; } // penalize unreachable peers if (profile.wasUnreachable()) capacity -= PENALTY_UNREACHABLE; // credit non-floodfill to reduce conn limit issues at floodfills // TODO only if we aren't floodfill ourselves? RouterInfo ri = context.netDb().lookupRouterInfoLocally(profile.getPeer()); if (!FloodfillNetworkDatabaseFacade.isFloodfill(ri)) capacity += BONUS_NON_FLOODFILL; // a tiny tweak to break ties and encourage closeness, -.25 to +.25 capacity -= profile.getXORDistance() * (BONUS_XOR / 128); capacity += profile.getCapacityBonus(); if (capacity < 0) capacity = 0; return capacity; } /** * If we haven't heard from them in an hour, they aren't too useful. * */ private static boolean tooOld(PeerProfile profile) { if (profile.getIsActive(60*60*1000)) return false; else return true; } /** * Compute a tunnel accept capacity-per-hour for the given period * This is perhaps the most critical part of the peer ranking and selection * system, so adjust with great care and testing to ensure good network * performance and prevent congestion collapse. * * The baseline or "growth factor" is 5. * Rejects will not reduce the baseline. Failures will. * * @param acceptStat Accept counter (1 = 1 accept) * @param rejectStat Reject counter (1 = 1 reject) * @param failedStat Failed counter (100 = 1 fail) * * Let A = accects, R = rejects, F = fails * @return estimated and adjusted accepts per hour, for the given period * which is, more or less, max(0, 5 + (A * (A / (A + 2R))) - (4 * F)) */ private static double estimateCapacity(RateStat acceptStat, RateStat rejectStat, RateStat failedStat, int period) { Rate curAccepted = acceptStat.getRate(period); Rate curRejected = rejectStat.getRate(period); Rate curFailed = failedStat.getRate(period); RateAverages ra = RateAverages.getTemp(); double eventCount = 0; if (curAccepted != null) { eventCount = curAccepted.computeAverages(ra, false).getTotalEventCount(); // Punish for rejections. // We don't want to simply do eventCount -= rejected or we get to zero with 50% rejection, // and we don't want everybody to be at zero during times of congestion. if (eventCount > 0 && curRejected != null) { long rejected = curRejected.computeAverages(ra,false).getTotalEventCount(); if (rejected > 0) eventCount *= eventCount / (eventCount + (2 * rejected)); } } double stretch = ((double)ESTIMATE_PERIOD) / period; double val = eventCount * stretch; // Let's say a failure is 4 times worse than a rejection. // It's actually much worse than that, but with 2-hop tunnels and a 8-peer // fast pool, for example, you have a 1/7 chance of being falsely blamed. // We also don't want to drive everybody's capacity to zero, that isn't helpful. if (curFailed != null) { double failed = curFailed.computeAverages(ra, false).getTotalValues(); if (failed > 0) { //if ( (period <= 10*60*1000) && (curFailed.getCurrentEventCount() > 0) ) // return 0.0d; // their tunnels have failed in the last 0-10 minutes //else // .04 = 4.0 / 100.0 adjustment to failed val -= 0.04 * failed * stretch; } } val += GROWTH_FACTOR; if (val >= 0) { return val; } else { return 0.0d; } } private static double periodWeight(int period) { switch (period) { case 10*60*1000: return .4; case 30*60*1000: return .3; case 60*60*1000: return .2; case 24*60*60*1000: return .1; default: throw new IllegalArgumentException("undefined period passed, period [" + period + "]???"); } } }