/*
* PeerCoordinator - Coordinates which peers do what (up and downloading).
* Copyright (C) 2003 Mark J. Wielaard
*
* This file is part of Snark.
*
* This program is free software; you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation; either version 2, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program; if not, write to the Free Software Foundation, Inc., 59 Temple
* Place - Suite 330, Boston, MA 02111-1307, USA.
*/
package org.klomp.snark;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Timer;
import java.util.logging.Level;
import java.util.logging.Logger;
import p2pp.DownloadProgressListener;
/**
* Coordinates what peer does what.
*/
public class PeerCoordinator implements PeerListener
{
final MetaInfo metainfo;
final Storage storage;
// package local for access by CheckDownLoadersTask
final static long CHECK_PERIOD = 20 * 1000; // 20 seconds
final static int MAX_CONNECTIONS = 24;
final static int MAX_UPLOADERS = 4;
// Approximation of the number of current uploaders.
// Resynced by PeerChecker once in a while.
int uploaders = 0;
// final static int MAX_DOWNLOADERS = MAX_CONNECTIONS;
// int downloaders = 0;
private long uploaded;
private long downloaded;
// synchronize on this when changing peers or downloaders
public final List<Peer> peers = new ArrayList<Peer>();
/** Timer to handle all periodical tasks. */
private final Timer timer = new Timer(true);
private final byte[] id;
// Some random wanted pieces. Is visible to subclasses [LIMA].
protected final List<Integer> wantedPieces;
private boolean halted = false;
private final CoordinatorListener listener;
private TrackerClient client;
// Monitor piece download progress [LIMA].
protected DownloadProgressListener downloadProgress;
public PeerCoordinator (byte[] id, MetaInfo metainfo, Storage storage,
CoordinatorListener listener)
{
this.id = id;
this.metainfo = metainfo;
this.storage = storage;
this.listener = listener;
// Make a random list of piece numbers
wantedPieces = new ArrayList<Integer>();
BitField bitfield = storage.getBitField();
for (int i = 0; i < metainfo.getPieces(); i++) {
if (!bitfield.get(i)) {
wantedPieces.add(i);
}
}
Collections.shuffle(wantedPieces);
// Install a timer to check the uploaders.
timer.schedule(new PeerCheckerTask(this), CHECK_PERIOD, CHECK_PERIOD);
}
/**
* Used to set the progress listener [LIMA].
*/
public void setDownloadProgressListener(DownloadProgressListener progress) {
this.downloadProgress = progress;
progress.setPeerCoordinator(this);
}
public void setTracker (TrackerClient client)
{
this.client = client;
}
public byte[] getID ()
{
return id;
}
public boolean completed ()
{
return storage.complete();
}
public int getPeers ()
{
synchronized (peers) {
return peers.size();
}
}
/**
* Returns how many bytes are still needed to get the complete file.
*/
public long getLeft ()
{
// XXX - Only an approximation.
return storage.needed() * metainfo.getPieceLength(0);
}
/**
* Returns the total number of uploaded bytes of all peers.
*/
public long getUploaded ()
{
return uploaded;
}
/**
* Returns the total number of downloaded bytes of all peers.
*/
public long getDownloaded ()
{
return downloaded;
}
public MetaInfo getMetaInfo ()
{
return metainfo;
}
public boolean needPeers ()
{
synchronized (peers) {
return !halted && peers.size() < MAX_CONNECTIONS;
}
}
public void halt ()
{
halted = true;
synchronized (peers) {
// Stop peer checker task.
timer.cancel();
// Stop peers.
Iterator it = peers.iterator();
while (it.hasNext()) {
Peer peer = (Peer)it.next();
peer.disconnect();
it.remove();
}
}
}
public void connected (Peer peer)
{
if (halted) {
peer.disconnect(false);
return;
}
synchronized (peers) {
if (peerIDInList(peer.getPeerID(), peers)) {
log.log(Level.FINER, "Already connected to: " + peer);
peer.disconnect(false); // Don't deregister this
// connection/peer.
} else {
log.log(Level.FINER, "New connection to peer: " + peer);
// Add it to the beginning of the list.
// And try to optimistically make it a uploader.
peers.add(0, peer);
unchokePeer();
if (listener != null) {
listener.peerChange(this, peer);
}
}
}
}
private static boolean peerIDInList (PeerID pid, List peers)
{
Iterator it = peers.iterator();
while (it.hasNext()) {
//if (pid.sameID(((Peer)it.next()).getPeerID())) {
// A peerId is considered already in the list if contantains
// a peerId with the same ip and port [LIMA].
if(pid.equals(((Peer) it.next()).getPeerID())) {
return true;
}
}
return false;
}
public void addPeer (final Peer peer)
{
if (halted) {
peer.disconnect(false);
return;
}
boolean need_more;
synchronized (peers) {
need_more = !peer.isConnected() && peers.size() < MAX_CONNECTIONS;
}
if (need_more) {
// Run the peer with us as listener and the current bitfield.
final PeerListener listener = this;
final BitField bitfield = storage.getBitField();
Runnable r = new Runnable() {
public void run ()
{
peer.runConnection(listener, bitfield);
}
};
String threadName = peer.toString();
new Thread(r, threadName).start();
//} else if (log.getLevel().intValue() <= Level.FINER.intValue()) {
} else {
if (peer.isConnected()) {
log.log(Level.FINER, "Add peer already connected: " + peer);
} else {
log.log(Level.FINER, "MAX_CONNECTIONS = " + MAX_CONNECTIONS
+ " not accepting extra peer: " + peer);
}
}
}
// (Optimistically) unchoke. Should be called with peers synchronized
void unchokePeer ()
{
// linked list will contain all interested peers that we choke.
// At the start are the peers that have us unchoked at the end the
// other peer that are interested, but are choking us.
List<Peer> interested = new LinkedList<Peer>();
Iterator it = peers.iterator();
while (it.hasNext()) {
Peer peer = (Peer)it.next();
if (uploaders < MAX_UPLOADERS && peer.isChoking()
&& peer.isInterested()) {
if (!peer.isChoked()) {
interested.add(0, peer);
} else {
interested.add(peer);
}
}
}
while (uploaders < MAX_UPLOADERS && interested.size() > 0) {
Peer peer = interested.remove(0);
log.log(Level.FINER, "Unchoke: " + peer);
peer.setChoking(false);
uploaders++;
// Put peer back at the end of the list.
peers.remove(peer);
peers.add(peer);
}
}
public byte[] getBitMap ()
{
return storage.getBitField().getFieldBytes();
}
/**
* Returns true if we don't have the given piece yet.
*/
public boolean gotHave (Peer peer, int piece)
{
if (listener != null) {
listener.peerChange(this, peer);
}
synchronized (wantedPieces) {
return wantedPieces.contains(new Integer(piece));
}
}
/**
* Returns true if the given bitfield contains at least one piece we are
* interested in.
*/
public boolean gotBitField (Peer peer, BitField bitfield)
{
if (listener != null) {
listener.peerChange(this, peer);
}
synchronized (wantedPieces) {
Iterator it = wantedPieces.iterator();
while (it.hasNext()) {
int i = ((Integer)it.next()).intValue();
if (bitfield.get(i)) {
return true;
}
}
}
return false;
}
/**
* Returns one of pieces in the given BitField that is still wanted or -1 if
* none of the given pieces are wanted.
*/
public int wantPiece (Peer peer, BitField havePieces)
{
if (halted) {
return -1;
}
synchronized (wantedPieces) {
Integer piece = null;
Iterator it = wantedPieces.iterator();
while (piece == null && it.hasNext()) {
Integer i = (Integer)it.next();
if (havePieces.get(i.intValue())) {
it.remove();
piece = i;
}
}
if (piece == null) {
return -1;
}
// We add it back at the back of the list. It will be removed
// if gotPiece is called later. This means that the last
// couple of pieces might very well be asked from multiple
// peers but that is OK.
wantedPieces.add(piece);
// Notify listener that a piece has been requested [LIMA].
if(downloadProgress != null)
downloadProgress.pieceRequested(peer, piece);
return piece.intValue();
}
}
/**
* Returns a byte array containing the requested piece or null of the piece
* is unknown.
*/
public byte[] gotRequest (Peer peer, int piece)
throws IOException
{
if (halted) {
return null;
}
try {
return storage.getPiece(piece);
} catch (IOException ioe) {
Snark.abort("Error reading storage", ioe);
return null; // Never reached.
}
}
/**
* Called when a peer has uploaded some bytes of a piece.
*/
public void uploaded (Peer peer, int size)
{
uploaded += size;
if (listener != null) {
listener.peerChange(this, peer);
}
}
/**
* Called when a peer has downloaded some bytes of a piece.
*/
public void downloaded (Peer peer, int size)
{
downloaded += size;
if (listener != null) {
listener.peerChange(this, peer);
}
}
/**
* Returns false if the piece is no good (according to the hash). In that
* case the peer that supplied the piece should probably be blacklisted.
*/
public boolean gotPiece (Peer peer, int piece, byte[] bs)
throws IOException
{
if (halted) {
return true; // We don't actually care anymore.
}
synchronized (wantedPieces) {
Integer p = new Integer(piece);
if (!wantedPieces.contains(p)) {
log.log(Level.FINER, peer + " piece " + piece
+ " no longer needed");
// No need to announce have piece to peers.
// Assume we got a good piece, we don't really care anymore.
return true;
}
try {
if (storage.putPiece(piece, bs)) {
log.log(Level.FINER, "Recv p" + piece + " " + peer);
// Notify progress listener about completed download
// of a piece [LIMA].
if(downloadProgress != null)
downloadProgress.pieceDownloaded(peer, piece);
} else {
// Oops. We didn't actually download this then... :(
downloaded -= metainfo.getPieceLength(piece);
log.log(Level.INFO, "Got BAD piece " + piece + " from "
+ peer);
return false; // No need to announce BAD piece to peers.
}
} catch (IOException ioe) {
Snark.abort("Error writing storage", ioe);
}
wantedPieces.remove(p);
}
// Announce to the world we have it!
synchronized (peers) {
Iterator it = peers.iterator();
while (it.hasNext()) {
Peer p = (Peer)it.next();
if (p.isConnected()) {
p.have(piece);
}
}
}
if (completed()) {
client.interrupt();
// Notify about completed download of all pieces [LIMA].
if(downloadProgress != null)
downloadProgress.downloadComplete();
}
return true;
}
public void gotChoke (Peer peer, boolean choke)
{
log.log(Level.FINER, "Got choke(" + choke + "): " + peer);
if (listener != null) {
listener.peerChange(this, peer);
}
}
public void gotInterest (Peer peer, boolean interest)
{
if (interest) {
synchronized (peers) {
if (uploaders < MAX_UPLOADERS) {
if (peer.isChoking()) {
uploaders++;
peer.setChoking(false);
log.log(Level.FINER, "Unchoke: " + peer);
}
}
}
}
if (listener != null) {
listener.peerChange(this, peer);
}
}
public void disconnected (Peer peer)
{
log.log(Level.FINER, "Disconnected " + peer);
synchronized (peers) {
// Make sure it is no longer in our lists
if (peers.remove(peer)) {
// Unchoke some random other peer
unchokePeer();
}
}
if (listener != null) {
listener.peerChange(this, peer);
}
}
protected static final Logger log = Logger.getLogger("org.klomp.snark.peer");
}