package org.klomp.snark.dht;
/*
* From zzzot, relicensed to GPLv2
*/
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import net.i2p.I2PAppContext;
import net.i2p.data.DataHelper;
import net.i2p.data.Hash;
import net.i2p.util.Log;
import net.i2p.util.SimpleTimer2;
/**
* The tracker stores peers, i.e. Dest hashes (not nodes).
*
* @since 0.9.2
* @author zzz
*/
class DHTTracker {
private final I2PAppContext _context;
private final Torrents _torrents;
private long _expireTime;
private final Log _log;
private volatile boolean _isRunning;
/** not current, updated by cleaner */
private int _peerCount;
/** not current, updated by cleaner */
private int _torrentCount;
/** stagger with other cleaners */
private static final long CLEAN_TIME = 199*1000;
/** no guidance in BEP 5; Vuze is 8h */
private static final long MAX_EXPIRE_TIME = 3*60*60*1000L;
private static final long MIN_EXPIRE_TIME = 15*60*1000;
private static final long DELTA_EXPIRE_TIME = 3*60*1000;
private static final int MAX_PEERS = 2000;
private static final int MAX_PEERS_PER_TORRENT = 150;
private static final int ABSOLUTE_MAX_PER_TORRENT = MAX_PEERS_PER_TORRENT * 2;
private static final int MAX_TORRENTS = 400;
DHTTracker(I2PAppContext ctx) {
_context = ctx;
_torrents = new Torrents();
_expireTime = MAX_EXPIRE_TIME;
_log = _context.logManager().getLog(DHTTracker.class);
}
public void start() {
_isRunning = true;
new Cleaner();
}
void stop() {
_torrents.clear();
_isRunning = false;
}
void announce(InfoHash ih, Hash hash, boolean isSeed) {
if (_log.shouldLog(Log.DEBUG))
_log.debug("Announce " + hash + " for " + ih);
Peers peers = _torrents.get(ih);
if (peers == null) {
if (_torrents.size() >= MAX_TORRENTS)
return;
peers = new Peers();
Peers peers2 = _torrents.putIfAbsent(ih, peers);
if (peers2 != null)
peers = peers2;
}
if (peers.size() < ABSOLUTE_MAX_PER_TORRENT) {
Peer peer = new Peer(hash.getData());
Peer peer2 = peers.putIfAbsent(peer, peer);
if (peer2 != null)
peer = peer2;
peer.setLastSeen(_context.clock().now());
// don't let false trump true, as not all sources know the seed status
if (isSeed)
peer.setSeed(true);
} else {
// We could update setLastSeen if he is already
// in there, but that would tend to keep
// the same set of peers.
// So let it expire so new ones can come in.
//Peer peer = peers.get(hash);
//if (peer != null)
// peer.setLastSeen(_context.clock().now());
}
}
void unannounce(InfoHash ih, Hash hash) {
Peers peers = _torrents.get(ih);
if (peers == null)
return;
peers.remove(hash);
}
/**
* Caller's responsibility to remove himself from the list
*
* @param noSeeds true if we do not want seeds in the result
* @return list or empty list (never null)
*/
@SuppressWarnings({"unchecked", "rawtypes"})
List<Hash> getPeers(InfoHash ih, int max, boolean noSeeds) {
Peers peers = _torrents.get(ih);
if (peers == null || max <= 0)
return Collections.emptyList();
List<Peer> rv = new ArrayList<Peer>(peers.values());
int size = rv.size();
if (max < size)
Collections.shuffle(rv, _context.random());
if (noSeeds) {
int i = 0;
for (Iterator<Peer> iter = rv.iterator(); iter.hasNext(); ) {
if (iter.next().isSeed())
iter.remove();
else if (++i >= max)
break;
}
if (max < rv.size())
rv = rv.subList(0, max);
} else {
if (max < size)
rv = rv.subList(0, max);
}
// a Peer is a Hash
List rv1 = rv;
List<Hash> rv2 = rv1;
return rv2;
}
/**
* Debug info, HTML formatted
*/
public void renderStatusHTML(StringBuilder buf) {
buf.append("DHT tracker: ").append(_torrentCount).append(" torrents ")
.append(_peerCount).append(" peers ")
.append(DataHelper.formatDuration(_expireTime)).append(" expiration<br>");
}
private class Cleaner extends SimpleTimer2.TimedEvent {
public Cleaner() {
super(SimpleTimer2.getInstance(), 2 * CLEAN_TIME);
}
public void timeReached() {
if (!_isRunning)
return;
long now = _context.clock().now();
int torrentCount = 0;
int peerCount = 0;
boolean tooMany = false;
for (Iterator<Peers> iter = _torrents.values().iterator(); iter.hasNext(); ) {
Peers p = iter.next();
int recent = 0;
for (Iterator<Peer> iterp = p.values().iterator(); iterp.hasNext(); ) {
Peer peer = iterp.next();
if (peer.lastSeen() < now - _expireTime)
iterp.remove();
else {
recent++;
peerCount++;
}
}
if (recent > MAX_PEERS_PER_TORRENT) {
// too many, delete at random
// TODO sort and remove oldest?
// TODO per-torrent adjustable expiration?
for (Iterator<Peer> iterp = p.values().iterator(); iterp.hasNext() && p.size() > MAX_PEERS_PER_TORRENT; ) {
iterp.next();
iterp.remove();
peerCount--;
}
torrentCount++;
tooMany = true;
} else if (recent <= 0) {
iter.remove();
} else {
torrentCount++;
}
}
if (peerCount > MAX_PEERS)
tooMany = true;
if (tooMany)
_expireTime = Math.max(_expireTime - DELTA_EXPIRE_TIME, MIN_EXPIRE_TIME);
else
_expireTime = Math.min(_expireTime + DELTA_EXPIRE_TIME, MAX_EXPIRE_TIME);
if (_log.shouldLog(Log.DEBUG))
_log.debug("DHT tracker cleaner done, now with " +
torrentCount + " torrents, " +
peerCount + " peers, " +
DataHelper.formatDuration(_expireTime) + " expiration");
_peerCount = peerCount;
_torrentCount = torrentCount;
schedule(tooMany ? CLEAN_TIME / 3 : CLEAN_TIME);
}
}
}